├── wizard.png ├── contracts └── UwuToken.cairo ├── tests └── test_contract.py ├── scripts ├── deploy.py └── transfer.py ├── .gitignore └── README.md /wizard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martriay/cairo-workshop/HEAD/wizard.png -------------------------------------------------------------------------------- /contracts/UwuToken.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | from openzeppelin.token.erc20.presets.ERC20 import ( 4 | constructor, 5 | name, 6 | symbol, 7 | totalSupply, 8 | decimals, 9 | balanceOf, 10 | allowance, 11 | transfer, 12 | transferFrom, 13 | approve, 14 | increaseAllowance, 15 | decreaseAllowance, 16 | ) 17 | -------------------------------------------------------------------------------- /tests/test_contract.py: -------------------------------------------------------------------------------- 1 | """contract.cairo test file.""" 2 | import os 3 | 4 | import pytest 5 | from starkware.starknet.testing.starknet import Starknet 6 | 7 | # The path to the contract source code. 8 | CONTRACT_FILE = os.path.join("contracts", "contract.cairo") 9 | 10 | 11 | # The testing library uses python's asyncio. So the following 12 | # decorator and the ``async`` keyword are needed. 13 | @pytest.mark.asyncio 14 | async def test_increase_balance(): 15 | """Test increase_balance method.""" 16 | # Create a new Starknet class that simulates the StarkNet 17 | # system. 18 | starknet = await Starknet.empty() 19 | 20 | # Deploy the contract. 21 | contract = await starknet.deploy( 22 | source=CONTRACT_FILE, 23 | ) 24 | 25 | # Invoke increase_balance() twice. 26 | await contract.increase_balance(amount=10).invoke() 27 | await contract.increase_balance(amount=20).invoke() 28 | 29 | # Check the result of get_balance(). 30 | execution_info = await contract.get_balance().call() 31 | assert execution_info.result == (30,) 32 | -------------------------------------------------------------------------------- /scripts/deploy.py: -------------------------------------------------------------------------------- 1 | import time 2 | from nile.utils import * 3 | 4 | ALIAS = "uwu_token" 5 | decimals = 18 6 | 7 | def run(nre): 8 | account_a = nre.get_or_deploy_account("ACCOUNT_A") 9 | 10 | name = str_to_felt("UwuToken") 11 | symbol = str_to_felt("UWU") 12 | initial_supply = to_uint(to_decimals(1337)) 13 | recipient = int(account_a.address, 16) 14 | 15 | arguments = [ 16 | name, 17 | symbol, 18 | decimals, 19 | *initial_supply, 20 | recipient 21 | ] 22 | 23 | token_address, _ = nre.deploy("UwuToken", arguments, alias=ALIAS) 24 | print("UwuToken deployed at", token_address) 25 | 26 | wait = 1 # seconds 27 | print(f"Waiting {wait} seconds for it to get confirmed") 28 | time.sleep(wait) 29 | 30 | supply = from_hex(nre.call("uwu_token", "totalSupply")[0]) 31 | print("total supply:", from_decimals(supply)) 32 | 33 | name = nre.call("uwu_token", "name")[0] 34 | print("token name:", felt_to_str(name)) 35 | 36 | symbol = nre.call("uwu_token", "symbol")[0] 37 | print("token symbol:", felt_to_str(symbol)) 38 | 39 | 40 | def from_decimals(x): 41 | return x / (10 ** decimals) 42 | 43 | def to_decimals(x): 44 | return x * (10 ** decimals) 45 | 46 | def from_hex(x): 47 | return int(x, 16) 48 | -------------------------------------------------------------------------------- /scripts/transfer.py: -------------------------------------------------------------------------------- 1 | import time 2 | from nile.utils import * 3 | 4 | ALIAS = "uwu_token" 5 | decimals = 18 6 | 7 | def run(nre): 8 | account_a = nre.get_or_deploy_account("ACCOUNT_A") 9 | account_b = nre.get_or_deploy_account("ACCOUNT_B") 10 | token_address, _ = nre.get_deployment(ALIAS) 11 | 12 | print_balance(nre, account_a.address, 'a') 13 | print_balance(nre, account_b.address, 'b') 14 | 15 | recipient = from_hex(account_b.address) 16 | amount = to_uint(to_decimals(0.5)) 17 | 18 | print(f"transfer {from_decimals(from_uint(amount))} to {account_b.address}") 19 | account_a.send(token_address, 'transfer', [recipient, *amount], max_fee=0) 20 | 21 | wait = 1 # seconds 22 | print(f"Waiting {wait} seconds for it to get confirmed") 23 | time.sleep(wait) 24 | 25 | print_balance(nre, account_a.address, 'a') 26 | print_balance(nre, account_b.address, 'b') 27 | 28 | 29 | def get_balance(nre, address): 30 | balance = nre.call(ALIAS, "balanceOf", [from_hex(address)])[0] 31 | return from_hex(balance) 32 | 33 | def print_balance(nre, address, alias): 34 | balance = get_balance(nre, address) 35 | print(f"balance {alias}", from_decimals(balance)) 36 | 37 | def from_decimals(x): 38 | return x / (10 ** decimals) 39 | 40 | def to_decimals(x): 41 | return x * (10 ** decimals) 42 | 43 | def from_hex(x): 44 | return int(x, 16) 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .temp/ 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | pip-wheel-metadata/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # vscode project settings 135 | .vscode/ 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Cairo (workshop) 👶🏻✨ 2 | 3 | Along with the slides ([🇬🇧eng](https://docs.google.com/presentation/d/1huYaO3NU8rJlqoQlyX8OhOgqMzq7P7B03_mEddPUJsE) / [🇪🇸spa](https://docs.google.com/presentation/d/1LUsjUU82w-Gs69UuBwrykYGtwCkf2RjzPT70J0FJAw8)), this repository works as an introductory guide to develop StarkNet smart contracts with the [Cairo](cairo-lang.org) programming language, the [OpenZeppelin Contracts for Cairo](https://github.com/OpenZeppelin/cairo-contracts/) library, and the [Nile](https://github.com/OpenZeppelin/nile/) development environment. 4 | 5 | #### Resources: 6 | 7 | - [Cairo by Example](https://perama-v.github.io/cairo/by-example/) (by Perama) 8 | - [StarkNet documentation](https://starknet.io/docs/) 9 | - [OpenZeppelin documentation](https://docs.openzeppelin.com/contracts-cairo) 10 | 11 | ## 1. Installation 12 | 13 | ### First time? 14 | 15 | Before installing Cairo on your machine, you need to install `gmp`: 16 | 17 | ```bash 18 | sudo apt install -y libgmp3-dev # linux 19 | brew install gmp # mac 20 | ``` 21 | 22 | > If you have any troubles installing gmp on your Apple M1 computer, [here’s a list of potential solutions](https://github.com/OpenZeppelin/nile/issues/22). 23 | 24 | ### Set up your project 25 | 26 | Create a directory for your project, then `cd` into it and create a Python virtual environment. 27 | 28 | ```bash 29 | mkdir cairo-workshop 30 | cd cairo-workshop 31 | python3 -m venv env 32 | source env/bin/activate 33 | ``` 34 | 35 | Install the [Nile](https://github.com/OpenZeppelin/nile) development environment and the [OpenZeppelin Contracts](https://github.com/OpenZeppelin/cairo-contracts/). 36 | 37 | ```bash 38 | pip install cairo-lang cairo-nile openzeppelin-cairo-contracts==0.4.0b 39 | ``` 40 | 41 | > Note that we're installing Contracts `v0.4.0b`. This is a beta release that works with the new Cairo 0.10 syntax. 42 | 43 | Run `init` to kickstart a new project. Nile will create the project directory structure and install dependencies such as [the Cairo language](https://www.cairo-lang.org/docs/quickstart.html), a [local network](https://github.com/Shard-Labs/starknet-devnet/), and a [testing framework](https://docs.pytest.org/en/6.2.x/). 44 | 45 | ```bash 46 | nile init 47 | ``` 48 | 49 | Now let's run a local StarkNet testing node so we can work locally, and leave it running for the rest of the steps: 50 | 51 | ```bash 52 | nile node 53 | ``` 54 | 55 | ## 2. Use a preset 56 | 57 | Rename `contracts/contract.cairo` to `contracts/UwuToken.cairo` and replace its contents with: 58 | 59 | ```cairo 60 | %lang starknet 61 | 62 | from openzeppelin.token.erc20.presets.ERC20 import ( 63 | constructor, 64 | name, 65 | symbol, 66 | totalSupply, 67 | decimals, 68 | balanceOf, 69 | allowance, 70 | transfer, 71 | transferFrom, 72 | approve, 73 | increaseAllowance, 74 | decreaseAllowance, 75 | ) 76 | ``` 77 | 78 | 79 | 80 | That's it! That's our ERC20 contract. What this does is to import the [ERC20 basic preset](https://github.com/OpenZeppelin/cairo-contracts/blob/ad399728e6fcd5956a4ed347fb5e8ee731d37ec4/src/openzeppelin/token/erc20/presets/ERC20.cairo) and re-exporting it. 81 | 82 | Let's try to compile it: 83 | 84 | ``` 85 | (env) ➜ nile compile 86 | 87 | 📁 Creating artifacts/abis to store compilation artifacts 88 | 🤖 Compiling all Cairo contracts in the contracts directory 89 | 🔨 Compiling contracts/UwuToken.cairo 90 | ✅ Done 91 | ``` 92 | 93 | Magic ✨ 94 | 95 | ## 3. Deploy it (with a script!) 96 | 97 | Let's now try to deploy our contract. Although we could simply use `nile deploy` like this: 98 | 99 | ```bash 100 | nile deploy UwuToken --alias uwu_token 101 | ``` 102 | 103 | Truth is that there's still some representation issues to overcome: 104 | - strings (`name` and `symbol`) need to be converted to an integer representation first 105 | - uint256 values such as `initial_supply` need to be represented by two `felt`s since they're just 252bits 106 | 107 | To overcome this issues, it's easier to write a deployment script instead of using the CLI directly. Therefore we need to create a `scripts/` directory and create a `deploy.py` file in it: 108 | 109 | > Note: you can find this script already written in this repo 110 | 111 | ```python 112 | # scripts/deploy.py 113 | import time 114 | from nile.utils import * 115 | 116 | ALIAS = "uwu_token" 117 | decimals = 18 118 | 119 | def run(nre): 120 | account_a = nre.get_or_deploy_account("ACCOUNT_A") 121 | 122 | name = str_to_felt("UwuToken") 123 | symbol = str_to_felt("UWU") 124 | initial_supply = to_uint(to_decimals(1337)) 125 | recipient = int(account_a.address, 16) 126 | 127 | arguments = [ 128 | name, 129 | symbol, 130 | decimals, 131 | *initial_supply, 132 | recipient 133 | ] 134 | 135 | token_address, _ = nre.deploy("UwuToken", arguments, alias=ALIAS) 136 | print("UwuToken deployed at", token_address) 137 | 138 | wait = 1 # seconds 139 | print(f"Waiting {wait} seconds for it to get confirmed") 140 | time.sleep(wait) 141 | 142 | supply = from_hex(nre.call("uwu_token", "totalSupply")[0]) 143 | print("total supply:", from_decimals(supply)) 144 | 145 | name = nre.call("uwu_token", "name")[0] 146 | print("token name:", felt_to_str(name)) 147 | 148 | symbol = nre.call("uwu_token", "symbol")[0] 149 | print("token symbol:", felt_to_str(symbol)) 150 | 151 | 152 | def from_decimals(x): 153 | return x / (10 ** decimals) 154 | 155 | def to_decimals(x): 156 | return x * (10 ** decimals) 157 | 158 | def from_hex(x): 159 | return int(x, 16) 160 | ``` 161 | 162 | There's a few things to note in here: 163 | 164 | - The script attempts to find or deploy an account controlled by the private key stored in the `ACCOUNT_A` environmental variable (see below). 165 | 166 | 167 | Create a `.env` file to store your private keys so the script can find them: 168 | 169 | ``` 170 | ACCOUNT_A=207965718267142127099503064836527205057 171 | ACCOUNT_B=671421270995030648365272050552079657182 172 | ``` 173 | 174 | That's it! We're ready to run: 175 | 176 | ```bash 177 | nile run scripts/deploy.py 178 | ``` 179 | 180 | You should see something like this: 181 | 182 | ``` 183 | UwuToken deployed at 0x02cc100da2779fbf2d9f86281c3e7d459167deb372fb21c2e0e8527fdb0812ea 184 | total supply: 1337.0 185 | token name: UwuToken 186 | token symbol: UWU 187 | ``` 188 | 189 | ## 4. Interact with it 190 | 191 | We can go one step further and write a script to transfer funds between accounts: 192 | 193 | ```python 194 | # scripts/transfer.py 195 | from nile.utils import * 196 | 197 | ALIAS = "uwu_token" 198 | decimals = 18 199 | 200 | def run(nre): 201 | account_a = nre.get_or_deploy_account("ACCOUNT_A") 202 | account_b = nre.get_or_deploy_account("ACCOUNT_B") 203 | token_address, _ = nre.get_deployment(ALIAS) 204 | 205 | print_balance(nre, account_a.address, 'a') 206 | print_balance(nre, account_b.address, 'b') 207 | 208 | recipient = from_hex(account_b.address) 209 | amount = to_uint(to_decimals(0.5)) 210 | 211 | print(f"transfer {from_decimals(from_uint(amount))} to {account_b.address}") 212 | account_a.send(token_address, 'transfer', [recipient, *amount], max_fee=0) 213 | 214 | print_balance(nre, account_a.address, 'a') 215 | print_balance(nre, account_b.address, 'b') 216 | 217 | 218 | def get_balance(nre, address): 219 | balance = nre.call(ALIAS, "balanceOf", [from_hex(address)])[0] 220 | return from_hex(balance) 221 | 222 | def print_balance(nre, address, alias): 223 | balance = get_balance(nre, address) 224 | print(f"balance {alias}", from_decimals(balance)) 225 | 226 | def from_decimals(x): 227 | return x / (10 ** decimals) 228 | 229 | def to_decimals(x): 230 | return x * (10 ** decimals) 231 | 232 | def from_hex(x): 233 | return int(x, 16) 234 | 235 | ``` 236 | 237 | And again, we run: 238 | 239 | ```bash 240 | nile run scripts/transfer.py 241 | ``` 242 | 243 | ## 5. Write a custom contract (i.e. extend a library) 244 | 245 | Without inheritance or another language native extensibility system, we need to come up with our own rules to safely extend existing modules to e.g. build our own custom ERC20 based on a standard library one. 246 | 247 | To do this we follow our own [Extensibility pattern](https://docs.openzeppelin.com/contracts-cairo/0.3.1/extensibility) (recommended reading), which extends `library` modules like this pausable `transfer` function: 248 | 249 | ```cairo 250 | %lang starknet 251 | 252 | from starkware.cairo.common.cairo_builtins import HashBuiltin 253 | from starkware.cairo.common.uint256 import Uint256 254 | from openzeppelin.security.pausable.library import Pausable 255 | from openzeppelin.token.erc20.library import ERC20 256 | 257 | (...) 258 | 259 | @external 260 | func transfer{ 261 | syscall_ptr : felt*, 262 | pedersen_ptr : HashBuiltin*, 263 | range_check_ptr 264 | }(recipient: felt, amount: Uint256) -> (success: felt): 265 | Pausable.assert_not_paused() 266 | ERC20.transfer(recipient, amount) 267 | return (TRUE) 268 | end 269 | ``` 270 | 271 | The main problem with this is that we need to manually re-export every function in order to make it available (`transfer`, `transferFrom`, `approve`, etc) even if we don't want to extend or make any changes to it. 272 | 273 | ### Luckily, we have [Wizard](https://wizard.openzeppelin.com/cairo) 274 | 275 | With it, we can just add a `name`, `symbol`, premint amount and any features we want to our contract. In this example, I'll be creating the `UwuToken` and make it Pausable. Then I can copy to clipboard and paste it into `contracts/UwuTokenPausable.cairo` 276 | 277 | ![Wizard for Cairo](wizard.png) 278 | 279 | 280 | ### Deploy to a public network 281 | 282 | To deploy to a public network like goerli or mainnet, contracts need to be declared first: 283 | 284 | ```bash 285 | nile declare UwuToken --network goerli 286 | ``` 287 | 288 | Now we can run our deployment script against the `goerli` testnet: 289 | 290 | ```bash 291 | nile run scripts/deploy.py --network goerli 292 | ``` 293 | 294 | 295 | ## Extra mile 296 | 297 | Develop your own custom contract using the [OpenZeppelin Contracts for Cairo](https://docs.openzeppelin.com/contracts-cairo) library! 298 | --------------------------------------------------------------------------------