Getting started with OpenZeppelin Contracts for Cairo

Tutorial

cairo-1

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.