Testing StarkNet contracts made easy with Protostar
Tomasz Rejowski•Jul 15, 2022•9 min readIn this article, I would like to introduce you to using Protostar for writing smart contracts on StarkNet. I will get you through the whole development cycle, including:
- Initializing the project
- Writing a contract
- Unit testing a contract
- Deploying it on testnet
This article is currently based on the 0.6.0 version of Protostar and may be updated in the future.
Background
Before I begin — a quick explanation of basic terms (or buzzwords) for those who come from different backgrounds than web3:
Ethereum is a blockchain, which allows you to write smart contracts.
A smart contract is a kind of computer program, stored on the blockchain. It’s transparent, verifiable, and also allows you to write/read state of the blockchain. This is exactly the thing that allows us to buy monkey pictures with ETH.
StarkNet — a zero-knowledge rollup for Ethereum. That is the „layer 2” solution over Ethereum, which takes the load off the main network onto the said layer. This allows to reduce costs and improve network scalability.
Cairo — a language for writing smart contracts for StarkNet. Ethereum has its own special language — Solidity.
And finally — Protostar is a toolchain we develop at Software Mansion, which allows you to write tests, install dependencies and compile/deploy your projects written in Cairo.
Prerequisites
You have to start by installing Protostar.
It is pretty straightforward, and you only need to copy a one-liner into your console as shown here.
After doing that, to make sure everything works, run theprotostar —-version The command output should look like this:
Protostar version: 0.6.0
Cairo-lang version: 0.10.0
You can always run this command in order to find out which cairo-lang version is supported in the given release of protostar. Note that with older versions you might not be able to use all the newer cairo features, or deploy the contracts properly to testnet/mainnet.
Creating a project
To initialize the project, type protostar init into the console.
You will be prompted if you want to use the current directory as the project directory (but only if protostar detects some .cairo files nearby — so you might not see this step).
Your current directory may be a cairo project.
Do you want to adapt current working directory as a project instead of creating a new project?. [y/n]:
We want to create a separate one, so we type n and press enter
Next up, we’re asked to provide the project name:
project directory name: voting-contract
And that’s all, your project has been created!
Project structure
If you cd into the created directory, you can see the generated project structure:
cd voting-contract
❯ tree -a
.
├── .git
│ ├── HEAD
│ ├── config
│ ├── description
│ ├── hooks
│ │ ├── applypatch-msg.sample
│ │ ├── commit-msg.sample
│ │ ├── fsmonitor-watchman.sample
│ │ ├── post-update.sample
│ │ ├── pre-applypatch.sample
│ │ ├── pre-commit.sample
│ │ ├── pre-merge-commit.sample
│ │ ├── pre-push.sample
│ │ ├── pre-rebase.sample
│ │ ├── pre-receive.sample
│ │ ├── prepare-commit-msg.sample
│ │ ├── push-to-checkout.sample
│ │ └── update.sample
│ ├── info
│ │ └── exclude
│ ├── objects
│ │ ├── info
│ │ └── pack
│ └── refs
│ ├── heads
│ └── tags
├── lib
├── protostar.toml
├── src
│ └── main.cairo
└── tests
└── test_main.cairo
12 directories, 20 files
Here are a few things about this structure:
- Libraries installed with Protostar will be added into
libdirectory. - There is a git repository created by default. Protostar uses git submodules for managing dependencies, as there is no official package manager for cairo.
- There is a configuration file called
protostar.toml. I will go through its content in the next subchapter. - There are 2 source directories created by default:
srcandtests— but don’t worry! You can change things up as you wish, as long as you reflect the changes inprotostar.toml
Feel free to remove the contents of main.cairo and the entire tests/test_main.cairo file — we won’t need it in this tutorial.
The protostar.toml file
This is the “project configuration” file — Protostar will read it, to check what it should compile, and where to load libraries from.
The configuration from this file is overridable by console options.
The default generated protostar.toml looks something like this:
All the commands run with Protostar can be configured from within this file (so you don’t have to type as much when executing commands 😀).
There also are profiles, which you can use for different environments or run configurations (i.e. separate ones for CI/local or testnet/devnet). We won’t touch on that deeper in this article — you can check them out yourself here.
The contract
Our task is going to be to write a contract, which allows us to conduct explicit voting procedures on different topics. Let’s establish some prerequisites:
- there will be 2 choices: yes/no, and only a limited range of users can take part in each voting;
- each user can vote only once;
- the voting is permanent and stored on the blockchain.
Let’s write the contract!
First, create a new file src/voting.cairo.
It will contain the voting-related logic of the contract.
The state
In voting.cairo, create a structure and storage variable which stores the voting state.
Now, let’s add the user state. It will contain two fields:
voted— will contain 1, if the user already voted — otherwise it will be empty (the contract won’t store any data about vote choice)allowed— will store information about user permission for voting
Registering voters
We need to initialize state upon contract deployment.
Cairo provides constructors for that purpose.
First, we will write a helper function register_voters — it will go through the list of voters’ addresses and register them in voter_info state.
Let’s create unit tests for that function, to make sure the written state is correct.
In order to do that, create a new file in the folder tests called test_registration.cairo.
Protostar will collect the file using its name, checking if it begins with test_ prefix, and has @external functions, which names begin with test_ .
Put the following function into the test file:
Now you can run protostar test while being in the project’s root, and you should see one passing test case.
Voting
Now let’s write the method for voting.
Put the following functions (making sure you understand how they work 😉) in voting.cairo .
This method is complex — we want to test each path, making sure we cover all the functionality.
Testing whether a user can vote “yes”/”no” and vote state is updated correctly
This test uses a hint with the start_prank function called inside — it’s one of protostar’s cheatcodes.
They enable you to cheat a little and perform actions that normally aren’t allowed on the blockchain — like changing the timestamp, caller address, etc.
start_prank changes the calling contract address, making it possible to act as another user. In this case, we want to change our identity to the user, which is permitted to vote.
You probably noticed, that there is a chunk of repeated code — we need to register users in both cases, so they are permitted to vote. We naturally would like to extract this common part, to keep the code clean.
The __setup__ function does just that. It makes it easy to extract the common parts and caches the starknet state as well, so your tests can run faster.
Beware that this function will set up the environment for each function in the test suite — so for context isolation, we’ll put those 2 tests in a separate file- tests/test_voting_success.cairo.
In our case, code with __setup__ will look something like this (don’t forget to mark __setup__ as @view/@external , or else Protostar will not be able to detect it):
This seems much cleaner! There is one more thing you should note here — the usage of context variables inside the hint.
It’s another Protostar goodie, which lets you store the variable (boolean, string, integer) in the context for later retrieval in the test.
Testing if a user which is not permitted to vote cannot in fact vote
Let’s put this, and the next one in a separate file called tests/test_voting_failure.cairo
This test uses another cheat code — expect_revert, which checks if the function reverts to the given type and error message.
Protostar will check all error messages in cairo stack trace to match the one from the parameter.
Neither error_type or error_message is required — you can use either or none, to match any error or one with a specific type/message.
Testing if a user can vote only once
That concludes the tests for the voting function — run protostar test to make sure you’ve implemented everything correctly.
Implementing getters
We want to let any third party check current results.
Create a new file called src/getters.cairo.
Implementation is pretty straightforward:
Contract’s constructor
We need to populate the contract with allowed voters for a given contract instance.
In order to do that, we need to pass an array of permitted user addresses to the constructor.
The function below shows how to do that (put it insidemain.cairo)
Integration testing
Now we would like to test the whole contract, along with the constructor.
In order to do that, we can deploy the contract using another cheatcode called deploy_contract.
Create a new file: tests/test_integration.cairo with this content:
deploy_contract is another cheatcode that allows deploying a contract from the test. This approach allows us to “integration test” a few contracts, by deploying them and interacting with any of them. It accepts a contract path and calldata for the constructor.
If all went well — you should see 6 passing tests after executing protostar test.
Test hierarchy
To make tests most speedy, and to make sure you are testing all the functionality in detail, you should be using the testing approach protostar is designed to work with best.
This basically means that most of your tests should be unit tests. Integration tests should be treated more like end-to-end scenarios — they are heavy to run and should cover the interaction between at least 2 contracts (the case above is just for demo purposes).
So the takeaway is — don’t deploy unless you are absolutely sure you need to. Most of the cases can be implemented by importing the interface method and using it directly in your tests. We’ve had success with this testing approach in our projects, in which we didn’t have a need for using this cheatcode.
Deploy the contract
The code is finished and tested — we can compile it and deploy it to testnet!
❯ protostar build
❯ protostar deploy ./build/main.json --inputs <i.e. your account address> <i.e your friends account address> --network alpha-goerli
And that’s it! I hope you got to this point and didn’t get super confused — if you did, you can try asking for some support on StarkNet’s discord (we have our own channel there!) or look through the complete project in this repository.
PS: Special thanks to Maksymilian Demitraszek for help with this article, and for the brilliant idea for the demo contract 👨🔬
