diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 6a177190c..9cf7f760a 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -1,7 +1,8 @@ * xref:index.adoc[Overview] -* xref:wizard.adoc[Wizard] -* xref:extensibility.adoc[Extensibility] -* xref:proxies.adoc[Proxies and Upgrades] +//* xref:wizard.adoc[Wizard] +//* xref:extensibility.adoc[Extensibility] +//* xref:proxies.adoc[Proxies and Upgrades] +* xref:interfaces.adoc[Interfaces and Dispatchers] * xref:accounts.adoc[Accounts] ** xref:/guides/deployment.adoc[Counterfactual deployments] @@ -10,17 +11,17 @@ * xref:access.adoc[Access Control] ** xref:/api/access.adoc[API Reference] -* Tokens -** xref:erc20.adoc[ERC20] -** xref:erc721.adoc[ERC721] -** xref:erc1155.adoc[ERC1155] +// * Tokens +// ** xref:erc20.adoc[ERC20] +// ** xref:erc721.adoc[ERC721] +// ** xref:erc1155.adoc[ERC1155] -* xref:security.adoc[Security] +// * xref:security.adoc[Security] * xref:introspection.adoc[Introspection] ** xref:/api/introspection.adoc[API Reference] -* xref:udc.adoc[Universal Deployer Contract] -* xref:utilities.adoc[Utilities] +// * xref:udc.adoc[Universal Deployer Contract] +// * xref:utilities.adoc[Utilities] * xref:contracts::index.adoc[Contracts for Solidity] diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc index 648763e0c..aa01b689a 100644 --- a/docs/modules/ROOT/pages/index.adoc +++ b/docs/modules/ROOT/pages/index.adoc @@ -1,115 +1,149 @@ -= Contracts for Cairo +:starknet: https://starkware.co/product/starknet/[Starknet] +:scarb: https://docs.swmansion.com/scarb[Scarb] +:installation: https://docs.swmansion.com/scarb/download.html[this guide] -*A library for secure smart contract development* written in Cairo for https://starkware.co/product/starknet/[StarkNet], a decentralized ZK Rollup. += Contracts for Cairo -== Usage +*A library for secure smart contract development* written in Cairo for {starknet}, a decentralized ZK Rollup. WARNING: This repo contains highly experimental code. Expect rapid iteration. *Use at your own risk.* -=== First time? +== Installation -Before installing Cairo on your machine, you need to install `gmp`: +The library is available as a {scarb} package. Follow {installation} for installing Cairo and Scarb on your machine +before proceeding, and run the following command to check that the installation was successful: [,bash] ---- -sudo apt install -y libgmp3-dev # linux -brew install gmp # mac ----- +$ scarb --version -TIP: If you have any trouble installing gmp on your Apple M1 computer, https://github.com/OpenZeppelin/nile/issues/22[here's a list of potential solutions]. +scarb 0.7.0 (58cc88efb 2023-08-23) +cairo: 2.2.0 (https://crates.io/crates/cairo-lang-compiler/2.2.0) +sierra: 1.3.0 +---- === Set up your project -Create a directory for your project, then `cd` into it and create a Python virtual environment. +Create an empty directory, and `cd` into it: [,bash] ---- -mkdir my-project -cd my-project -python3 -m venv env -source env/bin/activate +mkdir my_project/ && cd my_project/ ---- -Install the https://github.com/OpenZeppelin/nile[Nile] development environment and then run `init` to kickstart a new project. -Nile will create the project directory structure and install https://www.cairo-lang.org/docs/quickstart.html[the Cairo language], a https://github.com/Shard-Labs/starknet-devnet/[local network], and a https://docs.pytest.org/en/6.2.x/[testing framework]. +Initialize a new Scarb project: [,bash] ---- -pip install cairo-nile -nile init +scarb init ---- -=== Install the library +The contents of `my_project/` should now look like this: [,bash] ---- -pip install openzeppelin-cairo-contracts ----- - -WARNING: Installing directly through GitHub may contain incomplete or breaking implementations. -While we aim not to introduce such changes, we still strongly recommend installing through https://github.com/OpenZeppelin/cairo-contracts/releases/[official releases]. - -=== Use a basic preset +$ ls -Presets are ready-to-use contracts that you can deploy right away. -They also serve as examples of how to use library modules. -xref:extensibility.adoc#presets[Read more about presets]. - -[,cairo] ----- -// contracts/MyToken.cairo - -%lang starknet - -from openzeppelin.token.erc20.presets.ERC20 import ( - constructor, - name, - symbol, - totalSupply, - decimals, - balanceOf, - allowance, - transfer, - transferFrom, - approve, - increaseAllowance, - decreaseAllowance -) +Scarb.toml src ---- -Compile and deploy it right away: +=== Install the library -[,bash] ----- -nile compile +Install the library by declaring it as a dependency in the project's `Scarb.toml` file: -nile deploy MyToken --alias my_token +[,text] +---- +[dependencies] +openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.7.0" } ---- -NOTE: `` is expected to be two integers i.e. -`1` `0`. -See xref:utilities.adoc#uint256[uint256] for more information. +WARNING: Make sure the tag matches the target release. -=== Write a custom contract using library modules +== Basic usage -xref:extensibility.adoc#libraries[Read more about libraries]. +This is how it looks to build an account contract using the xref:accounts.adoc[account module]. +Copy the code into `src/lib.cairo`. -[,cairo] +[,javascript] +---- +#[starknet::contract] +mod MyAccount { + use openzeppelin::account::Account; + use openzeppelin::account::account::PublicKeyTrait; + use openzeppelin::account::interface; + use openzeppelin::introspection::interface::ISRC5; + use starknet::account::Call; + + // Storage members used by this contract are defined in each imported + // module whose `unsafe_state` is used. This design will be improved + // with the addition of components in the future. + #[storage] + struct Storage {} + + #[constructor] + fn constructor(ref self: ContractState, public_key: felt252) { + let mut unsafe_state = _unsafe_state(); + Account::InternalImpl::initializer(ref unsafe_state, public_key); + } + + #[external(v0)] + impl SRC6Impl of interface::ISRC6 { + fn __execute__(self: @ContractState, mut calls: Array) -> Array> { + Account::SRC6Impl::__execute__(@_unsafe_state(), calls) + } + + fn __validate__(self: @ContractState, mut calls: Array) -> felt252 { + Account::SRC6Impl::__validate__(@_unsafe_state(), calls) + } + + fn is_valid_signature( + self: @ContractState, hash: felt252, signature: Array + ) -> felt252 { + Account::SRC6Impl::is_valid_signature(@_unsafe_state(), hash, signature) + } + } + + #[external(v0)] + impl SRC5Impl of ISRC5 { + fn supports_interface(self: @ContractState, interface_id: felt252) -> bool { + Account::SRC5Impl::supports_interface(@_unsafe_state(), interface_id) + } + } + + #[external(v0)] + impl PublicKeyImpl of PublicKeyTrait { + fn get_public_key(self: @ContractState) -> felt252 { + Account::PublicKeyImpl::get_public_key(@_unsafe_state()) + } + + fn set_public_key(ref self: ContractState, new_public_key: felt252) { + let mut unsafe_state = _unsafe_state(); + Account::PublicKeyImpl::set_public_key(ref unsafe_state, new_public_key); + } + } + + #[external(v0)] + fn __validate_deploy__( + self: @ContractState, + class_hash: felt252, + contract_address_salt: felt252, + _public_key: felt252 + ) -> felt252 { + Account::__validate_deploy__( + @_unsafe_state(), class_hash, contract_address_salt, _public_key + ) + } + + #[inline(always)] + fn _unsafe_state() -> Account::ContractState { + Account::unsafe_new_contract_state() + } +} ---- -%lang starknet - -from starkware.cairo.common.cairo_builtins import HashBuiltin -from starkware.cairo.common.uint256 import Uint256 -from openzeppelin.security.pausable.library import Pausable -from openzeppelin.token.erc20.library import ERC20 -(...) +You can now compile it: -@external -func transfer{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( - recipient: felt, amount: Uint256 -) -> (success: felt) { - Pausable.assert_not_paused(); - return ERC20.transfer(recipient, amount); -} +[,bash] ---- +scarb build +---- \ No newline at end of file diff --git a/docs/modules/ROOT/pages/interfaces.adoc b/docs/modules/ROOT/pages/interfaces.adoc new file mode 100644 index 000000000..44125f15c --- /dev/null +++ b/docs/modules/ROOT/pages/interfaces.adoc @@ -0,0 +1,185 @@ +:great-interface-migration: link:https://community.starknet.io/t/the-great-interface-migration/92107[Great Interface Migration] + += Interfaces and Dispatchers + +This section describes the interfaces OpenZeppelin Contracts for Cairo offer, and explains the design choices behind them. + +Interfaces can be found in the module tree under the `interface` submodule, such as `token::erc20::interface`. For example: + +```javascript +use openzeppelin::token::erc20::interface::IERC20; +``` + +or + +```javascript +use openzeppelin::token::erc20::dual20::DualCaseERC20; +``` + +NOTE: For simplicity, we'll use ERC20 as example but the same concepts apply to other modules. + +== Interface traits +The library offers three types of traits to implement or interact with contracts: + +=== Standard traits + +These are associated with a predefined interface such as a standard. +This includes only the functions defined in the interface, and is the standard way to interact with a compliant contract. + +```javascript +#[starknet::interface] +trait IERC20 { + fn name(self: @TState) -> felt252; + fn symbol(self: @TState) -> felt252; + fn decimals(self: @TState) -> u8; + fn total_supply(self: @TState) -> u256; + fn balance_of(self: @TState, account: ContractAddress) -> u256; + fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress) -> u256; + fn transfer(ref self: TState, recipient: ContractAddress, amount: u256) -> bool; + fn transfer_from( + ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; + fn approve(ref self: TState, spender: ContractAddress, amount: u256) -> bool; +} +``` + +=== ABI traits + +They describe a contract's complete interface. This is useful to interface with a preset contract offered by this library, such as the ERC20 preset that includes non-standard functions like `increase_allowance`. + +```javascript +#[starknet::interface] +trait ERC20ABI { + fn name(self: @TState) -> felt252; + fn symbol(self: @TState) -> felt252; + fn decimals(self: @TState) -> u8; + fn total_supply(self: @TState) -> u256; + fn balance_of(self: @TState, account: ContractAddress) -> u256; + fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress) -> u256; + fn transfer(ref self: TState, recipient: ContractAddress, amount: u256) -> bool; + fn transfer_from( + ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; + fn approve(ref self: TState, spender: ContractAddress, amount: u256) -> bool; + fn increase_allowance(ref self: TState, spender: ContractAddress, added_value: u256) -> bool; + fn decrease_allowance( + ref self: TState, spender: ContractAddress, subtracted_value: u256 + ) -> bool; +} +``` + +=== Dispatcher traits +This is a utility trait to interface with contracts whose interface is unknown. Read more in the xref:#dualcase_dispatchers[DualCase Dispatchers] section. + +```javascript +#[derive(Copy, Drop)] +struct DualCaseERC20 { + contract_address: ContractAddress +} + +trait DualCaseERC20Trait { + fn name(self: @DualCaseERC20) -> felt252; + fn symbol(self: @DualCaseERC20) -> felt252; + fn decimals(self: @DualCaseERC20) -> u8; + fn total_supply(self: @DualCaseERC20) -> u256; + fn balance_of(self: @DualCaseERC20, account: ContractAddress) -> u256; + fn allowance(self: @DualCaseERC20, owner: ContractAddress, spender: ContractAddress) -> u256; + fn transfer(self: @DualCaseERC20, recipient: ContractAddress, amount: u256) -> bool; + fn transfer_from( + self: @DualCaseERC20, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; + fn approve(self: @DualCaseERC20, spender: ContractAddress, amount: u256) -> bool; +} +``` + +== Dual interfaces + +Following the {great-interface-migration} plan, we added `snake_case` functions to all of our preexisting `camelCase` contracts with the goal of eventually dropping support for the latter. + +In short, we offer two types of interfaces and utilities to handle them: + +1. `camelCase` interfaces, which are the ones we've been using so far. +2. `snake_case` interfaces, which are the ones we're migrating to. + +This means that currently most of our contracts implement _dual interfaces_. For example, the ERC20 preset contract exposes `transferFrom`, `transfer_from`, `balanceOf`, `balance_of`, etc. + +NOTE: Dual interfaces are available for all external functions present in previous versions of OpenZeppelin Contracts for Cairo (https://github.com/OpenZeppelin/cairo-contracts/releases/tag/v0.6.1[v0.6.1] and below). + +=== `IERC20` + +The default version of the ERC20 interface trait exposes `snake_case` functions: + +```javascript +#[starknet::interface] +trait IERC20 { + fn name(self: @TState) -> felt252; + fn symbol(self: @TState) -> felt252; + fn decimals(self: @TState) -> u8; + fn total_supply(self: @TState) -> u256; + fn balance_of(self: @TState, account: ContractAddress) -> u256; + fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress) -> u256; + fn transfer(ref self: TState, recipient: ContractAddress, amount: u256) -> bool; + fn transfer_from( + ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; + fn approve(ref self: TState, spender: ContractAddress, amount: u256) -> bool; +} +``` + +=== `IERC20Camel` + +On top of that, we also offer a `camelCase` version of the same interface: + +```javascript +#[starknet::interface] +trait IERC20Camel { + fn name(self: @TState) -> felt252; + fn symbol(self: @TState) -> felt252; + fn decimals(self: @TState) -> u8; + fn totalSupply(self: @TState) -> u256; + fn balanceOf(self: @TState, account: ContractAddress) -> u256; + fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress) -> u256; + fn transfer(ref self: TState, recipient: ContractAddress, amount: u256) -> bool; + fn transferFrom( + ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; + fn approve(ref self: TState, spender: ContractAddress, amount: u256) -> bool; +} +``` + +== `DualCase` dispatchers + +WARNING: `DualCase` dispatchers won't work on live chains (`mainnet` or testnets) until they implement panic handling in their runtime. Dispatchers work fine in testing environments. + +In order to ease this transition, OpenZeppelin Contracts for Cairo offer what we call `DualCase` dispatchers such as `DualCaseERC721` or `DualCaseAccount`. + +These modules wrap a target contract with a compatibility layer to expose a `snake_case` interface no matter what casing the underlying contract uses. +This way, an AMM wouldn't have problems integrating tokens independently of their interface. + +For example: + +```javascript +let token = DualCaseERC20 { contract_address: target }; +token.transfer_from(OWNER(), RECIPIENT(), VALUE); +``` + +This is done by simply executing the `snake_case` version of the function (e.g. `transfer_from`) and falling back to the `camelCase` one (e.g. `transferFrom`) in case it reverts with `ENTRYPOINT_NOT_FOUND`, like this: + +```javascript +fn try_selector_with_fallback( + target: ContractAddress, snake_selector: felt252, camel_selector: felt252, args: Span +) -> SyscallResult> { + match call_contract_syscall(target, snake_selector, args) { + Result::Ok(ret) => Result::Ok(ret), + Result::Err(errors) => { + if *errors.at(0) == 'ENTRYPOINT_NOT_FOUND' { + return call_contract_syscall(target, camel_selector, args); + } else { + Result::Err(errors) + } + } + } +} +``` + +Trying the `snake_case` interface first renders `camelCase` calls a bit more expensive since a failed `snake_case` call will always happen before. This is a design choice to incentivize casing adoption/transition as per the {great-interface-migration}. diff --git a/src/token/erc20/erc20.cairo b/src/token/erc20/erc20.cairo index df5d1fd25..9a97cee58 100644 --- a/src/token/erc20/erc20.cairo +++ b/src/token/erc20/erc20.cairo @@ -172,9 +172,9 @@ mod ERC20 { #[generate_trait] impl InternalImpl of InternalTrait { - fn initializer(ref self: ContractState, name_: felt252, symbol_: felt252) { - self._name.write(name_); - self._symbol.write(symbol_); + fn initializer(ref self: ContractState, name: felt252, symbol: felt252) { + self._name.write(name); + self._symbol.write(symbol); } fn _increase_allowance( diff --git a/src/token/erc721/erc721.cairo b/src/token/erc721/erc721.cairo index 559b57c9c..0373dad36 100644 --- a/src/token/erc721/erc721.cairo +++ b/src/token/erc721/erc721.cairo @@ -238,9 +238,9 @@ mod ERC721 { #[generate_trait] impl InternalImpl of InternalTrait { - fn initializer(ref self: ContractState, name_: felt252, symbol_: felt252) { - self._name.write(name_); - self._symbol.write(symbol_); + fn initializer(ref self: ContractState, name: felt252, symbol: felt252) { + self._name.write(name); + self._symbol.write(symbol); let mut unsafe_state = src5::SRC5::unsafe_new_contract_state(); src5::SRC5::InternalImpl::register_interface(ref unsafe_state, interface::IERC721_ID);