From b847776b4c88adcc308bd8ff2a229a34eb9f6be5 Mon Sep 17 00:00:00 2001 From: Jonathan LEI Date: Thu, 7 Mar 2024 11:10:39 +0000 Subject: [PATCH] feat: add L2 token contract --- .github/workflows/artifacts-dev.yaml | 7 +- .github/workflows/artifacts-release.yaml | 7 +- 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 +++++ 14 files changed, 1138 insertions(+), 2 deletions(-) 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 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/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, +}