We recently announced the first release of the OpenZeppelin Contracts for Cairo library, a smart contract library written in Cairo for StarkNet, a Zero Knowledge Rollup.
In this article we will provide you with a 0-to-1 tutorial as well as a brief introduction to some Cairo-specific coding aspects, such as our Extensibility Pattern.
First time?
Before installing Cairo on your machine, you need to install gmp
:
sudo apt install -y libgmp3-dev # linux brew install gmp # mac
If you have any troubles installing gmp on your Apple M1 computer, here’s a list of potential solutions.
Setting up the project
Create a new directory, cd
into it, and create a Python virtual environment:
mkdir my-project cd my-project python3 -m venv env source env/bin/activate
For this project, we will install the OpenZeppelin Contracts for Cairo library and Nile—a development environment for Cairo made by OpenZeppelin.
pip install cairo-nile openzeppelin-cairo-contracts
Finally, we can run nile init
which installs the Cairo language itself, a local network, a testing framework, and a sample project to quickly get started.
nile init (...) ✅ Dependencies successfully installed 🗄 Creating project directory tree ⛵️ Nile project ready! Try running: nile compile
Using the library
For this example we’ll create an ERC20 token. First, create a MyToken.cairo
file and write:
%lang starknet from openzeppelin.token.erc20.ERC20 import constructor
With this, we are importing and automatically re-exporting all of the functions contained in the ERC20 preset, making a fully functional token.
Deploying the token
To deploy our token, first we will run a local StarkNet network with nile node
nile node
Now, we will compile and deploy our MyToken.cairo
contract, passing the parameters to its constructor:
nile compile nile deploy MyToken <name> <symbol> <decimals> <initial_supply> <recipient> --alias my_token
Alternatively, we can use Nile’s scripting API to write a script like this one:
# scripts/deploy.py def run(nre): print(“Compiling contract…”) nre.compile(["contracts/MyToken.cairo"]) # we compile our contract first print(“Deploying contract…”) name = str(str_to_felt("MyToken")) symbol = str(str_to_felt("MTK")) decimals = "18" recipient = "0x057e792bbff407d7a128e61a722721bcc5ca8cf0488d4fe2d72fadd577e1c194" params = [name, symbol, decimals, "100", "0", recipient] address, abi = nre.deploy("MyToken", params, alias="my_token") print(f”ABI: {abi},\nContract address: {address}”) # Auxiliary functions def str_to_felt(text): b_text = bytes(text, "ascii") return int.from_bytes(b_text, "big") def uint(a): return(a, 0)
Execute it with nile run
:
nile run scripts/deploy.py
Interacting with the token
Now that our token has been deployed to our local network, we can call functions on it:
nile call my_token totalSupply 100 0
And that’s it! We have deployed our own ERC20 token successfully.
Writing a custom token
To write a custom ERC20 using the OpenZeppelin library, we first need to import the following functions from the ERC20 library:
%lang starknet from starkware.cairo.common.cairo_builtins import HashBuiltin from starkware.cairo.common.uint256 import Uint256 from openzeppelin.token.erc20.library import ( ERC20_name, ERC20_symbol, ERC20_totalSupply, ERC20_decimals, ERC20_balanceOf, ERC20_allowance, ERC20_initializer, ERC20_approve, ERC20_increaseAllowance, ERC20_decreaseAllowance, ERC20_transfer, ERC20_transferFrom, ERC20_mint )
Since, unlike Solidity, Cairo has no inheritance mechanism, we will need to manually import and re-export all of the ERC20 functions in our contract, like this:
@view func name{ syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr }() -> (name: felt): let (name) = ERC20_name() return (name) end
For simplicity we will not paste the whole code in here, but you can see a full example in our repository.
Once you have a basic, fully compliant ERC20 token, you can add more functionality—or extend existing functionality—by adding more logic to the external functions. For example, if we wanted to add pausable functionality to transfer
, this is how it would look:
from openzeppelin.security.pausable import Pausable_when_not_paused @external func transfer{ syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr }(recipient: felt, amount: Uint256) -> (success: felt): Pausable_when_not_paused() ERC20_transfer(recipient, amount) return (TRUE) end
The Extensibility Pattern
You may have noticed that we explicitly imported all of the ERC20 functionality we want —including both functions and storage. This is because, unlike Solidity, Cairo has no native extensibility mechanisms,such as inheritance or traits. For this reason, we came up with our own pattern to write smart contract libraries in Cairo. We call it The Extensibility Pattern.
The Extensibility Pattern contains two kinds of modules: library modules, which expose functionality to build contracts reusing predefined storage or functions (erc20
, pausable
, ownable
, etc.); and presets contracts, which are ready-to-use contracts and also serve as examples of how to use the libraries (ERC20_Upgradeable
, ERC721_Mintable_Burnable
, etc.). Additional information can be found in the official documentation.
Conclusion
That’s it! Although Cairo is still a new language, we were able to streamline our StarkNet development a lot by leveraging the OpenZeppelin Contracts for Cairo library, the Extensibility Pattern, and Nile.
This is all very new and there is a lot to improve. We would love to hear your feedback! Feel free to ask for features by opening an issue on GitHub or to ask for support in our forum.