Skip to content

Commit

Permalink
Update to cairo v0.9.0 (#364)
Browse files Browse the repository at this point in the history
* update access, account, and security tests

* update token tests

* update proxy, finish proxiable test

* update upgradeables and proxy tests

* update upgradeble erc20 and tests

* update docs

* fix sentence

* remove comment

* remove directives

* change definitions to classes

* update readme, remove error msg

* add assert_revert_entry_point

* fix comment

* add assert_revert_entry_point

* add declaring contracts section

* add specificity to declared classes

* add assert_revert_entry_point to ToC

* fix conflicts

* rebase

* Add RELEASING.md (#363)

* add releasing.md

* clarify branch role

* update token tests

* update proxy, finish proxiable test

* update upgradeables and proxy tests

* add assert_revert_entry_point

* rebase

* fix test name

* fix mocksigner section

* Apply suggestions from code review

Co-authored-by: Martín Triay <martriay@gmail.com>

* Update docs/Proxies.md

Co-authored-by: Martín Triay <martriay@gmail.com>

* change 'implementation instance'

* fix attribution to v0.2.0

* remove trailing commas

* change 'internal' comment

* remove unnecessary admin check

* fix proxy api

Co-authored-by: Martín Triay <martriay@gmail.com>
  • Loading branch information
andrew-fleming and martriay committed Jun 28, 2022
1 parent a6cd96d commit f7d319f
Show file tree
Hide file tree
Showing 28 changed files with 784 additions and 616 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,20 +226,20 @@ docker run cairo-tests
This repo utilizes the [pytest-xdist](https://pytest-xdist.readthedocs.io/en/latest/) plugin which runs tests in parallel. This feature increases testing speed; however, conflicts with a shared state can occur since tests do not run in order. To overcome this, independent cached versions of contracts being tested should be provisioned to each test case. Here's a simple fixture example:

```python
from utils import get_contract_def, cached_contract
from utils import get_contract_class, cached_contract

@pytest.fixture(scope='module')
def foo_factory():
# get contract definition
foo_def = get_contract_def('path/to/foo.cairo')
# get contract class
foo_cls = get_contract_class('path/to/foo.cairo')

# deploy contract
starknet = await Starknet.empty()
foo = await starknet.deploy(contract_def=foo_def)
foo = await starknet.deploy(contract_class=foo_cls)

# copy the state and cache contract
state = starknet.state.copy()
cached_foo = cached_contract(state, foo_def, foo)
cached_foo = cached_contract(state, foo_cls, foo)

return cached_foo
```
Expand Down
113 changes: 65 additions & 48 deletions docs/Proxies.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,31 +16,31 @@
* [Events](#events)
* [Using proxies](#using-proxies)
* [Contract upgrades](#contract-upgrades)
* [Declaring contracts](#declaring-contracts)
* [Handling method calls](#handling-method-calls)
* [Presets](#presets)

## Quickstart

The general workflow is:

1. deploy implementation contract
2. deploy proxy contract with the implementation contract's address set in the proxy's constructor calldata
3. initialize the implementation contract by sending a call to the proxy contract. This will redirect the call to the implementation contract and behave like the implementation contract's constructor
1. declare an implementation [contract class](https://starknet.io/docs/hello_starknet/intro.html#declare-the-contract-on-the-starknet-testnet)
2. deploy proxy contract with the implementation contract's class hash set in the proxy's constructor calldata
3. initialize the implementation contract by sending a call to the proxy contract. This will redirect the call to the implementation contract class and behave like the implementation contract's constructor

In Python, this would look as follows:

```python
# deploy implementation
IMPLEMENTATION = await starknet.deploy(
# declare implementation contract
IMPLEMENTATION = await starknet.declare(
"path/to/implementation.cairo",
constructor_calldata=[]
)

# deploy proxy
PROXY = await starknet.deploy(
"path/to/proxy.cairo",
constructor_calldata=[
IMPLEMENTATION.contract_address, # set implementation address
IMPLEMENTATION.class_hash, # set implementation contract class hash
]
)

Expand Down Expand Up @@ -74,7 +74,7 @@ The StarkNet compiler, meanwhile, already creates pseudo-random storage addresse

A proxy contract is a contract that delegates function calls to another contract. This type of pattern decouples state and logic. Proxy contracts store the state and redirect function calls to an implementation contract that handles the logic. This allows for different patterns such as upgrades, where implementation contracts can change but the proxy contract (and thus the state) does not; as well as deploying multiple proxy instances pointing to the same implementation. This can be useful to deploy many contracts with identical logic but unique initialization data.

In the case of contract upgrades, it is achieved by simply changing the proxy's reference to the implementation contract. This allows developers to add features, update logic, and fix bugs without touching the state or the contract address to interact with the application.
In the case of contract upgrades, it is achieved by simply changing the proxy's reference to the class hash of the declared implementation. This allows developers to add features, update logic, and fix bugs without touching the state or the contract address to interact with the application.

### Proxy contract

Expand All @@ -84,7 +84,7 @@ The [Proxy contract](../src/openzeppelin/upgrades/Proxy.cairo) includes two core

2. The `__l1_default__` method is also a fallback method; however, it redirects the function call and associated calldata to a layer one contract. In order to invoke `__l1_default__`, the original function call must include the library function `send_message_to_l1`. See Cairo's [Interacting with L1 contracts](https://www.cairo-lang.org/docs/hello_starknet/l1l2.html) for more information.

Since this proxy is designed to work both as an [UUPS-flavored upgrade proxy](https://eips.ethereum.org/EIPS/eip-1822) as well as a non-upgradeable proxy, it does not know how to handle its own state. Therefore it requires the implementation contract to be deployed beforehand, so its address can be passed to the Proxy on construction time.
Since this proxy is designed to work both as an [UUPS-flavored upgrade proxy](https://eips.ethereum.org/EIPS/eip-1822) as well as a non-upgradeable proxy, it does not know how to handle its own state. Therefore it requires the implementation contract class to be declared beforehand, so its class hash can be passed to the Proxy on construction time.

When interacting with the contract, function calls should be sent by the user to the proxy. The proxy's fallback function redirects the function call to the implementation contract to execute.

Expand All @@ -104,7 +104,8 @@ If the implementation is upgradeable, it should:

The implementation contract should NOT:

* deploy with a traditional constructor (decorated with `@constructor`). Instead, use an initializer method that invokes the Proxy `constructor`.
* be deployed like a regular contract. Instead, the implementation contract should be declared (which creates a `DeclaredClass` containing its hash and abi)
* set its initial state with a traditional constructor (decorated with `@constructor`). Instead, use an initializer method that invokes the Proxy `constructor`.

> Note that the Proxy `constructor` includes a check the ensures the initializer can only be called once; however, `_set_implementation` does not include this check. It's up to the developers to protect their implementation contract's upgradeability with access controls such as [`assert_only_admin`](#assert_only_admin).
Expand All @@ -117,26 +118,26 @@ For a full implementation contract example, please see:
### Methods

```cairo
func constructor(proxy_admin: felt):
func initializer(proxy_admin: felt):
end
func _set_implementation(new_implementation: felt):
func assert_only_admin():
end
func _set_admin(new_admin: felt):
func get_implementation_hash() -> (implementation: felt):
end
func get_implementation() -> (implementation: felt):
func get_admin() -> (admin: felt):
end
func get_admin() -> (admin: felt):
func _set_admin(new_admin: felt):
end
func assert_only_admin():
func _set_implementation_hash(new_implementation: felt):
end
```

#### `constructor`
#### `initializer`

Initializes the proxy contract with an initial implementation.

Expand All @@ -150,37 +151,21 @@ Returns:

None.

#### `_set_implementation`
#### `assert_only_admin`

Sets the implementation contract. This method is included in the proxy contract's constructor and is furthermore used to upgrade contracts.
Reverts if called by any account other than the admin.

Parameters:

```cairo
new_implementation: felt
```

Returns:

None.

#### `_set_admin`

Sets the admin of the proxy contract.

Parameters:

```cairo
new_admin: felt
```

Returns:

None.

#### `get_implementation`

Returns the current implementation address.
Returns the current implementation hash.

Parameters:

Expand All @@ -206,14 +191,30 @@ Returns:
admin: felt
```

#### `assert_only_admin`
#### `_set_admin`

Throws if called by any account other than the admin.
Sets `new_admin` as the admin of the proxy contract.

Parameters:

```cairo
new_admin: felt
```

Returns:

None.

#### `_set_implementation_hash`

Sets `new_implementation` as the implementation's contract class. This method is included in the proxy contract's constructor and can be used to upgrade contracts.

Parameters:

```cairo
new_implementation: felt
```

Returns:

None.
Expand All @@ -223,49 +224,61 @@ None.
```cairo
func Upgraded(implementation: felt):
end
func AdminChanged(previousAdmin: felt, newAdmin: felt):
end
```

#### `Upgraded`

Emitted when a proxy contract sets a new implementation address.
Emitted when a proxy contract sets a new implementation class hash.

Parameters:

```cairo
implementation: felt
```

#### `AdminChanged`

Emitted when the `admin` changes from `previousAdmin` to `newAdmin`.

Parameters:

```cairo
previousAdmin: felt
newAdmin: felt
```

## Using proxies

### Contract upgrades

To upgrade a contract, the implementation contract should include an `upgrade` method that, when called, changes the reference to a new deployed contract like this:

```python
# deploy first implementation
IMPLEMENTATION = await starknet.deploy(
# declare first implementation
IMPLEMENTATION = await starknet.declare(
"path/to/implementation.cairo",
constructor_calldata=[]
)

# deploy proxy
PROXY = await starknet.deploy(
"path/to/proxy.cairo",
constructor_calldata=[
IMPLEMENTATION.contract_address, # set implementation address
IMPLEMENTATION.class_hash, # set implementation hash
]
)

# deploy implementation v2
IMPLEMENTATION_V2 = await starknet.deploy(
# declare implementation v2
IMPLEMENTATION_V2 = await starknet.declare(
"path/to/implementation_v2.cairo",
constructor_calldata=[]
)

# call upgrade with the new implementation contract address
# call upgrade with the new implementation contract class hash
await signer.send_transaction(
account, PROXY.contract_address, 'upgrade', [
IMPLEMENTATION_V2.contract_address
IMPLEMENTATION_V2.class_hash
]
)
```
Expand All @@ -275,6 +288,10 @@ For a full deployment and upgrade implementation, please see:
* [Upgrades V1](../tests/mocks/upgrades_v1_mock.cairo)
* [Upgrades V2](../tests/mocks/upgrades_v2_mock.cairo)

### Declaring contracts

StarkNet contracts come in two forms: contract classes and contract instances. Contract classes represent the uninstantiated, stateless code; whereas, contract instances are instantiated and include the state. Since the Proxy contract references the implementation contract by its class hash, declaring an implementation contract proves sufficient (as opposed to a full deployment). For more information on declaring classes, see [StarkNet's documentation](https://starknet.io/docs/hello_starknet/intro.html#declare-contract).

### Handling method calls

As with most StarkNet contracts, interacting with a proxy contract requires an [account abstraction](../docs/Account.md#quickstart). One notable difference with proxy contracts versus other contract implementations is that calling `@view` methods also requires an account abstraction. As of now, direct calls to default entrypoints are only supported by StarkNet's `syscalls` from other contracts i.e. account contracts. The differences in getter methods written in Python, for example, are as follows:
Expand Down
50 changes: 32 additions & 18 deletions docs/Utilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ The following documentation provides context, reasoning, and examples for method
* [`sub_uint`](#sub_uint)
* [Assertions](#assertions)
* [`assert_revert`](#assert_revert)
* [`assert_revert_entry_point`](#assert_revert_entry_point)
* [`assert_events_emitted`](#assert_event_emitted)
* [Memoization](#memoization)
* [`get_contract_def`](#get_contract_def)
* [`get_contract_class`](#get_contract_class)
* [`cached_contract`](#cached_contract)
* [Signer](#signer)
* [MockSigner](#mocksigner)

## Constants

Expand Down Expand Up @@ -156,6 +157,19 @@ await assert_revert(signer.send_transaction(
)
```

### `assert_revert_entry_point`

An extension of `assert_revert` that asserts an entry point error occurs with the given `invalid_selector` parameter. This assertion is especially useful in checking proxy/implementation contracts. To use `assert_revert_entry_point`:

```python
await assert_revert_entry_point(
signer.send_transaction(
account, contract.contract_address, 'nonexistent_selector', []
),
invalid_selector='nonexistent_selector'
)
```

### `assert_event_emitted`

A helper method that checks a transaction receipt for the contract emitting the event (`from_address`), the emitted event itself (`name`), and the arguments emitted (`data`). To use `assert_event_emitted`:
Expand Down Expand Up @@ -185,44 +199,44 @@ assert_event_emitted(

Memoizing functions allow for quicker and computationally cheaper calculations which is immensely beneficial while testing smart contracts.

### `get_contract_def`
### `get_contract_class`

A helper method that returns the contract definition from the given path. To capture the contract definition, simply add the contracat path as an argument like this:
A helper method that returns the contract class from the given path. To capture the contract class, simply add the contract path as an argument like this:

```python
contract_definition = get_contract_def('path/to/contract.cairo')
contract_class = get_contract_class('path/to/contract.cairo')
```

### `cached_contract`

A helper method that returns the cached state of a given contract. It's recommended to first deploy all the relevant contracts before caching the state. The requisite contracts in the testing module should each be instantiated with `cached_contract` in a fixture after the state has been copied. The memoization pattern with `cached_contract` should look something like this:

```python
# get contract definitions
# get contract classes
@pytest.fixture(scope='module')
def contract_defs():
foo_def = get_contract_def('path/to/foo.cairo')
return foo_def
def contract_classes():
foo_cls = get_contract_class('path/to/foo.cairo')
return foo_cls

# deploy contracts
@pytest.fixture(scope='module')
async def foo_init(contract_defs):
foo_def = contract_defs
async def foo_init(contract_classes):
foo_cls = contract_classes
starknet = await Starknet.empty()
foo = await starknet.deploy(
contract_def=foo_def,
contract_class=foo_cls,
constructor_calldata=[]
)
return starknet.state, foo # return state and all deployed contracts

# memoization
@pytest.fixture(scope='module')
def foo_factory(contract_defs, foo_init):
foo_def = contract_defs # contract definitions
state, foo = foo_init # state and deployed contracts
_state = state.copy() # copy the state
cached_foo = cached_contract(_state, foo_def, foo) # cache contracts
return cached_foo # return cached contracts
def foo_factory(contract_classes, foo_init):
foo_cls = contract_classes # contract classes
state, foo = foo_init # state and deployed contracts
_state = state.copy() # copy the state
cached_foo = cached_contract(_state, foo_cls, foo) # cache contracts
return cached_foo # return cached contracts
```

## MockSigner
Expand Down
2 changes: 1 addition & 1 deletion src/openzeppelin/token/erc20/ERC20_Upgradeable.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func upgrade{
range_check_ptr
}(new_implementation: felt):
Proxy.assert_only_admin()
Proxy._set_implementation(new_implementation)
Proxy._set_implementation_hash(new_implementation)
return ()
end

Expand Down
Loading

0 comments on commit f7d319f

Please sign in to comment.