From 993cb03e58197bd499509f844e08080078b1d9c9 Mon Sep 17 00:00:00 2001 From: Jonathan LEI Date: Thu, 7 Mar 2024 12:15:07 +0000 Subject: [PATCH] feat: add L2 token contract --- .github/workflows/artifacts-dev.yaml | 7 +- .github/workflows/artifacts-release.yaml | 7 +- .gitignore | 1 + README.md | 74 +- cairo_project.toml | 3 + l2/openzeppelin/lib.cairo | 1 + l2/openzeppelin/token.cairo | 2 + l2/openzeppelin/token/erc20.cairo | 1 + l2/openzeppelin/token/erc20/interface.cairo | 79 +++ l2/openzeppelin/token/erc20_v070.cairo | 1 + l2/openzeppelin/token/erc20_v070/erc20.cairo | 700 +++++++++++++++++++ l2/src/access_control_interface.cairo | 42 ++ l2/src/err_msg.cairo | 35 + l2/src/lib.cairo | 8 + l2/src/mintable_token_interface.cairo | 13 + l2/src/replaceability_interface.cairo | 72 ++ l2/src/roles_interface.cairo | 172 +++++ scripts/compile_l2.sh | 29 + scripts/compile_l2_with_docker.sh | 15 + 19 files changed, 1259 insertions(+), 3 deletions(-) create mode 100644 cairo_project.toml create mode 100644 l2/openzeppelin/lib.cairo create mode 100644 l2/openzeppelin/token.cairo create mode 100644 l2/openzeppelin/token/erc20.cairo create mode 100644 l2/openzeppelin/token/erc20/interface.cairo create mode 100644 l2/openzeppelin/token/erc20_v070.cairo create mode 100644 l2/openzeppelin/token/erc20_v070/erc20.cairo create mode 100644 l2/src/access_control_interface.cairo create mode 100644 l2/src/err_msg.cairo create mode 100644 l2/src/lib.cairo create mode 100644 l2/src/mintable_token_interface.cairo create mode 100644 l2/src/replaceability_interface.cairo create mode 100644 l2/src/roles_interface.cairo create mode 100755 scripts/compile_l2.sh create mode 100755 scripts/compile_l2_with_docker.sh diff --git a/.github/workflows/artifacts-dev.yaml b/.github/workflows/artifacts-dev.yaml index 28707a3..2b312f3 100644 --- a/.github/workflows/artifacts-dev.yaml +++ b/.github/workflows/artifacts-dev.yaml @@ -19,10 +19,14 @@ jobs: with: version: "nightly" - - name: "Build contracts" + - name: "Build L1 contracts" run: | forge build + - name: "Build L2 contracts" + run: | + ./scripts/compile_l2_with_docker.sh + - name: "Set up deploy key for artifacts repo" uses: "webfactory/ssh-agent@v0.7.0" with: @@ -38,6 +42,7 @@ jobs: cp ./out/TokenEscrow.sol/TokenEscrow.json ./artifacts/zend-token/$COMMIT_HASH/ cp ./out/TransparentUpgradeableProxy.sol/TransparentUpgradeableProxy.json ./artifacts/zend-token/$COMMIT_HASH/ cp ./out/ProxyAdmin.sol/ProxyAdmin.json ./artifacts/zend-token/$COMMIT_HASH/ + cp ./build/ERC20.json ./artifacts/zend-token/$COMMIT_HASH/StarknetERC20.json (cd ./artifacts/zend-token/ && rm -rf ./latest && ln -s ./$COMMIT_HASH ./latest) cd ./artifacts diff --git a/.github/workflows/artifacts-release.yaml b/.github/workflows/artifacts-release.yaml index 220d958..d2ec0f6 100644 --- a/.github/workflows/artifacts-release.yaml +++ b/.github/workflows/artifacts-release.yaml @@ -19,10 +19,14 @@ jobs: with: version: "nightly" - - name: "Build contracts" + - name: "Build L1 contracts" run: | forge build + - name: "Build L2 contracts" + run: | + ./scripts/compile_l2_with_docker.sh + - name: "Set up deploy key for artifacts repo" uses: "webfactory/ssh-agent@v0.7.0" with: @@ -38,6 +42,7 @@ jobs: cp ./out/TokenEscrow.sol/TokenEscrow.json ./artifacts/zend-token/$COMMIT_HASH/ cp ./out/TransparentUpgradeableProxy.sol/TransparentUpgradeableProxy.json ./artifacts/zend-token/$COMMIT_HASH/ cp ./out/ProxyAdmin.sol/ProxyAdmin.json ./artifacts/zend-token/$COMMIT_HASH/ + cp ./build/ERC20.json ./artifacts/zend-token/$COMMIT_HASH/StarknetERC20.json (cd ./artifacts/zend-token/ && rm -rf ./latest && ln -s ./$COMMIT_HASH ./latest) cd ./artifacts diff --git a/.gitignore b/.gitignore index f49938e..cd4b338 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ +/build/ /cache/ /out/ diff --git a/README.md b/README.md index 4fb53d9..12eb966 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,78 @@ **ZEND token implementation** +## L1 token + +The [L1 token contract](./l1/ZendToken.sol) is a simple child contract of the OpenZeppelin reference ERC20 implementation. + +### Prerequisites + +Install [Foundry](https://github.com/foundry-rs/foundry) for Solidity compilation. Check your Foundry installation with: + +```console +forge --version +``` + +### Building + +To build the contract simply run: + +```console +forge build +``` + +Built artifacts will be available in the `./out` folder. + +## L2 token + +The L2 token contract is _not_ manually deployed. Instead, it's automatically deployed by the [L2 token bridge contract](https://github.com/starknet-io/starkgate-contracts/blob/d62a255307d2f3de65665f18316766a2c69ead78/src/cairo/token_bridge.cairo) when a [permissionless token enrollment](https://docs.starknet.io/documentation/tools/starkgate-adding_a_token/) is triggered on the [L1 token bridge manager contract](https://github.com/starknet-io/starkgate-contracts/blob/d62a255307d2f3de65665f18316766a2c69ead78/src/solidity/StarkgateManager.sol). Therefore, the L2 token contract code to be deployed is completely up to the official StarkGate configuration. Any L2 token contract instance deployed this way is mintable only from the bridge contract. + +> [!NOTE] +> +> As of this writing, the L2 token _class hash_ deployed by StarkGate is [`0x05ffbcfeb50d200a0677c48a129a11245a3fc519d1d98d76882d1c9a1b19c6ed`](https://starkscan.co/class/0x05ffbcfeb50d200a0677c48a129a11245a3fc519d1d98d76882d1c9a1b19c6ed). + +Despite our lack of control over the implementation, the token contract is still [reproduced here](./l2/openzeppelin/token/erc20_v070/erc20.cairo) (along with dependencies) from its [upstream source](https://github.com/starknet-io/starkgate-contracts/blob/d62a255307d2f3de65665f18316766a2c69ead78/src/openzeppelin/token/erc20_v070/erc20.cairo) for reference. + +Additionally, due to the fact that none of the major Starknet block explorers offer Cairo 1 contract verification as of this writing, this repo provides tools for [deterministic compilation](#deterministic-compilation-with-docker) as a means of [verification](#verifying-class-hash). + +### Building directly + +With the `starknet-compile` command from [starkware-libs/cairo](https://github.com/starkware-libs/cairo) installed, run: + +```console +mkdir -p ./build +starknet-compile . -c openzeppelin::token::erc20_v070::erc20::ERC20 ./build/ERC20.json +``` + +> [!TIP] +> +> You must install `v2.3.0` or newer for `starknet-compile` to be able to compile successfully. + +The compiled contract is available at `./build/ERC20.json`. + +### Deterministic compilation with Docker + +To ensure deterministic compilation output, a [script](./scripts/compile_l2_with_docker.sh) is provided that generates the exact same class as the one used in production: + +```console +./scripts/compile_l2_with_docker.sh +``` + +The compiled contract is available at `./build/ERC20.json`. + +### Verifying class hash + +Either [built directly](#building-directly) or [with Docker](#deterministic-compilation-with-docker), you may verify that the class hash of the compiled contract artifact with the `starkli class-hash` command from [Starkli](https://github.com/xJonathanLEI/starkli). + +## Deployed addresses + +This section lists deployed contract addresses. + +### Mainnet + +- Ethereum: [0xb2606492712D311be8f41d940AFE8CE742A52D44](https://etherscan.io/address/0xb2606492712D311be8f41d940AFE8CE742A52D44) +- Starknet: [0x00585c32b625999e6e5e78645ff8df7a9001cf5cf3eb6b80ccdd16cb64bd3a34](https://starkscan.co/contract/0x00585c32b625999e6e5e78645ff8df7a9001cf5cf3eb6b80ccdd16cb64bd3a34) + ## License Licensed under either of @@ -11,4 +83,4 @@ Licensed under either of - Apache License, Version 2.0 ([LICENSE-APACHE](./LICENSE-APACHE) or ) - MIT license ([LICENSE-MIT](./LICENSE-MIT) or ) -at your option. +at your option, except the content in [`./l2/`](./l2/), which is licensed with its upstream source. diff --git a/cairo_project.toml b/cairo_project.toml new file mode 100644 index 0000000..b5505cf --- /dev/null +++ b/cairo_project.toml @@ -0,0 +1,3 @@ +[crate_roots] +src = "l2/src" +openzeppelin = "l2/openzeppelin" diff --git a/l2/openzeppelin/lib.cairo b/l2/openzeppelin/lib.cairo new file mode 100644 index 0000000..40d3ff5 --- /dev/null +++ b/l2/openzeppelin/lib.cairo @@ -0,0 +1 @@ +mod token; diff --git a/l2/openzeppelin/token.cairo b/l2/openzeppelin/token.cairo new file mode 100644 index 0000000..975afb6 --- /dev/null +++ b/l2/openzeppelin/token.cairo @@ -0,0 +1,2 @@ +mod erc20; +mod erc20_v070; diff --git a/l2/openzeppelin/token/erc20.cairo b/l2/openzeppelin/token/erc20.cairo new file mode 100644 index 0000000..4dd3283 --- /dev/null +++ b/l2/openzeppelin/token/erc20.cairo @@ -0,0 +1 @@ +mod interface; diff --git a/l2/openzeppelin/token/erc20/interface.cairo b/l2/openzeppelin/token/erc20/interface.cairo new file mode 100644 index 0000000..f8cabe8 --- /dev/null +++ b/l2/openzeppelin/token/erc20/interface.cairo @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.8.0-beta.1 (token/erc20/interface.cairo) + +use starknet::ContractAddress; + +#[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; +} + +#[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; +} + +#[starknet::interface] +trait IERC20CamelOnly { + fn totalSupply(self: @TState) -> u256; + fn balanceOf(self: @TState, account: ContractAddress) -> u256; + fn transferFrom( + ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; +} + +#[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; +} + +#[starknet::interface] +trait ERC20CamelABI { + 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; + fn increaseAllowance(ref self: TState, spender: ContractAddress, addedValue: u256) -> bool; + fn decreaseAllowance(ref self: TState, spender: ContractAddress, subtractedValue: u256) -> bool; +} diff --git a/l2/openzeppelin/token/erc20_v070.cairo b/l2/openzeppelin/token/erc20_v070.cairo new file mode 100644 index 0000000..bfe4665 --- /dev/null +++ b/l2/openzeppelin/token/erc20_v070.cairo @@ -0,0 +1 @@ +mod erc20; diff --git a/l2/openzeppelin/token/erc20_v070/erc20.cairo b/l2/openzeppelin/token/erc20_v070/erc20.cairo new file mode 100644 index 0000000..314dd5f --- /dev/null +++ b/l2/openzeppelin/token/erc20_v070/erc20.cairo @@ -0,0 +1,700 @@ +//! SPDX-License-Identifier: MIT +//! OpenZeppelin Contracts for Cairo v0.7.0 (token/erc20/erc20.cairo) +//! +//! # ERC20 Contract and Implementation +//! +//! This ERC20 contract includes both a library and a basic preset implementation. +//! The library is agnostic regarding how tokens are created; however, +//! the preset implementation sets the initial supply in the constructor. +//! A derived contract can use [_mint](_mint) to create a different supply mechanism. +#[starknet::contract] +mod ERC20 { + use src::err_msg::AccessErrors as AccessErrors; + use src::err_msg::ERC20Errors as ERC20Errors; + use src::err_msg::ReplaceErrors as ReplaceErrors; + + use integer::BoundedInt; + use openzeppelin::token::erc20::interface::IERC20; + use openzeppelin::token::erc20::interface::IERC20CamelOnly; + use src::mintable_token_interface::{IMintableToken, IMintableTokenCamel}; + use src::access_control_interface::{ + IAccessControl, RoleId, RoleAdminChanged, RoleGranted, RoleRevoked + }; + use src::roles_interface::IMinimalRoles; + use src::roles_interface::{ + GOVERNANCE_ADMIN, UPGRADE_GOVERNOR, GovernanceAdminAdded, GovernanceAdminRemoved, + UpgradeGovernorAdded, UpgradeGovernorRemoved + }; + + use src::replaceability_interface::{ + ImplementationData, IReplaceable, IReplaceableDispatcher, IReplaceableDispatcherTrait, + EIC_INITIALIZE_SELECTOR, IMPLEMENTATION_EXPIRATION, ImplementationAdded, + ImplementationRemoved, ImplementationReplaced, ImplementationFinalized + }; + use starknet::ContractAddress; + use starknet::class_hash::{ClassHash, Felt252TryIntoClassHash}; + use starknet::{get_caller_address, get_block_timestamp}; + use starknet::syscalls::library_call_syscall; + + #[storage] + struct Storage { + ERC20_name: felt252, + ERC20_symbol: felt252, + ERC20_decimals: u8, + ERC20_total_supply: u256, + ERC20_balances: LegacyMap, + ERC20_allowances: LegacyMap<(ContractAddress, ContractAddress), u256>, + // --- MintableToken --- + permitted_minter: ContractAddress, + // --- Replaceability --- + // Delay in seconds before performing an upgrade. + upgrade_delay: u64, + // Timestamp by which implementation can be activated. + impl_activation_time: LegacyMap, + // Timestamp until which implementation can be activated. + impl_expiration_time: LegacyMap, + // Is the implementation finalized. + finalized: bool, + // --- Access Control --- + // For each role id store its role admin id. + role_admin: LegacyMap, + // For each role and address, stores true if the address has this role; otherwise, false. + role_members: LegacyMap<(RoleId, ContractAddress), bool>, + } + + #[event] + #[derive(Copy, Drop, PartialEq, starknet::Event)] + enum Event { + Transfer: Transfer, + Approval: Approval, + // --- Replaceability --- + ImplementationAdded: ImplementationAdded, + ImplementationRemoved: ImplementationRemoved, + ImplementationReplaced: ImplementationReplaced, + ImplementationFinalized: ImplementationFinalized, + // --- Access Control --- + RoleGranted: RoleGranted, + RoleRevoked: RoleRevoked, + RoleAdminChanged: RoleAdminChanged, + // --- Roles --- + GovernanceAdminAdded: GovernanceAdminAdded, + GovernanceAdminRemoved: GovernanceAdminRemoved, + UpgradeGovernorAdded: UpgradeGovernorAdded, + UpgradeGovernorRemoved: UpgradeGovernorRemoved, + } + + /// Emitted when tokens are moved from address `from` to address `to`. + #[derive(Copy, Drop, PartialEq, starknet::Event)] + struct Transfer { + // #[key] - Not indexed, to maintain backward compatibility. + from: ContractAddress, + // #[key] - Not indexed, to maintain backward compatibility. + to: ContractAddress, + value: u256 + } + + /// Emitted when the allowance of a `spender` for an `owner` is set by a call + /// to [approve](approve). `value` is the new allowance. + #[derive(Copy, Drop, PartialEq, starknet::Event)] + struct Approval { + // #[key] - Not indexed, to maintain backward compatibility. + owner: ContractAddress, + // #[key] - Not indexed, to maintain backward compatibility. + spender: ContractAddress, + value: u256 + } + + /// Initializes the state of the ERC20 contract. This includes setting the + /// initial supply of tokens as well as the recipient of the initial supply. + #[constructor] + fn constructor( + ref self: ContractState, + name: felt252, + symbol: felt252, + decimals: u8, + initial_supply: u256, + recipient: ContractAddress, + permitted_minter: ContractAddress, + provisional_governance_admin: ContractAddress, + upgrade_delay: u64, + ) { + self.initializer(name, symbol, decimals); + self._mint(recipient, initial_supply); + assert(permitted_minter.is_non_zero(), AccessErrors::INVALID_MINTER); + self.permitted_minter.write(permitted_minter); + self._initialize_roles(:provisional_governance_admin); + self.upgrade_delay.write(upgrade_delay); + } + + + #[generate_trait] + impl RolesInternal of _RolesInternal { + // --- Roles --- + fn _grant_role_and_emit( + ref self: ContractState, role: RoleId, account: ContractAddress, event: Event + ) { + if !self.has_role(:role, :account) { + assert(account.is_non_zero(), AccessErrors::ZERO_ADDRESS); + self.grant_role(:role, :account); + self.emit(event); + } + } + + fn _revoke_role_and_emit( + ref self: ContractState, role: RoleId, account: ContractAddress, event: Event + ) { + if self.has_role(:role, :account) { + self.revoke_role(:role, :account); + self.emit(event); + } + } + + // + // WARNING + // The following internal method is unprotected and should not be used outside of a + // contract's constructor. + // + fn _initialize_roles( + ref self: ContractState, provisional_governance_admin: ContractAddress + ) { + let un_initialized = self.get_role_admin(role: GOVERNANCE_ADMIN) == 0; + assert(un_initialized, AccessErrors::ALREADY_INITIALIZED); + assert( + provisional_governance_admin.is_non_zero(), AccessErrors::ZERO_ADDRESS_GOV_ADMIN + ); + self._grant_role(role: GOVERNANCE_ADMIN, account: provisional_governance_admin); + self._set_role_admin(role: GOVERNANCE_ADMIN, admin_role: GOVERNANCE_ADMIN); + self._set_role_admin(role: UPGRADE_GOVERNOR, admin_role: GOVERNANCE_ADMIN); + } + + fn only_upgrade_governor(self: @ContractState) { + assert( + self.is_upgrade_governor(get_caller_address()), AccessErrors::ONLY_UPGRADE_GOVERNOR + ); + } + } + + // + // External + // + + #[external(v0)] + impl MintableToken of IMintableToken { + fn permissioned_mint(ref self: ContractState, account: ContractAddress, amount: u256) { + assert(get_caller_address() == self.permitted_minter.read(), AccessErrors::ONLY_MINTER); + self._mint(account, :amount); + } + fn permissioned_burn(ref self: ContractState, account: ContractAddress, amount: u256) { + assert(get_caller_address() == self.permitted_minter.read(), AccessErrors::ONLY_MINTER); + self._burn(account, :amount); + } + } + + #[external(v0)] + impl MintableTokenCamelImpl of IMintableTokenCamel { + fn permissionedMint(ref self: ContractState, account: ContractAddress, amount: u256) { + MintableToken::permissioned_mint(ref self, account, amount); + } + fn permissionedBurn(ref self: ContractState, account: ContractAddress, amount: u256) { + MintableToken::permissioned_burn(ref self, account, amount); + } + } + + fn calc_impl_key(implementation_data: ImplementationData) -> felt252 { + // Hash the implementation_data to obtain a key. + let mut hash_input = ArrayTrait::new(); + implementation_data.serialize(ref hash_input); + poseidon::poseidon_hash_span(hash_input.span()) + } + + #[generate_trait] + impl ReplaceableInternal of _ReplaceableInternal { + // Returns if finalized. + fn is_finalized(self: @ContractState) -> bool { + self.finalized.read() + } + + // Sets the implementation as finalized. + fn finalize(ref self: ContractState) { + self.finalized.write(true); + } + + + // Sets the implementation activation time. + fn set_impl_activation_time( + ref self: ContractState, implementation_data: ImplementationData, activation_time: u64 + ) { + let impl_key = calc_impl_key(:implementation_data); + self.impl_activation_time.write(impl_key, activation_time); + } + + // Returns the implementation activation time. + fn get_impl_expiration_time( + self: @ContractState, implementation_data: ImplementationData + ) -> u64 { + let impl_key = calc_impl_key(:implementation_data); + self.impl_expiration_time.read(impl_key) + } + + // Sets the implementation expiration time. + fn set_impl_expiration_time( + ref self: ContractState, implementation_data: ImplementationData, expiration_time: u64 + ) { + let impl_key = calc_impl_key(:implementation_data); + self.impl_expiration_time.write(impl_key, expiration_time); + } + } + + #[external(v0)] + impl Replaceable of IReplaceable { + fn get_upgrade_delay(self: @ContractState) -> u64 { + self.upgrade_delay.read() + } + + // Gets the implementation activation time. + fn get_impl_activation_time( + self: @ContractState, implementation_data: ImplementationData + ) -> u64 { + let impl_key = calc_impl_key(:implementation_data); + self.impl_activation_time.read(impl_key) + } + + fn add_new_implementation( + ref self: ContractState, implementation_data: ImplementationData + ) { + self.only_upgrade_governor(); + + let activation_time = get_block_timestamp() + self.get_upgrade_delay(); + let expiration_time = activation_time + IMPLEMENTATION_EXPIRATION; + // TODO - add an assertion that the `implementation_data.impl_hash` is declared. + self.set_impl_activation_time(:implementation_data, :activation_time); + self.set_impl_expiration_time(:implementation_data, :expiration_time); + self.emit(ImplementationAdded { implementation_data: implementation_data }); + } + + fn remove_implementation(ref self: ContractState, implementation_data: ImplementationData) { + self.only_upgrade_governor(); + let impl_activation_time = self.get_impl_activation_time(:implementation_data); + + if (impl_activation_time.is_non_zero()) { + self.set_impl_activation_time(:implementation_data, activation_time: 0); + self.set_impl_expiration_time(:implementation_data, expiration_time: 0); + self.emit(ImplementationRemoved { implementation_data: implementation_data }); + } + } + // Replaces the non-finalized current implementation to one that was previously added and + // whose activation time had passed. + fn replace_to(ref self: ContractState, implementation_data: ImplementationData) { + // The call is restricted to the upgrade governor. + self.only_upgrade_governor(); + + // Validate implementation is not finalized. + assert(!self.is_finalized(), ReplaceErrors::FINALIZED); + + let now = get_block_timestamp(); + let impl_activation_time = self.get_impl_activation_time(:implementation_data); + let impl_expiration_time = self.get_impl_expiration_time(:implementation_data); + + // Zero activation time means that this implementation & init vector combination + // was not previously added. + assert(impl_activation_time.is_non_zero(), ReplaceErrors::UNKNOWN_IMPLEMENTATION); + + assert(impl_activation_time <= now, ReplaceErrors::NOT_ENABLED_YET); + assert(now <= impl_expiration_time, ReplaceErrors::IMPLEMENTATION_EXPIRED); + + // We emit now so that finalize emits last (if it does). + self.emit(ImplementationReplaced { implementation_data }); + + // Finalize imeplementation, if needed. + if (implementation_data.final) { + self.finalize(); + self.emit(ImplementationFinalized { impl_hash: implementation_data.impl_hash }); + } + + // Handle EIC. + match implementation_data.eic_data { + Option::Some(eic_data) => { + // Wrap the calldata as a span, as preperation for the library_call_syscall + // invocation. + let mut calldata_wrapper = ArrayTrait::new(); + eic_data.eic_init_data.serialize(ref calldata_wrapper); + + // Invoke the EIC's initialize function as a library call. + let res = library_call_syscall( + class_hash: eic_data.eic_hash, + function_selector: EIC_INITIALIZE_SELECTOR, + calldata: calldata_wrapper.span() + ); + assert(res.is_ok(), ReplaceErrors::EIC_LIB_CALL_FAILED); + }, + Option::None(()) => {} + }; + + // Replace the class hash. + let result = starknet::replace_class_syscall(implementation_data.impl_hash); + assert(result.is_ok(), ReplaceErrors::REPLACE_CLASS_HASH_FAILED); + + // Remove implementation, as it was consumed. + self.set_impl_activation_time(:implementation_data, activation_time: 0); + self.set_impl_expiration_time(:implementation_data, expiration_time: 0); + } + } + + #[external(v0)] + impl AccessControlImplExternal of IAccessControl { + fn has_role(self: @ContractState, role: RoleId, account: ContractAddress) -> bool { + self.role_members.read((role, account)) + } + + fn get_role_admin(self: @ContractState, role: RoleId) -> RoleId { + self.role_admin.read(role) + } + } + + #[generate_trait] + impl AccessControlImplInternal of IAccessControlInternal { + fn grant_role(ref self: ContractState, role: RoleId, account: ContractAddress) { + let admin = self.get_role_admin(:role); + self.assert_only_role(role: admin); + self._grant_role(:role, :account); + } + + fn revoke_role(ref self: ContractState, role: RoleId, account: ContractAddress) { + let admin = self.get_role_admin(:role); + self.assert_only_role(role: admin); + self._revoke_role(:role, :account); + } + + fn renounce_role(ref self: ContractState, role: RoleId, account: ContractAddress) { + assert(get_caller_address() == account, AccessErrors::ONLY_SELF_CAN_RENOUNCE); + self._revoke_role(:role, :account); + } + } + + #[generate_trait] + impl InternalAccessControl of _InternalAccessControl { + fn assert_only_role(self: @ContractState, role: RoleId) { + let authorized: bool = self.has_role(:role, account: get_caller_address()); + assert(authorized, AccessErrors::CALLER_MISSING_ROLE); + } + + // + // WARNING + // This method is unprotected and should be used only from the contract's constructor or + // from grant_role. + // + fn _grant_role(ref self: ContractState, role: RoleId, account: ContractAddress) { + if !self.has_role(:role, :account) { + self.role_members.write((role, account), true); + self.emit(RoleGranted { role, account, sender: get_caller_address() }); + } + } + + // + // WARNING + // This method is unprotected and should be used only from revoke_role or from + // renounce_role. + // + fn _revoke_role(ref self: ContractState, role: RoleId, account: ContractAddress) { + if self.has_role(:role, :account) { + self.role_members.write((role, account), false); + self.emit(RoleRevoked { role, account, sender: get_caller_address() }); + } + } + + // + // WARNING + // This method is unprotected and should not be used outside of a contract's constructor. + // + + fn _set_role_admin(ref self: ContractState, role: RoleId, admin_role: RoleId) { + let previous_admin_role = self.get_role_admin(:role); + self.role_admin.write(role, admin_role); + self.emit(RoleAdminChanged { role, previous_admin_role, new_admin_role: admin_role }); + } + } + + #[external(v0)] + impl RolesImpl of IMinimalRoles { + fn is_governance_admin(self: @ContractState, account: ContractAddress) -> bool { + self.has_role(role: GOVERNANCE_ADMIN, :account) + } + + fn is_upgrade_governor(self: @ContractState, account: ContractAddress) -> bool { + self.has_role(role: UPGRADE_GOVERNOR, :account) + } + + fn register_governance_admin(ref self: ContractState, account: ContractAddress) { + let event = Event::GovernanceAdminAdded( + GovernanceAdminAdded { added_account: account, added_by: get_caller_address() } + ); + self._grant_role_and_emit(role: GOVERNANCE_ADMIN, :account, :event); + } + + fn remove_governance_admin(ref self: ContractState, account: ContractAddress) { + let event = Event::GovernanceAdminRemoved( + GovernanceAdminRemoved { + removed_account: account, removed_by: get_caller_address() + } + ); + self._revoke_role_and_emit(role: GOVERNANCE_ADMIN, :account, :event); + } + + fn register_upgrade_governor(ref self: ContractState, account: ContractAddress) { + let event = Event::UpgradeGovernorAdded( + UpgradeGovernorAdded { added_account: account, added_by: get_caller_address() } + ); + self._grant_role_and_emit(role: UPGRADE_GOVERNOR, :account, :event); + } + + fn remove_upgrade_governor(ref self: ContractState, account: ContractAddress) { + let event = Event::UpgradeGovernorRemoved( + UpgradeGovernorRemoved { + removed_account: account, removed_by: get_caller_address() + } + ); + self._revoke_role_and_emit(role: UPGRADE_GOVERNOR, :account, :event); + } + + fn renounce(ref self: ContractState, role: RoleId) { + assert(role != GOVERNANCE_ADMIN, AccessErrors::GOV_ADMIN_CANNOT_RENOUNCE); + self.renounce_role(:role, account: get_caller_address()) + } + } + + + // + // External + // + + #[external(v0)] + impl ERC20Impl of IERC20 { + /// Returns the name of the token. + fn name(self: @ContractState) -> felt252 { + self.ERC20_name.read() + } + + /// Returns the ticker symbol of the token, usually a shorter version of the name. + fn symbol(self: @ContractState) -> felt252 { + self.ERC20_symbol.read() + } + + /// Returns the number of decimals used to get its user representation. + fn decimals(self: @ContractState) -> u8 { + self.ERC20_decimals.read() + } + + /// Returns the value of tokens in existence. + fn total_supply(self: @ContractState) -> u256 { + self.ERC20_total_supply.read() + } + + /// Returns the amount of tokens owned by `account`. + fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { + self.ERC20_balances.read(account) + } + + /// Returns the remaining number of tokens that `spender` is + /// allowed to spend on behalf of `owner` through [transfer_from](transfer_from). + /// This is zero by default. + /// This value changes when [approve](approve) or [transfer_from](transfer_from) + /// are called. + fn allowance( + self: @ContractState, owner: ContractAddress, spender: ContractAddress + ) -> u256 { + self.ERC20_allowances.read((owner, spender)) + } + + /// Moves `amount` tokens from the caller's token balance to `to`. + /// Emits a [Transfer](Transfer) event. + fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool { + let sender = get_caller_address(); + self._transfer(sender, recipient, amount); + true + } + + /// Moves `amount` tokens from `from` to `to` using the allowance mechanism. + /// `amount` is then deducted from the caller's allowance. + /// Emits a [Transfer](Transfer) event. + fn transfer_from( + ref self: ContractState, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) -> bool { + let caller = get_caller_address(); + self._spend_allowance(sender, caller, amount); + self._transfer(sender, recipient, amount); + true + } + + /// Sets `amount` as the allowance of `spender` over the caller’s tokens. + fn approve(ref self: ContractState, spender: ContractAddress, amount: u256) -> bool { + let caller = get_caller_address(); + self._approve(caller, spender, amount); + true + } + } + + /// Increases the allowance granted from the caller to `spender` by `added_value`. + /// Emits an [Approval](Approval) event indicating the updated allowance. + #[external(v0)] + fn increase_allowance( + ref self: ContractState, spender: ContractAddress, added_value: u256 + ) -> bool { + self._increase_allowance(spender, added_value) + } + + /// Decreases the allowance granted from the caller to `spender` by `subtracted_value`. + /// Emits an [Approval](Approval) event indicating the updated allowance. + #[external(v0)] + fn decrease_allowance( + ref self: ContractState, spender: ContractAddress, subtracted_value: u256 + ) -> bool { + self._decrease_allowance(spender, subtracted_value) + } + + #[external(v0)] + impl ERC20CamelOnlyImpl of IERC20CamelOnly { + /// Camel case support. + /// See [total_supply](total-supply). + fn totalSupply(self: @ContractState) -> u256 { + ERC20Impl::total_supply(self) + } + + /// Camel case support. + /// See [balance_of](balance_of). + fn balanceOf(self: @ContractState, account: ContractAddress) -> u256 { + ERC20Impl::balance_of(self, account) + } + + /// Camel case support. + /// See [transfer_from](transfer_from). + fn transferFrom( + ref self: ContractState, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) -> bool { + ERC20Impl::transfer_from(ref self, sender, recipient, amount) + } + } + + /// Camel case support. + /// See [increase_allowance](increase_allowance). + #[external(v0)] + fn increaseAllowance( + ref self: ContractState, spender: ContractAddress, addedValue: u256 + ) -> bool { + increase_allowance(ref self, spender, addedValue) + } + + /// Camel case support. + /// See [decrease_allowance](decrease_allowance). + #[external(v0)] + fn decreaseAllowance( + ref self: ContractState, spender: ContractAddress, subtractedValue: u256 + ) -> bool { + decrease_allowance(ref self, spender, subtractedValue) + } + + // + // Internal + // + + #[generate_trait] + impl InternalImpl of InternalTrait { + /// Initializes the contract by setting the token name and symbol. + /// To prevent reinitialization, this should only be used inside of a contract constructor. + fn initializer(ref self: ContractState, name: felt252, symbol: felt252, decimals: u8) { + self.ERC20_name.write(name); + self.ERC20_symbol.write(symbol); + self.ERC20_decimals.write(decimals); + } + + /// Internal method that moves an `amount` of tokens from `from` to `to`. + /// Emits a [Transfer](Transfer) event. + fn _transfer( + ref self: ContractState, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) { + assert(!sender.is_zero(), ERC20Errors::TRANSFER_FROM_ZERO); + assert(!recipient.is_zero(), ERC20Errors::TRANSFER_TO_ZERO); + self.ERC20_balances.write(sender, self.ERC20_balances.read(sender) - amount); + self.ERC20_balances.write(recipient, self.ERC20_balances.read(recipient) + amount); + self.emit(Transfer { from: sender, to: recipient, value: amount }); + } + + /// Internal method that sets `amount` as the allowance of `spender` over the + /// `owner`s tokens. + /// Emits an [Approval](Approval) event. + fn _approve( + ref self: ContractState, owner: ContractAddress, spender: ContractAddress, amount: u256 + ) { + assert(!owner.is_zero(), ERC20Errors::APPROVE_FROM_ZERO); + assert(!spender.is_zero(), ERC20Errors::APPROVE_TO_ZERO); + self.ERC20_allowances.write((owner, spender), amount); + self.emit(Approval { owner, spender, value: amount }); + } + + /// Creates a `value` amount of tokens and assigns them to `account`. + /// Emits a [Transfer](Transfer) event with `from` set to the zero address. + fn _mint(ref self: ContractState, recipient: ContractAddress, amount: u256) { + assert(!recipient.is_zero(), ERC20Errors::MINT_TO_ZERO); + self.ERC20_total_supply.write(self.ERC20_total_supply.read() + amount); + self.ERC20_balances.write(recipient, self.ERC20_balances.read(recipient) + amount); + self.emit(Transfer { from: Zeroable::zero(), to: recipient, value: amount }); + } + + /// Destroys a `value` amount of tokens from `account`. + /// Emits a [Transfer](Transfer) event with `to` set to the zero address. + fn _burn(ref self: ContractState, account: ContractAddress, amount: u256) { + assert(!account.is_zero(), ERC20Errors::BURN_FROM_ZERO); + self.ERC20_total_supply.write(self.ERC20_total_supply.read() - amount); + self.ERC20_balances.write(account, self.ERC20_balances.read(account) - amount); + self.emit(Transfer { from: account, to: Zeroable::zero(), value: amount }); + } + + /// Internal method for the external [increase_allowance](increase_allowance). + /// Emits an [Approval](Approval) event indicating the updated allowance. + fn _increase_allowance( + ref self: ContractState, spender: ContractAddress, added_value: u256 + ) -> bool { + let caller = get_caller_address(); + self + ._approve( + caller, spender, self.ERC20_allowances.read((caller, spender)) + added_value + ); + true + } + + /// Internal method for the external [decrease_allowance](decrease_allowance). + /// Emits an [Approval](Approval) event indicating the updated allowance. + fn _decrease_allowance( + ref self: ContractState, spender: ContractAddress, subtracted_value: u256 + ) -> bool { + let caller = get_caller_address(); + self + ._approve( + caller, + spender, + self.ERC20_allowances.read((caller, spender)) - subtracted_value + ); + true + } + + /// Updates `owner`s allowance for `spender` based on spent `amount`. + /// Does not update the allowance value in case of infinite allowance. + /// Possibly emits an [Approval](Approval) event. + fn _spend_allowance( + ref self: ContractState, owner: ContractAddress, spender: ContractAddress, amount: u256 + ) { + let current_allowance = self.ERC20_allowances.read((owner, spender)); + if current_allowance != BoundedInt::max() { + self._approve(owner, spender, current_allowance - amount); + } + } + } +} diff --git a/l2/src/access_control_interface.cairo b/l2/src/access_control_interface.cairo new file mode 100644 index 0000000..60e4eef --- /dev/null +++ b/l2/src/access_control_interface.cairo @@ -0,0 +1,42 @@ +use starknet::ContractAddress; + + +type RoleId = felt252; + +#[starknet::interface] +trait IAccessControl { + fn has_role(self: @TContractState, role: RoleId, account: ContractAddress) -> bool; + fn get_role_admin(self: @TContractState, role: RoleId) -> RoleId; +} + +// An event that is emitted when `account` is granted `role`. +// `sender` is the account that originated the contract call, an admin role +// bearer (except if `_grant_role` is called during initialization from the constructor). +#[derive(Copy, Drop, PartialEq, starknet::Event)] +struct RoleGranted { + role: RoleId, + account: ContractAddress, + sender: ContractAddress, +} + +// An event that is emitted when `account` is revoked `role`. +// `sender` is the account that originated the contract call: +// - If using `revoke_role`, it is the admin role bearer. +// - If using `renounce_role`, it is the role bearer (i.e. `account`). +#[derive(Copy, Drop, PartialEq, starknet::Event)] +struct RoleRevoked { + role: RoleId, + account: ContractAddress, + sender: ContractAddress, +} + +// An event that is emitted when `new_admin_role` is set as `role`'s admin role, replacing +// `previous_admin_role`. +// `DEFAULT_ADMIN_ROLE`(0) is the starting admin for all roles, despite {RoleAdminChanged} not +// being emitted signaling this. +#[derive(Copy, Drop, PartialEq, starknet::Event)] +struct RoleAdminChanged { + role: RoleId, + previous_admin_role: RoleId, + new_admin_role: RoleId, +} diff --git a/l2/src/err_msg.cairo b/l2/src/err_msg.cairo new file mode 100644 index 0000000..e623451 --- /dev/null +++ b/l2/src/err_msg.cairo @@ -0,0 +1,35 @@ +mod ERC20Errors { + const APPROVE_FROM_ZERO: felt252 = 'ERC20: approve from 0'; + const APPROVE_TO_ZERO: felt252 = 'ERC20: approve to 0'; + const TRANSFER_FROM_ZERO: felt252 = 'ERC20: transfer from 0'; + const TRANSFER_TO_ZERO: felt252 = 'ERC20: transfer to 0'; + const BURN_FROM_ZERO: felt252 = 'ERC20: burn from 0'; + const MINT_TO_ZERO: felt252 = 'ERC20: mint to 0'; +} + +mod AccessErrors { + const INVALID_MINTER: felt252 = 'INVALID_MINTER_ADDRESS'; + const INVALID_TOKEN: felt252 = 'INVALID_TOKEN_ADDRESS'; + const CALLER_MISSING_ROLE: felt252 = 'CALLER_IS_MISSING_ROLE'; + const ZERO_ADDRESS: felt252 = 'INVALID_ACCOUNT_ADDRESS'; + const ALREADY_INITIALIZED: felt252 = 'ROLES_ALREADY_INITIALIZED'; + const ZERO_ADDRESS_GOV_ADMIN: felt252 = 'ZERO_PROVISIONAL_GOV_ADMIN'; + const ONLY_APP_GOVERNOR: felt252 = 'ONLY_APP_GOVERNOR'; + const ONLY_OPERATOR: felt252 = 'ONLY_OPERATOR'; + const ONLY_TOKEN_ADMIN: felt252 = 'ONLY_TOKEN_ADMIN'; + const ONLY_UPGRADE_GOVERNOR: felt252 = 'ONLY_UPGRADE_GOVERNOR'; + const ONLY_SECURITY_ADMIN: felt252 = 'ONLY_SECURITY_ADMIN'; + const ONLY_SECURITY_AGENT: felt252 = 'ONLY_SECURITY_AGENT'; + const ONLY_MINTER: felt252 = 'MINTER_ONLY'; + const ONLY_SELF_CAN_RENOUNCE: felt252 = 'ONLY_SELF_CAN_RENOUNCE'; + const GOV_ADMIN_CANNOT_RENOUNCE: felt252 = 'GOV_ADMIN_CANNOT_SELF_REMOVE'; +} + +mod ReplaceErrors { + const FINALIZED: felt252 = 'FINALIZED'; + const UNKNOWN_IMPLEMENTATION: felt252 = 'UNKNOWN_IMPLEMENTATION'; + const NOT_ENABLED_YET: felt252 = 'NOT_ENABLED_YET'; + const IMPLEMENTATION_EXPIRED: felt252 = 'IMPLEMENTATION_EXPIRED'; + const EIC_LIB_CALL_FAILED: felt252 = 'EIC_LIB_CALL_FAILED'; + const REPLACE_CLASS_HASH_FAILED: felt252 = 'REPLACE_CLASS_HASH_FAILED'; +} diff --git a/l2/src/lib.cairo b/l2/src/lib.cairo new file mode 100644 index 0000000..b3d73be --- /dev/null +++ b/l2/src/lib.cairo @@ -0,0 +1,8 @@ +// Interfaces. +mod access_control_interface; +mod mintable_token_interface; +mod replaceability_interface; +mod roles_interface; + +// Modules. +mod err_msg; diff --git a/l2/src/mintable_token_interface.cairo b/l2/src/mintable_token_interface.cairo new file mode 100644 index 0000000..12fca4a --- /dev/null +++ b/l2/src/mintable_token_interface.cairo @@ -0,0 +1,13 @@ +use starknet::ContractAddress; + +#[starknet::interface] +trait IMintableToken { + fn permissioned_mint(ref self: TContractState, account: ContractAddress, amount: u256); + fn permissioned_burn(ref self: TContractState, account: ContractAddress, amount: u256); +} + +#[starknet::interface] +trait IMintableTokenCamel { + fn permissionedMint(ref self: TContractState, account: ContractAddress, amount: u256); + fn permissionedBurn(ref self: TContractState, account: ContractAddress, amount: u256); +} diff --git a/l2/src/replaceability_interface.cairo b/l2/src/replaceability_interface.cairo new file mode 100644 index 0000000..d7aeffe --- /dev/null +++ b/l2/src/replaceability_interface.cairo @@ -0,0 +1,72 @@ +use array::ArrayTrait; +use array::SpanTrait; +use option::OptionTrait; + +use starknet::class_hash::{ClassHash, Felt252TryIntoClassHash}; + +// Holds EIC data. +// * eic_hash is the EIC class hash. +// * eic_init_data is a span of the EIC init args. +#[derive(Copy, Drop, Serde, PartialEq)] +struct EICData { + eic_hash: ClassHash, + eic_init_data: Span +} + +// Holds implementation data. +// * impl_hash is the implementation class hash. +// * eic_data is the EIC data when applicable, and empty otherwise. +// * final indicates whether the implementation is finalized. +#[derive(Copy, Drop, Serde, PartialEq)] +struct ImplementationData { + impl_hash: ClassHash, + eic_data: Option, + final: bool +} + + +// starknet_keccak(eic_initialize). +const EIC_INITIALIZE_SELECTOR: felt252 = + 1770792127795049777084697565458798191120226931451376769053057094489776256516; + +// Duration from implementation is eligible until it expires. (1209600 = 2 weeks). +const IMPLEMENTATION_EXPIRATION: u64 = 1209600; + +#[starknet::interface] +trait IEICInitializable { + fn eic_initialize(ref self: TContractState, eic_init_data: Span); +} + + +#[starknet::interface] +trait IReplaceable { + fn get_upgrade_delay(self: @TContractState) -> u64; + fn get_impl_activation_time( + self: @TContractState, implementation_data: ImplementationData + ) -> u64; + fn add_new_implementation(ref self: TContractState, implementation_data: ImplementationData); + fn remove_implementation(ref self: TContractState, implementation_data: ImplementationData); + fn replace_to(ref self: TContractState, implementation_data: ImplementationData); +} + +#[derive(Copy, Drop, PartialEq, starknet::Event)] +struct ImplementationAdded { + implementation_data: ImplementationData, +} + + +#[derive(Copy, Drop, PartialEq, starknet::Event)] +struct ImplementationRemoved { + implementation_data: ImplementationData, +} + + +#[derive(Copy, Drop, PartialEq, starknet::Event)] +struct ImplementationReplaced { + implementation_data: ImplementationData, +} + +#[derive(Copy, Drop, PartialEq, starknet::Event)] +struct ImplementationFinalized { + impl_hash: ClassHash, +} diff --git a/l2/src/roles_interface.cairo b/l2/src/roles_interface.cairo new file mode 100644 index 0000000..ad7dd8e --- /dev/null +++ b/l2/src/roles_interface.cairo @@ -0,0 +1,172 @@ +use starknet::ContractAddress; +use super::access_control_interface::RoleId; + +// Role | Role Admin +// ---------------------------------------- +// GOVERNANCE_ADMIN | GOVERNANCE_ADMIN +// UPGRADE_GOVERNOR | GOVERNANCE_ADMIN +// APP_ROLE_ADMIN | GOVERNANCE_ADMIN +// APP_GOVERNOR | APP_ROLE_ADMIN +// OPERATOR | APP_ROLE_ADMIN +// TOKEN_ADMIN | APP_ROLE_ADMIN +// SECURITY_ADMIN | SECURITY_ADMIN +// SECURITY_AGENT | SECURITY_ADMIN. + +// int.from_bytes(Web3.keccak(text="ROLE_APP_GOVERNOR"), "big") & MASK_250 . +const APP_GOVERNOR: RoleId = 0xd2ead78c620e94b02d0a996e99298c59ddccfa1d8a0149080ac3a20de06068; + +// int.from_bytes(Web3.keccak(text="ROLE_APP_ROLE_ADMIN"), "big") & MASK_250 . +const APP_ROLE_ADMIN: RoleId = 0x3e615638e0b79444a70f8c695bf8f2a47033bf1cf95691ec3130f64939cee99; + +// int.from_bytes(Web3.keccak(text="ROLE_GOVERNANCE_ADMIN"), "big") & MASK_250 . +const GOVERNANCE_ADMIN: RoleId = 0x3711c9d994faf6055172091cb841fd4831aa743e6f3315163b06a122c841846; + +// int.from_bytes(Web3.keccak(text="ROLE_OPERATOR"), "big") & MASK_250 . +const OPERATOR: RoleId = 0x023edb77f7c8cc9e38e8afe78954f703aeeda7fffe014eeb6e56ea84e62f6da7; + +// int.from_bytes(Web3.keccak(text="ROLE_TOKEN_ADMIN"), "big") & MASK_250 . +const TOKEN_ADMIN: RoleId = 0x0128d63adbf6b09002c26caf55c47e2f26635807e3ef1b027218aa74c8d61a3e; + +// int.from_bytes(Web3.keccak(text="ROLE_UPGRADE_GOVERNOR"), "big") & MASK_250 . +const UPGRADE_GOVERNOR: RoleId = 0x251e864ca2a080f55bce5da2452e8cfcafdbc951a3e7fff5023d558452ec228; + +// int.from_bytes(Web3.keccak(text="ROLE_SECURITY_ADMIN"), "big") & MASK_250 . +const SECURITY_ADMIN: RoleId = 0x26bd110619d11cfdfc28e281df893bc24828e89177318e9dbd860cdaedeb6b3; + +// int.from_bytes(Web3.keccak(text="ROLE_SECURITY_AGENT"), "big") & MASK_250 . +const SECURITY_AGENT: RoleId = 0x37693ba312785932d430dccf0f56ffedd0aa7c0f8b6da2cc4530c2717689b96; + +#[starknet::interface] +trait IRoles { + fn is_app_governor(self: @TContractState, account: ContractAddress) -> bool; + fn is_app_role_admin(self: @TContractState, account: ContractAddress) -> bool; + fn is_governance_admin(self: @TContractState, account: ContractAddress) -> bool; + fn is_operator(self: @TContractState, account: ContractAddress) -> bool; + fn is_token_admin(self: @TContractState, account: ContractAddress) -> bool; + fn is_upgrade_governor(self: @TContractState, account: ContractAddress) -> bool; + fn is_security_admin(self: @TContractState, account: ContractAddress) -> bool; + fn is_security_agent(self: @TContractState, account: ContractAddress) -> bool; + fn register_app_governor(ref self: TContractState, account: ContractAddress); + fn remove_app_governor(ref self: TContractState, account: ContractAddress); + fn register_app_role_admin(ref self: TContractState, account: ContractAddress); + fn remove_app_role_admin(ref self: TContractState, account: ContractAddress); + fn register_governance_admin(ref self: TContractState, account: ContractAddress); + fn remove_governance_admin(ref self: TContractState, account: ContractAddress); + fn register_operator(ref self: TContractState, account: ContractAddress); + fn remove_operator(ref self: TContractState, account: ContractAddress); + fn register_token_admin(ref self: TContractState, account: ContractAddress); + fn remove_token_admin(ref self: TContractState, account: ContractAddress); + fn register_upgrade_governor(ref self: TContractState, account: ContractAddress); + fn remove_upgrade_governor(ref self: TContractState, account: ContractAddress); + fn renounce(ref self: TContractState, role: RoleId); + fn register_security_admin(ref self: TContractState, account: ContractAddress); + fn remove_security_admin(ref self: TContractState, account: ContractAddress); + fn register_security_agent(ref self: TContractState, account: ContractAddress); + fn remove_security_agent(ref self: TContractState, account: ContractAddress); +} + +#[starknet::interface] +trait IMinimalRoles { + fn is_governance_admin(self: @TContractState, account: ContractAddress) -> bool; + fn is_upgrade_governor(self: @TContractState, account: ContractAddress) -> bool; + fn register_governance_admin(ref self: TContractState, account: ContractAddress); + fn remove_governance_admin(ref self: TContractState, account: ContractAddress); + fn register_upgrade_governor(ref self: TContractState, account: ContractAddress); + fn remove_upgrade_governor(ref self: TContractState, account: ContractAddress); + fn renounce(ref self: TContractState, role: RoleId); +} + +#[derive(Copy, Drop, PartialEq, starknet::Event)] +struct AppGovernorAdded { + added_account: ContractAddress, + added_by: ContractAddress, +} + +#[derive(Copy, Drop, PartialEq, starknet::Event)] +struct AppGovernorRemoved { + removed_account: ContractAddress, + removed_by: ContractAddress, +} +#[derive(Copy, Drop, PartialEq, starknet::Event)] +struct AppRoleAdminAdded { + added_account: ContractAddress, + added_by: ContractAddress, +} + +#[derive(Copy, Drop, PartialEq, starknet::Event)] +struct AppRoleAdminRemoved { + removed_account: ContractAddress, + removed_by: ContractAddress, +} + +#[derive(Copy, Drop, PartialEq, starknet::Event)] +struct GovernanceAdminAdded { + added_account: ContractAddress, + added_by: ContractAddress, +} + +#[derive(Copy, Drop, PartialEq, starknet::Event)] +struct GovernanceAdminRemoved { + removed_account: ContractAddress, + removed_by: ContractAddress, +} + +#[derive(Copy, Drop, PartialEq, starknet::Event)] +struct OperatorAdded { + added_account: ContractAddress, + added_by: ContractAddress, +} + +#[derive(Copy, Drop, PartialEq, starknet::Event)] +struct SecurityAgentAdded { + added_account: ContractAddress, + added_by: ContractAddress, +} + +#[derive(Copy, Drop, PartialEq, starknet::Event)] +struct SecurityAgentRemoved { + removed_account: ContractAddress, + removed_by: ContractAddress, +} + +#[derive(Copy, Drop, PartialEq, starknet::Event)] +struct SecurityAdminAdded { + added_account: ContractAddress, + added_by: ContractAddress, +} + +#[derive(Copy, Drop, PartialEq, starknet::Event)] +struct SecurityAdminRemoved { + removed_account: ContractAddress, + removed_by: ContractAddress, +} + +#[derive(Copy, Drop, PartialEq, starknet::Event)] +struct OperatorRemoved { + removed_account: ContractAddress, + removed_by: ContractAddress, +} + +#[derive(Copy, Drop, PartialEq, starknet::Event)] +struct TokenAdminAdded { + added_account: ContractAddress, + added_by: ContractAddress, +} + +#[derive(Copy, Drop, PartialEq, starknet::Event)] +struct TokenAdminRemoved { + removed_account: ContractAddress, + removed_by: ContractAddress, +} + +#[derive(Copy, Drop, PartialEq, starknet::Event)] +struct UpgradeGovernorAdded { + added_account: ContractAddress, + added_by: ContractAddress, +} + +#[derive(Copy, Drop, PartialEq, starknet::Event)] +struct UpgradeGovernorRemoved { + removed_account: ContractAddress, + removed_by: ContractAddress, +} diff --git a/scripts/compile_l2.sh b/scripts/compile_l2.sh new file mode 100755 index 0000000..b77d4dc --- /dev/null +++ b/scripts/compile_l2.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +set -e + +SCRIPT_DIR=$( cd -- "$( dirname "$0" )" &> /dev/null && pwd ) +REPO_ROOT=$( cd -- "$( dirname $( dirname "$0" ) )" &> /dev/null && pwd ) + +compile () { + MODULE="$1" + NAME="$2" + OUTPUT="$REPO_ROOT/build/$NAME.json" + + echo "Compiling $MODULE::$NAME" + + # This is better than using the output option, which does not emit EOL at the end. + starknet-compile -c "$MODULE::$NAME" $REPO_ROOT > $OUTPUT + + if [ -n "$USER_ID" ] && [ -n "$GROUP_ID" ]; then + chown $USER_ID:$GROUP_ID $OUTPUT + fi +} + +mkdir -p "$REPO_ROOT/build" + +compile openzeppelin::token::erc20_v070::erc20 ERC20 + +if [ -n "$USER_ID" ] && [ -n "$GROUP_ID" ]; then + chown -R $USER_ID:$GROUP_ID "$REPO_ROOT/build" +fi diff --git a/scripts/compile_l2_with_docker.sh b/scripts/compile_l2_with_docker.sh new file mode 100755 index 0000000..072f5cf --- /dev/null +++ b/scripts/compile_l2_with_docker.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +set -e + +SCRIPT_DIR=$( cd -- "$( dirname "$0" )" &> /dev/null && pwd ) +REPO_ROOT=$( cd -- "$( dirname $( dirname "$0" ) )" &> /dev/null && pwd ) + +# Deterministically generate contract artifacts +docker run --rm \ + -v "$REPO_ROOT:/work" \ + --env "USER_ID=$(id -u)" \ + --env "GROUP_ID=$(id -g)" \ + --entrypoint sh \ + starknet/cairo:2.3.0 \ + -c "cd /work && ./scripts/compile_l2.sh"