diff --git a/requirements.txt b/requirements.txt index d5303e6..42c8fb2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ black==23.1a1 cairo-lang==0.11.2 web3==5.31.3 +pytest==7.4.4 diff --git a/scripts/build-cairo.sh b/scripts/build-cairo.sh index a59f3a1..1878061 100755 --- a/scripts/build-cairo.sh +++ b/scripts/build-cairo.sh @@ -3,11 +3,12 @@ pushd $(dirname $0)/.. set -e mkdir -p cairo_contracts +scripts/starknet-compile.py src --contract-path src::strk::erc20_lockable::ERC20Lockable cairo_contracts/ERC20Lockable.sierra scripts/starknet-compile.py src --contract-path src::update_712_vars_eic::Update712VarsEIC cairo_contracts/Update712VarsEIC.sierra scripts/starknet-compile.py src --contract-path src::roles_init_eic::RolesExternalInitializer cairo_contracts/RolesExternalInitializer.sierra scripts/starknet-compile.py src --contract-path src::legacy_bridge_eic::LegacyBridgeUpgradeEIC cairo_contracts/LegacyBridgeUpgradeEIC.sierra scripts/starknet-compile.py src --contract-path src::token_bridge::TokenBridge cairo_contracts/TokenBridge.sierra -scripts/starknet-compile.py src --contract-path openzeppelin::token::erc20::presets::erc20votes::ERC20VotesPreset cairo_contracts/ERC20VotesPreset.sierra +scripts/starknet-compile.py src --contract-path openzeppelin::token::erc20::presets::erc20_votes_lock::ERC20VotesLock cairo_contracts/ERC20VotesLock.sierra scripts/starknet-compile.py src --contract-path openzeppelin::token::erc20_v070::erc20::ERC20 cairo_contracts/ERC20.sierra set +e popd diff --git a/src/cairo/err_msg.cairo b/src/cairo/err_msg.cairo index 81f2c6f..e623451 100644 --- a/src/cairo/err_msg.cairo +++ b/src/cairo/err_msg.cairo @@ -9,11 +9,17 @@ mod ERC20Errors { 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'; diff --git a/src/cairo/legacy_bridge_tester.cairo b/src/cairo/legacy_bridge_tester.cairo index 3f61b5d..51b7a9e 100644 --- a/src/cairo/legacy_bridge_tester.cairo +++ b/src/cairo/legacy_bridge_tester.cairo @@ -145,18 +145,15 @@ mod LegacyBridgeTester { impl internals of _internals { fn only_upgrade_governor(self: @ContractState) {} - // 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 ) { @@ -164,7 +161,6 @@ mod LegacyBridgeTester { 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 { @@ -172,7 +168,6 @@ mod LegacyBridgeTester { 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 ) { diff --git a/src/cairo/legacy_eic_test.cairo b/src/cairo/legacy_eic_test.cairo index 96cbedb..1604569 100644 --- a/src/cairo/legacy_eic_test.cairo +++ b/src/cairo/legacy_eic_test.cairo @@ -10,7 +10,7 @@ mod legacy_eic_test { use serde::Serde; use starknet::class_hash::{ClassHash, class_hash_const}; use starknet::{ContractAddress, EthAddress, EthAddressZeroable, syscalls::deploy_syscall}; - use openzeppelin::token::erc20::presets::erc20votes::ERC20VotesPreset::{ + use openzeppelin::token::erc20::presets::erc20_votes_lock::ERC20VotesLock::{ DAPP_NAME, DAPP_VERSION }; @@ -24,7 +24,7 @@ mod legacy_eic_test { use src::roles_interface::{IRolesDispatcher, IRolesDispatcherTrait}; use src::test_utils::test_utils::{ caller, get_roles, get_token_bridge, set_contract_address_as_caller, get_replaceable, - simple_deploy_l2_token, DEFAULT_UPGRADE_DELAY + simple_deploy_token, DEFAULT_UPGRADE_DELAY }; use src::token_bridge_interface::{ITokenBridgeDispatcher, ITokenBridgeDispatcherTrait}; use src::token_bridge::TokenBridge; @@ -181,8 +181,8 @@ mod legacy_eic_test { #[test] #[available_gas(30000000)] fn test_happy_path() { - let l2_token = simple_deploy_l2_token(); - let tester_address = deploy_legacy_tester(l2_token); + let l2_token = simple_deploy_token(); + let tester_address = deploy_legacy_tester(:l2_token); let impl_data = token_bridge_w_eic_implementation_data( l1_token: L1_TOKEN_ADDRESS(), :l2_token, ); @@ -191,7 +191,7 @@ mod legacy_eic_test { ); let token_bridge = get_token_bridge(tester_address); - let l1_token = token_bridge.get_l1_token(l2_token); + let l1_token = token_bridge.get_l1_token(:l2_token); let l2_token_actual = token_bridge.get_l2_token(L1_TOKEN_ADDRESS()); assert(L1_TOKEN_ADDRESS() == l1_token, 'L1_ZEROED'); assert(l2_token == l2_token_actual, 'L2_ZEROED'); @@ -206,7 +206,7 @@ mod legacy_eic_test { add_impl_and_replace_to(replaceable_address: tester1, :implementation_data); // Tester 1 roles are not initialzied, and gov admin not set. - let roles1 = get_roles(tester1); + let roles1 = get_roles(contract_address: tester1); assert(!roles1.is_governance_admin(caller()), 'Roles should not be initialized'); assert(!roles1.is_upgrade_governor(caller()), 'Roles should not be initialized'); assert(!roles1.is_security_admin(caller()), 'Roles should not be initialized'); @@ -217,7 +217,7 @@ mod legacy_eic_test { add_impl_and_replace_to(replaceable_address: tester2, :implementation_data); // Tester 2 roles are initialized and gov admin assigned. - let roles = get_roles(tester2); + let roles = get_roles(contract_address: tester2); assert(roles.is_governance_admin(caller()), 'Roles should be initialized'); assert(roles.is_upgrade_governor(caller()), 'Roles should be initialized'); assert(roles.is_security_admin(caller()), 'Roles should be initialized'); @@ -322,7 +322,7 @@ mod legacy_eic_test { #[available_gas(30000000)] fn test_upgrade_an_upgraded() { // Test failing to upgrade twice. - let l2_token = simple_deploy_l2_token(); + let l2_token = simple_deploy_token(); let tester_address = deploy_legacy_tester(l2_token); let impl_data = token_bridge_w_eic_implementation_data( l1_token: L1_TOKEN_ADDRESS(), :l2_token, diff --git a/src/cairo/lib.cairo b/src/cairo/lib.cairo index b79179b..4868e45 100644 --- a/src/cairo/lib.cairo +++ b/src/cairo/lib.cairo @@ -1,9 +1,13 @@ +// STRK Token (ERC20Lockable). +mod strk; + // Interfaces. mod access_control_interface; mod token_bridge_admin_interface; mod token_bridge_interface; mod erc20_interface; mod mintable_token_interface; +mod mintable_lock_interface; mod replaceability_interface; mod roles_interface; mod receiver_interface; diff --git a/src/cairo/mintable_lock_interface.cairo b/src/cairo/mintable_lock_interface.cairo new file mode 100644 index 0000000..6bbcdae --- /dev/null +++ b/src/cairo/mintable_lock_interface.cairo @@ -0,0 +1,48 @@ +use starknet::ContractAddress; + +#[starknet::interface] +trait IMintableLock { + fn permissioned_lock_and_delegate( + ref self: TContractState, account: ContractAddress, delegatee: ContractAddress, amount: u256 + ); +} + +#[starknet::interface] +trait ILockingContract { + fn set_locking_contract(ref self: TContractState, locking_contract: ContractAddress); + fn get_locking_contract(self: @TContractState) -> ContractAddress; +} + +#[starknet::interface] +trait ILockAndDelegate { + fn lock_and_delegate(ref self: TContractState, delegatee: ContractAddress, amount: u256); + fn lock_and_delegate_by_sig( + ref self: TContractState, + account: ContractAddress, + delegatee: ContractAddress, + amount: u256, + nonce: felt252, + expiry: u64, + signature: Array + ); +} + +#[starknet::interface] +trait ITokenLock { + fn lock(ref self: TContractState, amount: u256); + fn unlock(ref self: TContractState, amount: u256); +} + +#[derive(Copy, Drop, PartialEq, starknet::Event)] +struct Locked { + #[key] + account: ContractAddress, + amount: u256 +} + +#[derive(Copy, Drop, PartialEq, starknet::Event)] +struct Unlocked { + #[key] + account: ContractAddress, + amount: u256 +} diff --git a/src/cairo/permissioned_token_test.cairo b/src/cairo/permissioned_token_test.cairo index bcd94f1..a11f79b 100644 --- a/src/cairo/permissioned_token_test.cairo +++ b/src/cairo/permissioned_token_test.cairo @@ -9,17 +9,17 @@ mod permissioned_token_test { use integer::BoundedInt; use serde::Serde; use starknet::{contract_address_const, ContractAddress, syscalls::deploy_syscall}; + use src::err_msg::AccessErrors as AccessErrors; use super::super::mintable_token_interface::{ IMintableTokenDispatcher, IMintableTokenDispatcherTrait }; use super::super::erc20_interface::{IERC20Dispatcher, IERC20DispatcherTrait}; use super::super::test_utils::test_utils::{ - get_erc20_token, deploy_l2_votes_token, deploy_l2_token, get_mintable_token, - get_l2_token_deployment_calldata + get_erc20_token, deploy_l2_token, get_mintable_token, get_l2_token_deployment_calldata }; - use openzeppelin::token::erc20::presets::erc20votes::ERC20VotesPreset; + use openzeppelin::token::erc20::presets::erc20_votes_lock::ERC20VotesLock; use openzeppelin::token::erc20_v070::erc20::ERC20; fn _l2_erc20(initial_supply: u256) -> ContractAddress { @@ -28,23 +28,6 @@ mod permissioned_token_test { deploy_l2_token(:initial_owner, :permitted_minter, :initial_supply) } - fn _l2_votes_erc20(initial_supply: u256) -> ContractAddress { - let initial_owner = starknet::contract_address_const::<10>(); - let permitted_minter = starknet::contract_address_const::<20>(); - deploy_l2_votes_token(:initial_owner, :permitted_minter, :initial_supply) - } - - #[test] - #[available_gas(30000000)] - fn test_votes_erc20_successful_permitted_mint() { - let initial_owner = starknet::contract_address_const::<10>(); - let permitted_minter = starknet::contract_address_const::<20>(); - let l2_token = deploy_l2_votes_token( - :initial_owner, :permitted_minter, initial_supply: 1000 - ); - _successful_permitted_mint(:l2_token, :initial_owner, :permitted_minter); - } - #[test] #[available_gas(30000000)] fn test_erc20_successful_permitted_mint() { @@ -85,21 +68,6 @@ mod permissioned_token_test { ); } - #[test] - #[should_panic(expected: ('u256_add Overflow', 'ENTRYPOINT_FAILED',))] - #[available_gas(30000000)] - fn test_votes_erc20_overflowing_permitted_mint() { - // Setup. - let initial_owner = starknet::contract_address_const::<10>(); - let permitted_minter = starknet::contract_address_const::<20>(); - - // Deploy the l2 token contract. - let l2_token = deploy_l2_votes_token( - :initial_owner, :permitted_minter, initial_supply: BoundedInt::max() - ); - _overflowing_permitted_mint(:l2_token, :initial_owner, :permitted_minter); - } - #[test] #[should_panic(expected: ('u256_add Overflow', 'ENTRYPOINT_FAILED',))] #[available_gas(30000000)] @@ -125,14 +93,6 @@ mod permissioned_token_test { mintable_token.permissioned_mint(account: mint_recipient, amount: 1); } - #[test] - #[should_panic(expected: ('MINTER_ONLY', 'ENTRYPOINT_FAILED',))] - #[available_gas(30000000)] - fn test_votes_erc20_unpermitted_permitted_mint() { - let l2_token = _l2_votes_erc20(initial_supply: 1000); - _unpermitted_permitted_mint(:l2_token); - } - #[test] #[should_panic(expected: ('MINTER_ONLY', 'ENTRYPOINT_FAILED',))] #[available_gas(30000000)] @@ -170,19 +130,10 @@ mod permissioned_token_test { .unwrap_err() .span(); assert(error_message.len() == 2, 'UNEXPECTED_ERROR_LEN_MISMATCH'); - assert(error_message.at(0) == @'INVALID_MINTER_ADDRESS', 'INVALID_MINTER_ADDRESS_ERROR'); - assert(error_message.at(1) == @'CONSTRUCTOR_FAILED', 'CONSTRUCTOR_ERROR_MISMATCH'); - } - - #[test] - #[available_gas(30000000)] - fn test_votes_erc20_successful_permitted_burn() { - let initial_owner = starknet::contract_address_const::<10>(); - let permitted_minter = starknet::contract_address_const::<20>(); - let l2_token = deploy_l2_votes_token( - :initial_owner, :permitted_minter, initial_supply: 1000 + assert( + error_message.at(0) == @AccessErrors::INVALID_MINTER, 'INVALID_MINTER_ADDRESS_ERROR' ); - _successful_permitted_burn(:l2_token, :initial_owner, :permitted_minter); + assert(error_message.at(1) == @'CONSTRUCTOR_FAILED', 'CONSTRUCTOR_ERROR_MISMATCH'); } #[test] @@ -216,21 +167,6 @@ mod permissioned_token_test { assert(erc20_token.total_supply() == expected_after, 'TOTAL_SUPPLY_PERM_BURN_ERROR'); } - #[test] - #[should_panic(expected: ('u256_sub Overflow', 'ENTRYPOINT_FAILED',))] - #[available_gas(30000000)] - fn test_votes_erc20_exceeding_amount_permitted_burn() { - // Setup. - let initial_owner = starknet::contract_address_const::<10>(); - let permitted_minter = starknet::contract_address_const::<20>(); - - // Deploy the l2 token contract. - let l2_token = deploy_l2_votes_token( - :initial_owner, :permitted_minter, initial_supply: 1000 - ); - _exceeding_amount_permitted_burn(:l2_token, :initial_owner, :permitted_minter); - } - #[test] #[should_panic(expected: ('u256_sub Overflow', 'ENTRYPOINT_FAILED',))] #[available_gas(30000000)] @@ -254,21 +190,6 @@ mod permissioned_token_test { mintable_token.permissioned_burn(account: initial_owner, amount: 1001); } - #[test] - #[should_panic(expected: ('MINTER_ONLY', 'ENTRYPOINT_FAILED',))] - #[available_gas(30000000)] - fn test_votes_erc20_unpermitted_permitted_burn() { - // Setup. - let initial_owner = starknet::contract_address_const::<10>(); - let permitted_minter = starknet::contract_address_const::<20>(); - - // Deploy the l2 token contract. - let l2_token = deploy_l2_votes_token( - :initial_owner, :permitted_minter, initial_supply: 1000 - ); - _unpermitted_permitted_burn(:l2_token, :initial_owner, :permitted_minter); - } - #[test] #[should_panic(expected: ('MINTER_ONLY', 'ENTRYPOINT_FAILED',))] #[available_gas(30000000)] diff --git a/src/cairo/replaceability_test.cairo b/src/cairo/replaceability_test.cairo index 9b08930..a7b7ca1 100644 --- a/src/cairo/replaceability_test.cairo +++ b/src/cairo/replaceability_test.cairo @@ -47,7 +47,8 @@ mod replaceability_test { caller, not_caller, initial_owner, set_contract_address_as_caller, set_contract_address_as_not_caller, pop_and_deserialize_last_event, pop_last_k_events, deserialize_event, get_erc20_token, get_replaceable, set_caller_as_upgrade_governor, - simple_deploy_l2_token, deploy_token_bridge, DEFAULT_UPGRADE_DELAY + simple_deploy_token, simple_deploy_lockable_token, deploy_token_bridge, + DEFAULT_UPGRADE_DELAY }; use super::super::replaceability_interface::{ EICData, ImplementationData, IReplaceable, IReplaceableDispatcher, @@ -120,7 +121,13 @@ mod replaceability_test { #[test] #[available_gas(30000000)] fn test_replaceability_erc20_get_upgrade_delay() { - _get_upgrade_delay(simple_deploy_l2_token()); + _get_upgrade_delay(simple_deploy_token()); + } + + #[test] + #[available_gas(30000000)] + fn test_replaceability_lockable_get_upgrade_delay() { + _get_upgrade_delay(simple_deploy_lockable_token()); } fn _get_upgrade_delay(replaceable_address: ContractAddress) { @@ -141,7 +148,14 @@ mod replaceability_test { #[test] #[available_gas(30000000)] fn test_replaceability_erc20_add_new_implementation() { - let replaceable_address = simple_deploy_l2_token(); + let replaceable_address = simple_deploy_token(); + _add_new_implementation(:replaceable_address); + } + + #[test] + #[available_gas(30000000)] + fn test_replaceability_lockable_add_new_implementation() { + let replaceable_address = simple_deploy_lockable_token(); _add_new_implementation(:replaceable_address); } @@ -187,7 +201,16 @@ mod replaceability_test { #[available_gas(30000000)] fn test_replaceability_erc20_add_new_implementation_not_upgrade_governor() { // Deploy the ERC20 token and continue with the test. - let replaceable_address = simple_deploy_l2_token(); + let replaceable_address = simple_deploy_token(); + _add_new_impl_not_upg_gov(:replaceable_address); + } + + #[test] + #[should_panic(expected: ('ONLY_UPGRADE_GOVERNOR', 'ENTRYPOINT_FAILED',))] + #[available_gas(30000000)] + fn test_replaceability_lockable_add_new_implementation_not_upgrade_governor() { + // Deploy the Lockable token and continue with the test. + let replaceable_address = simple_deploy_lockable_token(); _add_new_impl_not_upg_gov(:replaceable_address); } @@ -213,7 +236,14 @@ mod replaceability_test { #[test] #[available_gas(30000000)] fn test_replaceability_erc20_remove_implementation() { - let replaceable_address = simple_deploy_l2_token(); + let replaceable_address = simple_deploy_token(); + _remove_implementation(:replaceable_address); + } + + #[test] + #[available_gas(30000000)] + fn test_replaceability_lockable_remove_implementation() { + let replaceable_address = simple_deploy_lockable_token(); _remove_implementation(:replaceable_address); } @@ -273,7 +303,15 @@ mod replaceability_test { #[should_panic(expected: ('ONLY_UPGRADE_GOVERNOR', 'ENTRYPOINT_FAILED',))] #[available_gas(30000000)] fn test_replaceability_erc20_remove_implementation_not_upgrade_governor() { - let replaceable_address = simple_deploy_l2_token(); + let replaceable_address = simple_deploy_token(); + _remove_implementation_not_upgrade_governor(:replaceable_address); + } + + #[test] + #[should_panic(expected: ('ONLY_UPGRADE_GOVERNOR', 'ENTRYPOINT_FAILED',))] + #[available_gas(30000000)] + fn test_replaceability_lockable_remove_implementation_not_upgrade_governor() { + let replaceable_address = simple_deploy_lockable_token(); _remove_implementation_not_upgrade_governor(:replaceable_address); } @@ -297,7 +335,14 @@ mod replaceability_test { #[test] #[available_gas(30000000)] fn test_replaceability_erc20_replace_to_with_eic() { - let replaceable_address = simple_deploy_l2_token(); + let replaceable_address = simple_deploy_token(); + _replace_to_with_eic(:replaceable_address); + } + + #[test] + #[available_gas(30000000)] + fn test_replaceability_lockable_replace_to_with_eic() { + let replaceable_address = simple_deploy_lockable_token(); _replace_to_with_eic(:replaceable_address); } @@ -339,7 +384,14 @@ mod replaceability_test { #[test] #[available_gas(30000000)] fn test_replaceability_erc20_replace_to_nonfinal() { - let replaceable_address = simple_deploy_l2_token(); + let replaceable_address = simple_deploy_token(); + _replace_to_nonfinal(:replaceable_address); + } + + #[test] + #[available_gas(30000000)] + fn test_replaceability_lockable_replace_to_nonfinal() { + let replaceable_address = simple_deploy_lockable_token(); _replace_to_nonfinal(:replaceable_address); } @@ -386,7 +438,15 @@ mod replaceability_test { #[should_panic(expected: ('UNKNOWN_IMPLEMENTATION', 'ENTRYPOINT_FAILED',))] #[available_gas(30000000)] fn test_replaceability_erc20_remove_impl_on_replace() { - let replaceable_address = simple_deploy_l2_token(); + let replaceable_address = simple_deploy_token(); + _replace_remove_impl_on_replace(:replaceable_address); + } + + #[test] + #[should_panic(expected: ('UNKNOWN_IMPLEMENTATION', 'ENTRYPOINT_FAILED',))] + #[available_gas(30000000)] + fn test_replaceability_lockable_remove_impl_on_replace() { + let replaceable_address = simple_deploy_lockable_token(); _replace_remove_impl_on_replace(:replaceable_address); } @@ -447,7 +507,15 @@ mod replaceability_test { #[available_gas(30000000)] fn test_replaceability_erc20_expire_impl() { // Tests that when impl class-hash cannot be replaced to after expiration. - _expire_impl(replaceable_address: simple_deploy_l2_token()); + _expire_impl(replaceable_address: simple_deploy_token()); + } + + #[test] + #[should_panic(expected: ('IMPLEMENTATION_EXPIRED', 'ENTRYPOINT_FAILED',))] + #[available_gas(30000000)] + fn test_replaceability_lockable_expire_impl() { + // Tests that when impl class-hash cannot be replaced to after expiration. + _expire_impl(replaceable_address: simple_deploy_lockable_token()); } fn _expire_impl(replaceable_address: ContractAddress) { @@ -489,7 +557,13 @@ mod replaceability_test { #[test] #[available_gas(30000000)] fn test_replaceability_erc20_replace_to_final() { - _replace_to_final(replaceable_address: deploy_token_bridge()); + _replace_to_final(replaceable_address: simple_deploy_token()); + } + + #[test] + #[available_gas(30000000)] + fn test_replaceability_lockable_replace_to_final() { + _replace_to_final(replaceable_address: simple_deploy_lockable_token()); } fn _replace_to_final(replaceable_address: ContractAddress) { @@ -551,7 +625,14 @@ mod replaceability_test { #[should_panic(expected: ('ONLY_UPGRADE_GOVERNOR', 'ENTRYPOINT_FAILED',))] #[available_gas(30000000)] fn test_replaceability_erc20_replace_to_not_upgrade_governor() { - _replace_to_not_upgrade_governor(replaceable_address: simple_deploy_l2_token()); + _replace_to_not_upgrade_governor(replaceable_address: simple_deploy_token()); + } + + #[test] + #[should_panic(expected: ('ONLY_UPGRADE_GOVERNOR', 'ENTRYPOINT_FAILED',))] + #[available_gas(30000000)] + fn test_replaceability_lockable_replace_to_not_upgrade_governor() { + _replace_to_not_upgrade_governor(replaceable_address: simple_deploy_lockable_token()); } fn _replace_to_not_upgrade_governor(replaceable_address: ContractAddress) { @@ -575,7 +656,15 @@ mod replaceability_test { #[should_panic(expected: ('FINALIZED', 'ENTRYPOINT_FAILED',))] #[available_gas(30000000)] fn test_replaceability_erc20_replace_to_already_final() { - let replaceable_address = simple_deploy_l2_token(); + let replaceable_address = simple_deploy_token(); + _replace_to_already_final(:replaceable_address); + } + + #[test] + #[should_panic(expected: ('FINALIZED', 'ENTRYPOINT_FAILED',))] + #[available_gas(30000000)] + fn test_replaceability_lockable_replace_to_already_final() { + let replaceable_address = simple_deploy_lockable_token(); _replace_to_already_final(:replaceable_address); } @@ -610,7 +699,15 @@ mod replaceability_test { #[should_panic(expected: ('UNKNOWN_IMPLEMENTATION', 'ENTRYPOINT_FAILED',))] #[available_gas(30000000)] fn test_replaceability_erc20_unknown_implementation() { - let replaceable_address = simple_deploy_l2_token(); + let replaceable_address = simple_deploy_token(); + _replace_unknown_implementation(:replaceable_address); + } + + #[test] + #[should_panic(expected: ('UNKNOWN_IMPLEMENTATION', 'ENTRYPOINT_FAILED',))] + #[available_gas(30000000)] + fn test_replaceability_lockable_unknown_implementation() { + let replaceable_address = simple_deploy_lockable_token(); _replace_unknown_implementation(:replaceable_address); } diff --git a/src/cairo/roles_test.cairo b/src/cairo/roles_test.cairo index 38e0cc9..b4e5029 100644 --- a/src/cairo/roles_test.cairo +++ b/src/cairo/roles_test.cairo @@ -1,33 +1,32 @@ #[cfg(test)] mod roles_test { - use super::super::token_bridge::TokenBridge; - use super::super::token_bridge::TokenBridge::{ + use starknet::ContractAddress; + use src::strk::erc20_lockable::ERC20Lockable; + use src::token_bridge::TokenBridge; + use src::token_bridge::TokenBridge::{ Event, L1BridgeSet, Erc20ClassHashStored, DeployHandled, WithdrawInitiated, DepositHandled, - deposit_handled, DepositWithMessageHandled, withdraw_initiated, RoleRevoked, - RoleAdminChanged, AppRoleAdminAdded, AppRoleAdminRemoved, UpgradeGovernorAdded, - UpgradeGovernorRemoved, GovernanceAdminAdded, GovernanceAdminRemoved, AppGovernorAdded, - AppGovernorRemoved, OperatorAdded, OperatorRemoved, TokenAdminAdded, TokenAdminRemoved + deposit_handled, DepositWithMessageHandled, withdraw_initiated, AppRoleAdminAdded, + AppRoleAdminRemoved, UpgradeGovernorAdded, SecurityAdminAdded, SecurityAdminRemoved, + SecurityAgentAdded, SecurityAgentRemoved, UpgradeGovernorRemoved, GovernanceAdminAdded, + GovernanceAdminRemoved, AppGovernorAdded, AppGovernorRemoved, OperatorAdded, + OperatorRemoved, TokenAdminAdded, TokenAdminRemoved }; - use super::super::test_utils::test_utils::{ - caller, not_caller, initial_owner, permitted_minter, set_contract_address_as_caller, - get_erc20_token, deploy_l2_token, pop_and_deserialize_last_event, pop_last_k_events, - deserialize_event, arbitrary_event, assert_role_granted_event, assert_role_revoked_event, - validate_empty_event_queue, get_roles, get_access_control, deploy_token_bridge, - stock_erc20_class_hash, votes_erc20_class_hash, deploy_stub_msg_receiver, - withdraw_and_validate, deploy_upgraded_legacy_bridge, get_token_bridge, - get_token_bridge_admin, _get_daily_withdrawal_limit, disable_withdrawal_limit, - enable_withdrawal_limit, set_caller_as_app_role_admin_app_governor, default_amount, - get_default_l1_addresses, prepare_bridge_for_deploy_token, deploy_new_token, - deploy_new_token_and_deposit, DEFAULT_INITIAL_SUPPLY_HIGH, DEFAULT_L1_BRIDGE_ETH_ADDRESS, - DEFAULT_INITIAL_SUPPLY_LOW, NAME, SYMBOL, DECIMALS + use src::access_control_interface::{ + IAccessControl, IAccessControlDispatcher, IAccessControlDispatcherTrait, RoleId, + RoleAdminChanged, RoleGranted, RoleRevoked }; - use super::super::access_control_interface::{ - IAccessControlDispatcher, IAccessControlDispatcherTrait + + use src::test_utils::test_utils::{ + caller, not_caller, set_contract_address_as_caller, pop_and_deserialize_last_event, + pop_last_k_events, deserialize_event, arbitrary_event, assert_role_granted_event, + assert_role_revoked_event, validate_empty_event_queue, get_roles, get_access_control, + deploy_token_bridge, get_token_bridge, deploy_new_token, deploy_new_token_and_deposit, + simple_deploy_lockable_token }; - use super::super::roles_interface::{ - APP_GOVERNOR, APP_ROLE_ADMIN, GOVERNANCE_ADMIN, OPERATOR, TOKEN_ADMIN, UPGRADE_GOVERNOR + use src::roles_interface::{ + APP_GOVERNOR, APP_ROLE_ADMIN, GOVERNANCE_ADMIN, OPERATOR, TOKEN_ADMIN, UPGRADE_GOVERNOR, + SECURITY_ADMIN, SECURITY_AGENT, IRolesDispatcher, IRolesDispatcherTrait }; - use super::super::roles_interface::{IRolesDispatcher, IRolesDispatcherTrait}; // Validates is_app_governor function, under the assumption that register_app_role_admin and @@ -38,7 +37,7 @@ mod roles_test { // Deploy the token bridge. As part of it, the caller becomes the Governance Admin. let token_bridge_address = deploy_token_bridge(); - let token_bridge_roles = get_roles(:token_bridge_address); + let token_bridge_roles = get_roles(contract_address: token_bridge_address); let arbitrary_account = not_caller(); assert( !token_bridge_roles.is_app_governor(account: arbitrary_account), @@ -47,7 +46,7 @@ mod roles_test { // Grant the caller the App Role Admin role and then the caller grant the arbitrary_account // the App Governor role. - let token_bridge_roles = get_roles(:token_bridge_address); + let token_bridge_roles = get_roles(contract_address: token_bridge_address); token_bridge_roles.register_app_role_admin(account: caller()); token_bridge_roles.register_app_governor(account: arbitrary_account); @@ -61,7 +60,7 @@ mod roles_test { fn test_is_app_role_admin() { let token_bridge_address = deploy_token_bridge(); - let token_bridge_roles = get_roles(:token_bridge_address); + let token_bridge_roles = get_roles(contract_address: token_bridge_address); let arbitrary_account = not_caller(); assert( !token_bridge_roles.is_app_role_admin(account: arbitrary_account), @@ -69,7 +68,7 @@ mod roles_test { ); // Grant the arbitrary_account the App Role Admin role by the caller. - let token_bridge_roles = get_roles(:token_bridge_address); + let token_bridge_roles = get_roles(contract_address: token_bridge_address); token_bridge_roles.register_app_role_admin(account: arbitrary_account); assert( @@ -82,22 +81,27 @@ mod roles_test { #[test] #[available_gas(30000000)] fn test_is_governance_admin() { - let token_bridge_address = deploy_token_bridge(); + let contract_address = deploy_token_bridge(); + _test_is_governance_admin(:contract_address); + } - let token_bridge_roles = get_roles(:token_bridge_address); + #[test] + #[available_gas(30000000)] + fn test_lockable_is_governance_admin() { + let contract_address = simple_deploy_lockable_token(); + _test_is_governance_admin(:contract_address); + } + + fn _test_is_governance_admin(contract_address: ContractAddress) { + let _roles = get_roles(contract_address: contract_address); let arbitrary_account = not_caller(); - assert( - !token_bridge_roles.is_governance_admin(account: arbitrary_account), - 'Unexpected role detected' - ); + assert(!_roles.is_governance_admin(account: arbitrary_account), 'Unexpected role detected'); // Grant the arbitrary_account the Governance Admin role by the caller. - let token_bridge_roles = get_roles(:token_bridge_address); - token_bridge_roles.register_governance_admin(account: arbitrary_account); + let _roles = get_roles(contract_address: contract_address); + _roles.register_governance_admin(account: arbitrary_account); - assert( - token_bridge_roles.is_governance_admin(account: arbitrary_account), 'Role not granted' - ); + assert(_roles.is_governance_admin(account: arbitrary_account), 'Role not granted'); } // Validates is_operator_admin function, under the assumption that register_app_role_admin and @@ -107,7 +111,7 @@ mod roles_test { fn test_is_operator() { let token_bridge_address = deploy_token_bridge(); - let token_bridge_roles = get_roles(:token_bridge_address); + let token_bridge_roles = get_roles(contract_address: token_bridge_address); let arbitrary_account = not_caller(); assert( !token_bridge_roles.is_operator(account: arbitrary_account), 'Unexpected role detected' @@ -115,7 +119,7 @@ mod roles_test { // Grant the caller the App Role Admin role and then the caller grant the arbitrary_account // the Operator role. - let token_bridge_roles = get_roles(:token_bridge_address); + let token_bridge_roles = get_roles(contract_address: token_bridge_address); token_bridge_roles.register_app_role_admin(account: caller()); token_bridge_roles.register_operator(account: arbitrary_account); @@ -129,7 +133,7 @@ mod roles_test { fn test_is_token_admin() { let token_bridge_address = deploy_token_bridge(); - let token_bridge_roles = get_roles(:token_bridge_address); + let token_bridge_roles = get_roles(contract_address: token_bridge_address); let arbitrary_account = not_caller(); assert( !token_bridge_roles.is_token_admin(account: arbitrary_account), @@ -138,7 +142,7 @@ mod roles_test { // Grant the caller the App Role Admin role and then the caller grant the arbitrary_account // the Token Admin role. - let token_bridge_roles = get_roles(:token_bridge_address); + let token_bridge_roles = get_roles(contract_address: token_bridge_address); token_bridge_roles.register_app_role_admin(account: caller()); token_bridge_roles.register_token_admin(account: arbitrary_account); @@ -150,21 +154,72 @@ mod roles_test { #[test] #[available_gas(30000000)] fn test_is_upgrade_governor() { + let contract_address = deploy_token_bridge(); + _test_is_upgrade_governor(:contract_address); + } + + #[test] + #[available_gas(30000000)] + fn test_lockable_is_upgrade_governor() { + let contract_address = simple_deploy_lockable_token(); + _test_is_upgrade_governor(:contract_address); + } + + fn _test_is_upgrade_governor(contract_address: ContractAddress) { + let _roles = get_roles(contract_address: contract_address); + let arbitrary_account = not_caller(); + assert(!_roles.is_upgrade_governor(account: arbitrary_account), 'Unexpected role detected'); + + // Grant the arbitrary account the Upgrade Governor role. + let _roles = get_roles(contract_address: contract_address); + _roles.register_upgrade_governor(account: arbitrary_account); + + assert(_roles.is_upgrade_governor(account: arbitrary_account), 'Role not granted'); + } + + // Validates is_security_admin function, under the assumption that register_security_admin, + // functions as expected. + #[test] + #[available_gas(30000000)] + fn test_is_security_admin() { let token_bridge_address = deploy_token_bridge(); - let token_bridge_roles = get_roles(:token_bridge_address); + let token_bridge_roles = get_roles(contract_address: token_bridge_address); let arbitrary_account = not_caller(); assert( - !token_bridge_roles.is_upgrade_governor(account: arbitrary_account), + !token_bridge_roles.is_security_admin(account: arbitrary_account), 'Unexpected role detected' ); - // Grant the arbitrary account the Upgrade Governor role. - let token_bridge_roles = get_roles(:token_bridge_address); - token_bridge_roles.register_upgrade_governor(account: arbitrary_account); + // Grant the arbitrary account the Security Admin role. + let token_bridge_roles = get_roles(contract_address: token_bridge_address); + token_bridge_roles.register_security_admin(account: arbitrary_account); + + assert( + token_bridge_roles.is_security_admin(account: arbitrary_account), 'Role not granted' + ); + } + + // Validates is_security_agent function, under the assumption that register_security_agent, + // function as expected. + #[test] + #[available_gas(30000000)] + fn test_is_security_agent() { + let token_bridge_address = deploy_token_bridge(); + let token_bridge_roles = get_roles(contract_address: token_bridge_address); + let arbitrary_account = not_caller(); assert( - token_bridge_roles.is_upgrade_governor(account: arbitrary_account), 'Role not granted' + !token_bridge_roles.is_security_agent(account: arbitrary_account), + 'Unexpected role detected' + ); + + // Grant the arbitrary account the Security Agent role. + let token_bridge_roles = get_roles(contract_address: token_bridge_address); + token_bridge_roles.register_security_agent(account: arbitrary_account); + + assert( + token_bridge_roles.is_security_agent(account: arbitrary_account), 'Role not granted' ); } @@ -177,7 +232,7 @@ mod roles_test { // Deploy the token bridge. As part of it, the caller becomes the Governance Admin. let token_bridge_address = deploy_token_bridge(); - let token_bridge_roles = get_roles(:token_bridge_address); + let token_bridge_roles = get_roles(contract_address: token_bridge_address); let arbitrary_account = not_caller(); assert( !token_bridge_roles.is_app_governor(account: arbitrary_account), @@ -241,7 +296,7 @@ mod roles_test { fn test_register_and_remove_app_role_admin() { let token_bridge_address = deploy_token_bridge(); - let token_bridge_roles = get_roles(:token_bridge_address); + let token_bridge_roles = get_roles(contract_address: token_bridge_address); let arbitrary_account = not_caller(); assert( !token_bridge_roles.is_app_role_admin(account: arbitrary_account), @@ -301,22 +356,28 @@ mod roles_test { #[test] #[available_gas(30000000)] fn test_register_and_remove_governance_admin() { - let token_bridge_address = deploy_token_bridge(); + let contract_address = deploy_token_bridge(); + _test_register_and_remove_governance_admin(:contract_address); + } - let token_bridge_roles = get_roles(:token_bridge_address); + #[test] + #[available_gas(30000000)] + fn test_lockable_register_and_remove_governance_admin() { + let contract_address = simple_deploy_lockable_token(); + _test_register_and_remove_governance_admin(:contract_address); + } + + fn _test_register_and_remove_governance_admin(contract_address: ContractAddress) { + let _roles = get_roles(contract_address: contract_address); let arbitrary_account = not_caller(); + assert(!_roles.is_governance_admin(account: arbitrary_account), 'Unexpected role detected'); + _roles.register_governance_admin(account: arbitrary_account); assert( - !token_bridge_roles.is_governance_admin(account: arbitrary_account), - 'Unexpected role detected' - ); - token_bridge_roles.register_governance_admin(account: arbitrary_account); - assert( - token_bridge_roles.is_governance_admin(account: arbitrary_account), - 'register_governance_adm failed' + _roles.is_governance_admin(account: arbitrary_account), 'register_governance_adm failed' ); // Validate the two Governance Admin registration events. - let registration_events = pop_last_k_events(address: token_bridge_address, k: 2); + let registration_events = pop_last_k_events(address: contract_address, k: 2); assert_role_granted_event( raw_event: *registration_events.at(0), @@ -333,14 +394,14 @@ mod roles_test { 'GovAdminAdded was not emitted' ); - token_bridge_roles.remove_governance_admin(account: arbitrary_account); + _roles.remove_governance_admin(account: arbitrary_account); assert( - !token_bridge_roles.is_governance_admin(account: arbitrary_account), + !_roles.is_governance_admin(account: arbitrary_account), 'remove_governance_admin failed' ); // Validate the two Governance Admin removal events. - let removal_events = pop_last_k_events(address: token_bridge_address, k: 2); + let removal_events = pop_last_k_events(address: contract_address, k: 2); assert_role_revoked_event( raw_event: *removal_events.at(0), @@ -365,7 +426,7 @@ mod roles_test { fn test_register_and_remove_operator() { let token_bridge_address = deploy_token_bridge(); - let token_bridge_roles = get_roles(:token_bridge_address); + let token_bridge_roles = get_roles(contract_address: token_bridge_address); let arbitrary_account = not_caller(); assert( !token_bridge_roles.is_operator(account: arbitrary_account), 'Unexpected role detected' @@ -428,7 +489,7 @@ mod roles_test { fn test_register_and_remove_token_admin() { let token_bridge_address = deploy_token_bridge(); - let token_bridge_roles = get_roles(:token_bridge_address); + let token_bridge_roles = get_roles(contract_address: token_bridge_address); let arbitrary_account = not_caller(); assert( !token_bridge_roles.is_token_admin(account: arbitrary_account), @@ -491,22 +552,28 @@ mod roles_test { #[test] #[available_gas(30000000)] fn test_register_and_remove_upgrade_governor() { - let token_bridge_address = deploy_token_bridge(); + let contract_address = deploy_token_bridge(); + _test_register_and_remove_upgrade_governor(:contract_address); + } - let token_bridge_roles = get_roles(:token_bridge_address); + #[test] + #[available_gas(30000000)] + fn test_lockable_register_and_remove_upgrade_governor() { + let contract_address = simple_deploy_lockable_token(); + _test_register_and_remove_upgrade_governor(:contract_address); + } + + fn _test_register_and_remove_upgrade_governor(contract_address: ContractAddress) { + let _roles = get_roles(contract_address: contract_address); let arbitrary_account = not_caller(); + assert(!_roles.is_upgrade_governor(account: arbitrary_account), 'Unexpected role detected'); + _roles.register_upgrade_governor(account: arbitrary_account); assert( - !token_bridge_roles.is_upgrade_governor(account: arbitrary_account), - 'Unexpected role detected' - ); - token_bridge_roles.register_upgrade_governor(account: arbitrary_account); - assert( - token_bridge_roles.is_upgrade_governor(account: arbitrary_account), - 'register_upgrade_gov failed' + _roles.is_upgrade_governor(account: arbitrary_account), 'register_upgrade_gov failed' ); // Validate the two Upgrade Governor registration events. - let registration_events = pop_last_k_events(address: token_bridge_address, k: 2); + let registration_events = pop_last_k_events(address: contract_address, k: 2); assert_role_granted_event( raw_event: *registration_events.at(0), @@ -523,14 +590,14 @@ mod roles_test { 'UpgradeGovAdded was not emitted' ); - token_bridge_roles.remove_upgrade_governor(account: arbitrary_account); + _roles.remove_upgrade_governor(account: arbitrary_account); assert( - !token_bridge_roles.is_upgrade_governor(account: arbitrary_account), + !_roles.is_upgrade_governor(account: arbitrary_account), 'remove_upgrade_governor failed' ); // Validate the two Upgrade Governor removal events. - let removal_events = pop_last_k_events(address: token_bridge_address, k: 2); + let removal_events = pop_last_k_events(address: contract_address, k: 2); assert_role_revoked_event( raw_event: *removal_events.at(0), @@ -548,31 +615,159 @@ mod roles_test { ); } + // Validates register_security_admin and remove_security_admin functions under the + // assumption that is_security_admin, functions as expected. + #[test] + #[available_gas(30000000)] + fn test_register_and_remove_security_admin() { + let token_bridge_address = deploy_token_bridge(); + + let token_bridge_roles = get_roles(contract_address: token_bridge_address); + let arbitrary_account = not_caller(); + assert( + !token_bridge_roles.is_security_admin(account: arbitrary_account), + 'Unexpected role detected' + ); + token_bridge_roles.register_security_admin(account: arbitrary_account); + assert( + token_bridge_roles.is_security_admin(account: arbitrary_account), + 'register_security_admin failed' + ); + + // Validate the two Upgrade Governor registration events. + let registration_events = pop_last_k_events(address: token_bridge_address, k: 2); + + assert_role_granted_event( + raw_event: *registration_events.at(0), + role: SECURITY_ADMIN, + account: arbitrary_account, + sender: caller() + ); + + let registration_emitted_event = deserialize_event(raw_event: *registration_events.at(1)); + assert( + registration_emitted_event == SecurityAdminAdded { + added_account: arbitrary_account, added_by: caller() + }, + 'SecurAdminAdded was not emitted' + ); + + token_bridge_roles.remove_security_admin(account: arbitrary_account); + assert( + !token_bridge_roles.is_security_admin(account: arbitrary_account), + 'remove_security_admin failed' + ); + + // Validate the two Upgrade Governor removal events. + let removal_events = pop_last_k_events(address: token_bridge_address, k: 2); + + assert_role_revoked_event( + raw_event: *removal_events.at(0), + role: SECURITY_ADMIN, + account: arbitrary_account, + sender: caller() + ); + + let removal_emitted_event = deserialize_event(raw_event: *removal_events.at(1)); + assert( + removal_emitted_event == SecurityAdminRemoved { + removed_account: arbitrary_account, removed_by: caller() + }, + 'SecurAdminRemoved wasnt emitted' + ); + } + // Validates register_security_agent and remove_security_agent functions under the + // assumption that is_security_agent, functions as expected. #[test] #[available_gas(30000000)] - fn test_renounce() { - // Deploy the token bridge. As part of it, the caller becomes the Governance Admin. + fn test_register_and_remove_security_agent() { let token_bridge_address = deploy_token_bridge(); - let token_bridge_roles = get_roles(:token_bridge_address); + let token_bridge_roles = get_roles(contract_address: token_bridge_address); let arbitrary_account = not_caller(); - token_bridge_roles.register_upgrade_governor(account: arbitrary_account); assert( - token_bridge_roles.is_upgrade_governor(account: arbitrary_account), - 'register_upgrade_gov failed' + !token_bridge_roles.is_security_agent(account: arbitrary_account), + 'Unexpected role detected' + ); + token_bridge_roles.register_security_agent(account: arbitrary_account); + assert( + token_bridge_roles.is_security_agent(account: arbitrary_account), + 'register_security_agent failed' ); - starknet::testing::set_contract_address(address: arbitrary_account); - token_bridge_roles.renounce(role: UPGRADE_GOVERNOR); + // Validate the two Upgrade Governor registration events. + let registration_events = pop_last_k_events(address: token_bridge_address, k: 2); + + assert_role_granted_event( + raw_event: *registration_events.at(0), + role: SECURITY_AGENT, + account: arbitrary_account, + sender: caller() + ); + + let registration_emitted_event = deserialize_event(raw_event: *registration_events.at(1)); assert( - !token_bridge_roles.is_upgrade_governor(account: arbitrary_account), 'renounce failed' + registration_emitted_event == SecurityAgentAdded { + added_account: arbitrary_account, added_by: caller() + }, + 'SecurAgentAdded was not emitted' ); - // Validate event emission. - let role_revoked_emitted_event = pop_and_deserialize_last_event( - address: token_bridge_address + token_bridge_roles.remove_security_agent(account: arbitrary_account); + assert( + !token_bridge_roles.is_security_agent(account: arbitrary_account), + 'remove_security_agent failed' + ); + + // Validate the two Upgrade Governor removal events. + let removal_events = pop_last_k_events(address: token_bridge_address, k: 2); + + assert_role_revoked_event( + raw_event: *removal_events.at(0), + role: SECURITY_AGENT, + account: arbitrary_account, + sender: caller() ); + + let removal_emitted_event = deserialize_event(raw_event: *removal_events.at(1)); + assert( + removal_emitted_event == SecurityAgentRemoved { + removed_account: arbitrary_account, removed_by: caller() + }, + 'SecurAgentRemoved wasnt emitted' + ); + } + + #[test] + #[available_gas(30000000)] + fn test_renounce() { + // Deploy the token bridge. As part of it, the caller becomes the Governance Admin. + let contract_address = deploy_token_bridge(); + _test_renounce(:contract_address); + } + + #[test] + #[available_gas(30000000)] + fn test_lockable_renounce() { + let contract_address = simple_deploy_lockable_token(); + _test_renounce(:contract_address); + } + + fn _test_renounce(contract_address: ContractAddress) { + let _roles = get_roles(contract_address: contract_address); + let arbitrary_account = not_caller(); + _roles.register_upgrade_governor(account: arbitrary_account); + assert( + _roles.is_upgrade_governor(account: arbitrary_account), 'register_upgrade_gov failed' + ); + + starknet::testing::set_contract_address(address: arbitrary_account); + _roles.renounce(role: UPGRADE_GOVERNOR); + assert(!_roles.is_upgrade_governor(account: arbitrary_account), 'renounce failed'); + + // Validate event emission. + let role_revoked_emitted_event = pop_and_deserialize_last_event(address: contract_address); assert( role_revoked_emitted_event == Event::RoleRevoked( RoleRevoked { @@ -586,26 +781,30 @@ mod roles_test { #[test] #[available_gas(30000000)] fn test_void_renounce() { - let token_bridge_address = deploy_token_bridge(); + let contract_address = deploy_token_bridge(); + _test_void_renounce(:contract_address); + } + + #[test] + #[available_gas(30000000)] + fn test_lockable_void_renounce() { + let contract_address = simple_deploy_lockable_token(); + _test_void_renounce(:contract_address); + } - let token_bridge_roles = get_roles(:token_bridge_address); + fn _test_void_renounce(contract_address: ContractAddress) { + let _roles = get_roles(contract_address: contract_address); let arbitrary_account = not_caller(); - assert( - !token_bridge_roles.is_upgrade_governor(account: arbitrary_account), - 'Unexpected role detected' - ); + assert(!_roles.is_upgrade_governor(account: arbitrary_account), 'Unexpected role detected'); // Empty the event queue. - pop_last_k_events(address: token_bridge_address, k: 1); + pop_last_k_events(address: contract_address, k: 1); // The caller, which does not have an Upgrade Governor role, try to renounce this role. // Nothing should happen. - token_bridge_roles.renounce(role: UPGRADE_GOVERNOR); - assert( - !token_bridge_roles.is_upgrade_governor(account: arbitrary_account), - 'Unexpected role detected' - ); - validate_empty_event_queue(token_bridge_address); + _roles.renounce(role: UPGRADE_GOVERNOR); + assert(!_roles.is_upgrade_governor(account: arbitrary_account), 'Unexpected role detected'); + validate_empty_event_queue(contract_address); } #[test] @@ -613,21 +812,42 @@ mod roles_test { #[available_gas(30000000)] fn test_renounce_governance_admin() { // Deploy the token bridge. As part of it, the caller becomes the Governance Admin. - let token_bridge_address = deploy_token_bridge(); + let contract_address = deploy_token_bridge(); + _test_renounce_governance_admin(:contract_address); + } - let token_bridge_roles = get_roles(:token_bridge_address); - token_bridge_roles.renounce(role: GOVERNANCE_ADMIN); + #[test] + #[should_panic(expected: ('GOV_ADMIN_CANNOT_SELF_REMOVE', 'ENTRYPOINT_FAILED',))] + #[available_gas(30000000)] + fn test_locakable_renounce_governance_admin() { + let contract_address = simple_deploy_lockable_token(); + _test_renounce_governance_admin(:contract_address); } + fn _test_renounce_governance_admin(contract_address: ContractAddress) { + let _roles = get_roles(contract_address: contract_address); + _roles.renounce(role: GOVERNANCE_ADMIN); + } // Tests the functionality of the internal function grant_role_and_emit // which is commonly used by all role registration functions. #[test] #[available_gas(30000000)] fn test_grant_role_and_emit() { + let contract_address = deploy_token_bridge(); + _test_grant_role_and_emit(:contract_address); + } + + #[test] + #[available_gas(30000000)] + fn test_lockable_grant_role_and_emit() { + let contract_address = simple_deploy_lockable_token(); + _test_grant_role_and_emit(:contract_address); + } + + fn _test_grant_role_and_emit(contract_address: ContractAddress) { let mut token_bridge_state = TokenBridge::contract_state_for_testing(); - let token_bridge_address = deploy_token_bridge(); - let token_bridge_acess_control = get_access_control(:token_bridge_address); + let token_bridge_acess_control = get_access_control(contract_address: contract_address); // Set admin_of_arbitrary_role as an admin role of arbitrary_role and then grant the caller // the role of admin_of_arbitrary_role. @@ -636,7 +856,7 @@ mod roles_test { // Set the token bridge address to be the contract address since we are calling internal // funcitons later. - starknet::testing::set_contract_address(address: token_bridge_address); + starknet::testing::set_contract_address(address: contract_address); TokenBridge::InternalAccessControl::_set_role_admin( ref token_bridge_state, role: arbitrary_role, admin_role: admin_of_arbitrary_role ); @@ -658,7 +878,7 @@ mod roles_test { // Set the token bridge address to be the contract address since we are calling internal // functions later. - starknet::testing::set_contract_address(address: token_bridge_address); + starknet::testing::set_contract_address(address: contract_address); // The caller grant arbitrary_account the role of arbitrary_role. let role = 'DUMMY_0'; @@ -680,7 +900,7 @@ mod roles_test { ); // Validate event emission. - let arbitrary_emitted_event = pop_and_deserialize_last_event(address: token_bridge_address); + let arbitrary_emitted_event = pop_and_deserialize_last_event(address: contract_address); assert( arbitrary_emitted_event == RoleAdminChanged { role, previous_admin_role, new_admin_role @@ -691,14 +911,14 @@ mod roles_test { // Uneventful success in redundant registration. // I.e. If an account holds a role, re-registering it will not fail, but will not incur // any state change or emission of event. - starknet::testing::set_contract_address(address: token_bridge_address); + starknet::testing::set_contract_address(address: contract_address); TokenBridge::RolesInternal::_grant_role_and_emit( ref token_bridge_state, role: arbitrary_role, account: arbitrary_account, event: arbitrary_event(:role, :previous_admin_role, :new_admin_role) ); - validate_empty_event_queue(address: token_bridge_address); + validate_empty_event_queue(address: contract_address); } @@ -724,9 +944,20 @@ mod roles_test { #[test] #[available_gas(30000000)] fn test_revoke_role_and_emit() { + let contract_address = deploy_token_bridge(); + _test_revoke_role_and_emit(:contract_address); + } + + #[test] + #[available_gas(30000000)] + fn test_lockable_revoke_role_and_emit() { + let contract_address = simple_deploy_lockable_token(); + _test_revoke_role_and_emit(:contract_address); + } + + fn _test_revoke_role_and_emit(contract_address: ContractAddress) { let mut token_bridge_state = TokenBridge::contract_state_for_testing(); - let token_bridge_address = deploy_token_bridge(); - let token_bridge_acess_control = get_access_control(:token_bridge_address); + let token_bridge_acess_control = get_access_control(contract_address: contract_address); // Set admin_of_arbitrary_role as an admin role of arbitrary_role and then grant the caller // the role of admin_of_arbitrary_role. @@ -735,7 +966,7 @@ mod roles_test { // Set the token bridge address to be the contract address since we are calling internal // funcitons later. - starknet::testing::set_contract_address(address: token_bridge_address); + starknet::testing::set_contract_address(address: contract_address); TokenBridge::InternalAccessControl::_set_role_admin( ref token_bridge_state, role: arbitrary_role, admin_role: admin_of_arbitrary_role ); @@ -760,7 +991,7 @@ mod roles_test { // Set the token bridge address to be the contract address since we are calling internal // funcitons later. - starknet::testing::set_contract_address(address: token_bridge_address); + starknet::testing::set_contract_address(address: contract_address); // The caller revoke arbitrary_account the role of arbitrary_role. let role = 'DUMMY_0'; @@ -782,7 +1013,7 @@ mod roles_test { ); // Validate event emission. - let arbitrary_emitted_event = pop_and_deserialize_last_event(address: token_bridge_address); + let arbitrary_emitted_event = pop_and_deserialize_last_event(address: contract_address); assert( arbitrary_emitted_event == RoleAdminChanged { role, previous_admin_role, new_admin_role @@ -793,14 +1024,14 @@ mod roles_test { // Uneventful success in redundant removal. // I.e. If an account does not hold a role, removing the role will not fail, but will not // incur any state change or emission of event. - starknet::testing::set_contract_address(address: token_bridge_address); + starknet::testing::set_contract_address(address: contract_address); TokenBridge::RolesInternal::_revoke_role_and_emit( ref token_bridge_state, role: arbitrary_role, account: arbitrary_account, event: arbitrary_event(:role, :previous_admin_role, :new_admin_role) ); - validate_empty_event_queue(address: token_bridge_address); + validate_empty_event_queue(address: contract_address); } #[test] @@ -848,7 +1079,7 @@ mod roles_test { // deploy_token_bridge calls the constructor which calls _initialize_roles. let token_bridge_address = deploy_token_bridge(); - let token_bridge_acess_control = get_access_control(:token_bridge_address); + let token_bridge_acess_control = get_access_control(contract_address: token_bridge_address); // Validate that provisional_governance_admin is the GOVERNANCE_ADMIN. assert( @@ -883,6 +1114,29 @@ mod roles_test { ); } + #[test] + #[available_gas(30000000)] + fn test_lockable_initialize_roles() { + let contract_address = simple_deploy_lockable_token(); + + let _acess_control = get_access_control(contract_address: contract_address); + + // Validate that provisional_governance_admin is the GOVERNANCE_ADMIN. + assert( + _acess_control.has_role(role: GOVERNANCE_ADMIN, account: caller()), + 'grant_role to account failed' + ); + + assert( + _acess_control.get_role_admin(role: GOVERNANCE_ADMIN) == GOVERNANCE_ADMIN, + 'Expected GOVERNANCE_ADMIN' + ); + assert( + _acess_control.get_role_admin(role: UPGRADE_GOVERNOR) == GOVERNANCE_ADMIN, + 'Expected GOVERNANCE_ADMIN' + ); + } + #[test] #[should_panic(expected: ('ROLES_ALREADY_INITIALIZED',))] #[available_gas(30000000)] @@ -894,6 +1148,17 @@ mod roles_test { TokenBridge::RolesInternal::_initialize_roles(ref token_bridge_state); } + #[test] + #[should_panic(expected: ('ROLES_ALREADY_INITIALIZED',))] + #[available_gas(30000000)] + fn test_lockable_initialize_roles_already_set() { + let mut _state = ERC20Lockable::contract_state_for_testing(); + + starknet::testing::set_caller_address(address: not_caller()); + ERC20Lockable::RolesInternal::_initialize_roles(ref _state, caller()); + ERC20Lockable::RolesInternal::_initialize_roles(ref _state, caller()); + } + #[test] #[available_gas(30000000)] fn test_only_app_governor() { @@ -901,7 +1166,7 @@ mod roles_test { let token_bridge_address = deploy_token_bridge(); // The Governance Admin register the caller as an App Role Admin. - let token_bridge_roles = get_roles(:token_bridge_address); + let token_bridge_roles = get_roles(contract_address: token_bridge_address); token_bridge_roles.register_app_role_admin(account: caller()); let arbitrary_account = not_caller(); @@ -935,7 +1200,7 @@ mod roles_test { let token_bridge_address = deploy_token_bridge(); // The Governance Admin register the caller as an App Role Admin. - let token_bridge_roles = get_roles(:token_bridge_address); + let token_bridge_roles = get_roles(contract_address: token_bridge_address); token_bridge_roles.register_app_role_admin(account: caller()); let arbitrary_account = not_caller(); @@ -966,7 +1231,7 @@ mod roles_test { let token_bridge_address = deploy_token_bridge(); // The Governance Admin register the caller as an App Role Admin. - let token_bridge_roles = get_roles(:token_bridge_address); + let token_bridge_roles = get_roles(contract_address: token_bridge_address); token_bridge_roles.register_app_role_admin(account: caller()); let arbitrary_account = not_caller(); @@ -990,13 +1255,14 @@ mod roles_test { TokenBridge::RolesInternal::only_token_admin(@token_bridge_state); } + #[test] #[available_gas(30000000)] fn test_only_upgrade_governor() { let token_bridge_address = deploy_token_bridge(); let arbitrary_account = not_caller(); - let token_bridge_roles = get_roles(:token_bridge_address); + let token_bridge_roles = get_roles(contract_address: token_bridge_address); token_bridge_roles.register_upgrade_governor(account: arbitrary_account); let mut token_bridge_state = TokenBridge::contract_state_for_testing(); @@ -1005,6 +1271,22 @@ mod roles_test { TokenBridge::RolesInternal::only_upgrade_governor(@token_bridge_state); } + #[test] + #[available_gas(30000000)] + fn test_lockable_only_upgrade_governor() { + let contract_address = simple_deploy_lockable_token(); + + let _roles = get_roles(contract_address: contract_address); + + let arbitrary_account = not_caller(); + _roles.register_upgrade_governor(account: arbitrary_account); + + let mut _state = ERC20Lockable::contract_state_for_testing(); + starknet::testing::set_contract_address(address: contract_address); + starknet::testing::set_caller_address(address: arbitrary_account); + ERC20Lockable::RolesInternal::only_upgrade_governor(@_state); + } + #[test] #[should_panic(expected: ('ONLY_UPGRADE_GOVERNOR',))] #[available_gas(30000000)] @@ -1017,4 +1299,73 @@ mod roles_test { TokenBridge::RolesInternal::only_upgrade_governor(@token_bridge_state); } + + #[test] + #[should_panic(expected: ('ONLY_UPGRADE_GOVERNOR',))] + #[available_gas(30000000)] + fn test_lockable_only_upgrade_governor_negative() { + let contract_address = simple_deploy_lockable_token(); + + let mut _state = ERC20Lockable::contract_state_for_testing(); + starknet::testing::set_contract_address(address: contract_address); + starknet::testing::set_caller_address(address: not_caller()); + + ERC20Lockable::RolesInternal::only_upgrade_governor(@_state); + } + + #[test] + #[available_gas(30000000)] + fn test_only_security_admin() { + let token_bridge_address = deploy_token_bridge(); + + let arbitrary_account = not_caller(); + let token_bridge_roles = get_roles(contract_address: token_bridge_address); + token_bridge_roles.register_security_admin(account: arbitrary_account); + + let mut token_bridge_state = TokenBridge::contract_state_for_testing(); + starknet::testing::set_contract_address(address: token_bridge_address); + starknet::testing::set_caller_address(address: arbitrary_account); + TokenBridge::RolesInternal::only_security_admin(@token_bridge_state); + } + + #[test] + #[should_panic(expected: ('ONLY_SECURITY_ADMIN',))] + #[available_gas(30000000)] + fn test_only_security_admin_negative() { + let token_bridge_address = deploy_token_bridge(); + + let mut token_bridge_state = TokenBridge::contract_state_for_testing(); + starknet::testing::set_contract_address(address: token_bridge_address); + starknet::testing::set_caller_address(address: not_caller()); + + TokenBridge::RolesInternal::only_security_admin(@token_bridge_state); + } + + #[test] + #[available_gas(30000000)] + fn test_only_security_agent() { + let token_bridge_address = deploy_token_bridge(); + + let arbitrary_account = not_caller(); + let token_bridge_roles = get_roles(contract_address: token_bridge_address); + token_bridge_roles.register_security_agent(account: arbitrary_account); + + let mut token_bridge_state = TokenBridge::contract_state_for_testing(); + starknet::testing::set_contract_address(address: token_bridge_address); + starknet::testing::set_caller_address(address: arbitrary_account); + TokenBridge::RolesInternal::only_security_agent(@token_bridge_state); + } + + #[test] + #[should_panic(expected: ('ONLY_SECURITY_AGENT',))] + #[available_gas(30000000)] + fn test_only_security_agent_negative() { + let token_bridge_address = deploy_token_bridge(); + + let mut token_bridge_state = TokenBridge::contract_state_for_testing(); + starknet::testing::set_contract_address(address: token_bridge_address); + starknet::testing::set_caller_address(address: not_caller()); + + TokenBridge::RolesInternal::only_security_agent(@token_bridge_state); + } } diff --git a/src/cairo/strk.cairo b/src/cairo/strk.cairo new file mode 100644 index 0000000..04477c4 --- /dev/null +++ b/src/cairo/strk.cairo @@ -0,0 +1,5 @@ +mod erc20_lockable; +mod erc20_lockable_test; +mod erc20_votes_lock_test; +mod eip712helper; +mod eip712helper_test; diff --git a/src/cairo/strk/eip712helper.cairo b/src/cairo/strk/eip712helper.cairo new file mode 100644 index 0000000..da9aee0 --- /dev/null +++ b/src/cairo/strk/eip712helper.cairo @@ -0,0 +1,77 @@ +// sn_keccak('StarkNetDomain(name:felt,version:felt,chainId:felt)') +const STARKNET_DOMAIN_TYPE_HASH: felt252 = + 0x1bfc207425a47a5dfa1a50a4f5241203f50624ca5fdf5e18755765416b8e288; + +// sn_keccak('LockAndDelegateRequest(delegatee:felt,amount:felt,nonce:felt,expiry:felt)') +const LOCK_AND_DELEGATE_TYPE_HASH: felt252 = + 0x2ab9656e71e13c39f9f290cc5354d2e50a410992032118a1779539be0e4e75; + +const DAPP_NAME: felt252 = 'TOKEN_LOCK_AND_DELEGATION'; +const DAPP_VERSION: felt252 = '1.0.0'; +const STARKNET_MESSAGE: felt252 = 'StarkNet Message'; + +use starknet::{ContractAddress, get_tx_info}; +use openzeppelin::account::interface::{AccountABIDispatcher, AccountABIDispatcherTrait}; + +fn validate_signature(account: ContractAddress, hash: felt252, signature: Array) { + let is_valid_signature_felt = AccountABIDispatcher { contract_address: account } + .is_valid_signature(:hash, :signature); + + // Check either 'VALID' or True for backwards compatibility. + let is_valid_signature = is_valid_signature_felt == starknet::VALIDATED + || is_valid_signature_felt == 1; + + assert(is_valid_signature, 'SIGNATURE_VALIDATION_FAILED'); +} + +// Calculates the message hash for signing, following the SNIP equivalent of EIP-712, +// detailed in https://community.starknet.io/t/snip-off-chain-signatures-a-la-eip712/98029 +#[inline(always)] +fn lock_and_delegate_message_hash( + domain: felt252, + account: ContractAddress, + delegatee: ContractAddress, + amount: u256, + nonce: felt252, + expiry: u64, +) -> felt252 { + let mut lock_and_delegate_inputs = array![ + LOCK_AND_DELEGATE_TYPE_HASH, + delegatee.into(), + amount.low.into(), // 2**128 is good enough here, as the entire supply < 2**94. + nonce, + expiry.into() + ] + .span(); + let lock_and_delegate_hash = pedersen_hash_span(elements: lock_and_delegate_inputs); + let mut message_inputs = array![ + STARKNET_MESSAGE, domain, account.into(), lock_and_delegate_hash + ] + .span(); + pedersen_hash_span(elements: message_inputs) +} + +fn calc_domain_hash() -> felt252 { + let mut domain_state_inputs = array![ + STARKNET_DOMAIN_TYPE_HASH, DAPP_NAME, DAPP_VERSION, get_tx_info().unbox().chain_id + ] + .span(); + pedersen_hash_span(elements: domain_state_inputs) +} + +fn pedersen_hash_span(mut elements: Span) -> felt252 { + let number_of_elements = elements.len(); + assert(number_of_elements > 0, 'Requires at least one element'); + + // Pad with 0. + let mut current: felt252 = 0; + loop { + // Pop elements and apply hash. + match elements.pop_front() { + Option::Some(next) => { current = pedersen::pedersen(current, *next); }, + Option::None(()) => { break; }, + }; + }; + // Hash with number of elements. + pedersen::pedersen(current, number_of_elements.into()) +} diff --git a/src/cairo/strk/eip712helper_test.cairo b/src/cairo/strk/eip712helper_test.cairo new file mode 100644 index 0000000..80f944b --- /dev/null +++ b/src/cairo/strk/eip712helper_test.cairo @@ -0,0 +1,93 @@ +#[cfg(test)] +mod eip712helper_test { + use src::test_utils::test_utils::deploy_account; + use src::strk::eip712helper::{ + pedersen_hash_span, validate_signature, calc_domain_hash, lock_and_delegate_message_hash + }; + + const PUBLIC_KEY: felt252 = 0x3a59358373db02be1870eb01ff39d8cf76139d60bd594ef123e550262ba43ae; + const MESSAGE_HASH: felt252 = 0x1312; + const INCORRECT_MESSAGE_HASH: felt252 = 0x1313; + const SIG_R: felt252 = 0x1b1b51df737f1a26cdcdbda0a2fc16a128a82fd19ea1b4305d152aac756a6c4; + const SIG_S: felt252 = 0x460c481611160ed8b744e0bfd7e18b7982476c15d3a80cc53af8c43215b7a9f; + + #[test] + #[available_gas(30000000)] + fn test_validate_signature_valid_sig() { + let account_address = deploy_account(public_key: PUBLIC_KEY); + let sig = array![SIG_R, SIG_S,]; + validate_signature(account: account_address, hash: MESSAGE_HASH, signature: sig); + } + + #[test] + #[should_panic(expected: ('SIGNATURE_VALIDATION_FAILED',))] + #[available_gas(30000000)] + fn test_validate_signature_invalid_sig() { + let account_address = deploy_account(public_key: PUBLIC_KEY); + let sig = array![SIG_R, SIG_S,]; + validate_signature(account: account_address, hash: INCORRECT_MESSAGE_HASH, signature: sig); + } + + #[test] + #[available_gas(30000000)] + fn test_pedersen_hash_span() { + let mut input_1 = array![1, 2, 3].span(); + assert( + pedersen_hash_span( + elements: input_1 + ) == 441445179418634841919081406710178353724709968888928575445243752807295331953, + 'HASH_CHAIN_MISMATCH' + ); + + let mut input_2 = array![1, 1, 2, 3, 5, 8].span(); + assert( + pedersen_hash_span( + elements: input_2 + ) == 2383567234044941266234273954434601971633866581716422196120361961392048788157, + 'HASH_CHAIN_MISMATCH' + ); + } + + + fn validate_lock_and_delegate_hash( + chain_id: felt252, expected_domain_hash: felt252, expected_lock_hash: felt252, + ) { + let account = starknet::contract_address_const::<20>(); + let delegatee = starknet::contract_address_const::<21>(); + let amount = 200; + let nonce = 17; + let expiry = 1234; + + starknet::testing::set_chain_id(:chain_id); + assert(calc_domain_hash() == expected_domain_hash, 'DOMAIN_HASH_MISMATCH'); + assert( + lock_and_delegate_message_hash( + domain: expected_domain_hash, :account, :delegatee, :amount, :nonce, :expiry + ) == expected_lock_hash, + 'LOCK_AND_DELEGATE_HASH_MISMATCH' + ); + } + + + #[test] + #[available_gas(30000000)] + fn test_lock_and_delegate_message_hash() { + validate_lock_and_delegate_hash( + chain_id: 'SN_MAIN', + expected_domain_hash: 0x23be9c6c2dae4eb0f63f635d0299a52406da231334529560d829dfa505dd102, + expected_lock_hash: 0x1b1da5b69f289991e11c75383c0ce5c3f5c5dc6412f2ba76d3fdf1d092be046, + ); + + validate_lock_and_delegate_hash( + chain_id: 'SN_GOERLI', + expected_domain_hash: 0x7fbbf1a57a6370927e09cad58ccbfbd6b26b1cc6ee639edf8e0e36f020284bb, + expected_lock_hash: 0x700e4547ec169faac705c3f0bfdca19b12d1477ed0ce9d2f6824d541ce3c43c, + ); + + validate_lock_and_delegate_hash( + chain_id: 'SN_SEPOLIA', + expected_domain_hash: 0x2b8163ee3c860582618b34edefdc1afd0511a50fd69016eb92c7dce447fc55d, + expected_lock_hash: 0x7dcea700fe19c5c1650843c652997e692605f02c9542e1265826b8f138903b4, + ); + } +} diff --git a/src/cairo/strk/erc20_lockable.cairo b/src/cairo/strk/erc20_lockable.cairo new file mode 100644 index 0000000..6517b11 --- /dev/null +++ b/src/cairo/strk/erc20_lockable.cairo @@ -0,0 +1,794 @@ +//! 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 ERC20Lockable { + 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::strk::eip712helper::{ + calc_domain_hash, lock_and_delegate_message_hash, validate_signature + }; + use src::mintable_token_interface::{IMintableToken, IMintableTokenCamel}; + use src::mintable_lock_interface::{ + ILockAndDelegate, IMintableLock, IMintableLockDispatcher, IMintableLockDispatcherTrait, + ILockingContract + }; + 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>, + // --- Lock And Delegate --- + // Address of the contract that is used to lock & delegate on. + locking_contract: ContractAddress, + // Hashes of Lock & Delegate called by signature, to prevent replay. + recorded_locks: LegacyMap, + // EIP 712 domain separation. + domain_hash: felt252, + // --- Mintable Token --- + 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); + self.domain_hash.write(calc_domain_hash()); + } + + + #[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 + // + + // Sets the address of the locking contract. + #[external(v0)] + impl LockingContract of ILockingContract { + fn set_locking_contract(ref self: ContractState, locking_contract: ContractAddress) { + self.only_upgrade_governor(); + assert(self.locking_contract.read().is_zero(), 'LOCKING_CONTRACT_ALREADY_SET'); + assert(locking_contract.is_non_zero(), 'ZERO_ADDRESS'); + self.locking_contract.write(locking_contract); + } + + fn get_locking_contract(self: @ContractState) -> ContractAddress { + self.locking_contract.read() + } + } + + #[external(v0)] + impl LockAndDelegate of ILockAndDelegate { + fn lock_and_delegate(ref self: ContractState, delegatee: ContractAddress, amount: u256) { + let account = get_caller_address(); + self._lock_and_delegate(:account, :delegatee, :amount); + } + + fn lock_and_delegate_by_sig( + ref self: ContractState, + account: ContractAddress, + delegatee: ContractAddress, + amount: u256, + nonce: felt252, + expiry: u64, + signature: Array + ) { + assert(starknet::get_block_timestamp() <= expiry, 'SIGNATURE_EXPIRED'); + let domain = self.domain_hash.read(); + let hash = lock_and_delegate_message_hash( + :domain, :account, :delegatee, :amount, :nonce, :expiry + ); + + // Assert this signed request was not used. + let is_known_hash = self.recorded_locks.read(hash); + assert(is_known_hash == false, 'SIGNED_REQUEST_ALREADY_USED'); + + // Mark the request as used to prevent future replay. + self.recorded_locks.write(hash, true); + + validate_signature(:account, :hash, :signature); + self._lock_and_delegate(:account, :delegatee, :amount); + } + } + + #[generate_trait] + impl LockInternal of _LockInternal { + fn _lock_and_delegate( + ref self: ContractState, + account: ContractAddress, + delegatee: ContractAddress, + amount: u256 + ) { + let locking_contract = self.locking_contract.read(); + assert(locking_contract.is_non_zero(), 'LOCKING_CONTRACT_NOT_SET'); + self._increase_account_allowance(:account, spender: locking_contract, :amount); + IMintableLockDispatcher { contract_address: locking_contract } + .permissioned_lock_and_delegate(:account, :delegatee, :amount); + } + + fn _increase_account_allowance( + ref self: ContractState, + account: ContractAddress, + spender: ContractAddress, + amount: u256 + ) { + let current_allowance = self.ERC20_allowances.read((account, spender)); + // Skip, in case of allowance + amount exceed max_uint. + if current_allowance <= BoundedInt::max() - amount { + self._approve(owner: account, :spender, amount: (current_allowance + amount)); + } + } + } + + #[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/src/cairo/strk/erc20_lockable_test.cairo b/src/cairo/strk/erc20_lockable_test.cairo new file mode 100644 index 0000000..37ea10a --- /dev/null +++ b/src/cairo/strk/erc20_lockable_test.cairo @@ -0,0 +1,362 @@ +#[cfg(test)] +mod lockable_token_test { + use starknet::ContractAddress; + use integer::BoundedInt; + use serde::Serde; + use src::erc20_interface::{IERC20Dispatcher, IERC20DispatcherTrait}; + use src::test_utils::test_utils::{ + initial_owner, caller, permitted_minter, get_erc20_token, set_caller_as_upgrade_governor, + arbitrary_address, arbitrary_user, deploy_lock_and_votes_tokens, + deploy_lock_and_votes_tokens_with_owner, get_locking_contract_interface, not_caller, + deploy_account, get_erc20_votes_token, deploy_lockable_token, + get_lock_and_delegate_interface + }; + use src::mintable_lock_interface::{ + ILockingContract, ILockingContractDispatcher, ILockingContractDispatcherTrait, + ILockAndDelegate, ILockAndDelegateDispatcher, ILockAndDelegateDispatcherTrait + }; + + fn deploy_testing_lockable_token() -> ContractAddress { + let initial_owner = initial_owner(); + deploy_lockable_token(:initial_owner, initial_supply: 1000_u256) + } + use openzeppelin::governance::utils::interfaces::votes::{ + IVotesDispatcher, IVotesDispatcherTrait + }; + + fn set_locking_contract(lockable_token: ContractAddress, locking_contract: ContractAddress) { + let locking_contract_interface = get_locking_contract_interface(l2_token: lockable_token); + locking_contract_interface.set_locking_contract(:locking_contract); + } + + // Sets the caller as the upgrade governor and then set the locking contract. + fn prepare_and_set_locking_contract( + lockable_token: ContractAddress, locking_contract: ContractAddress + ) { + set_caller_as_upgrade_governor(replaceable_address: lockable_token); + set_locking_contract(:lockable_token, :locking_contract); + } + + + fn lock_and_delegate( + lockable_token: ContractAddress, delegatee: ContractAddress, amount: u256 + ) { + let lock_and_delegate_interface = get_lock_and_delegate_interface(l2_token: lockable_token); + lock_and_delegate_interface.lock_and_delegate(:delegatee, :amount); + } + + fn lock_and_delegate_by_sig( + lockable_token: ContractAddress, + account: ContractAddress, + delegatee: ContractAddress, + amount: u256, + nonce: felt252, + expiry: u64, + signature: Array + ) { + let lock_and_delegate_interface = get_lock_and_delegate_interface(l2_token: lockable_token); + lock_and_delegate_interface + .lock_and_delegate_by_sig(:account, :delegatee, :amount, :nonce, :expiry, :signature); + } + + #[test] + #[available_gas(30000000)] + fn test_deploy_lockable_token() { + deploy_testing_lockable_token(); + } + + #[test] + #[should_panic(expected: ('ONLY_UPGRADE_GOVERNOR', 'ENTRYPOINT_FAILED',))] + #[available_gas(30000000)] + fn test_failed_set_locking_contract_not_upgrade_governor() { + let lockable_token = deploy_testing_lockable_token(); + set_locking_contract(:lockable_token, locking_contract: arbitrary_address()); + } + + #[test] + #[should_panic(expected: ('ZERO_ADDRESS', 'ENTRYPOINT_FAILED',))] + #[available_gas(30000000)] + fn test_failed_set_locking_contract_zero_address() { + let lockable_token = deploy_testing_lockable_token(); + let zero_locking_contract_address = starknet::contract_address_const::<0>(); + prepare_and_set_locking_contract( + :lockable_token, locking_contract: zero_locking_contract_address + ); + } + + #[test] + #[should_panic(expected: ('LOCKING_CONTRACT_ALREADY_SET', 'ENTRYPOINT_FAILED',))] + #[available_gas(30000000)] + fn test_failed_set_locking_contract_already_set() { + let lockable_token = deploy_testing_lockable_token(); + + prepare_and_set_locking_contract(:lockable_token, locking_contract: arbitrary_address()); + let another_locking_contract_address = starknet::contract_address_const::<20>(); + set_locking_contract(:lockable_token, locking_contract: another_locking_contract_address); + } + + #[test] + #[available_gas(30000000)] + fn test_set_and_get_locking_contact() { + let lockable_token = deploy_testing_lockable_token(); + + set_caller_as_upgrade_governor(replaceable_address: lockable_token); + let locking_contract_interface = get_locking_contract_interface(l2_token: lockable_token); + locking_contract_interface.set_locking_contract(locking_contract: arbitrary_address()); + let locking_contract_result = locking_contract_interface.get_locking_contract(); + assert(locking_contract_result == arbitrary_address(), 'UNEXPECTED_LOCKING_CONTRACT'); + } + + #[test] + #[should_panic(expected: ('LOCKING_CONTRACT_NOT_SET', 'ENTRYPOINT_FAILED',))] + #[available_gas(30000000)] + fn test_failed_lock_and_delegate_not_set() { + let lockable_token = deploy_testing_lockable_token(); + let delegatee = arbitrary_user(); + lock_and_delegate(:lockable_token, :delegatee, amount: 100_u256); + } + + #[test] + #[available_gas(30000000)] + fn test_happy_flow_lock_and_delegate() { + let initial_supply = 1000_u256; + let (lockable_token, votes_lock_token) = deploy_lock_and_votes_tokens(:initial_supply); + + // Store votes_lock_token as the locking contract. + prepare_and_set_locking_contract(:lockable_token, locking_contract: votes_lock_token); + + let erc20_lockable_interface = get_erc20_token(l2_token: lockable_token); + let erc20_votes_lock_interface = get_erc20_token(l2_token: votes_lock_token); + + // Verify that the caller has balance of initial_supply for the locked token and zero + // balance of the votes token. + assert( + erc20_lockable_interface.balance_of(account: caller()) == initial_supply, + 'BAD_BALANCE_TEST_SETUP' + ); + assert( + erc20_votes_lock_interface.balance_of(account: caller()) == 0, 'BAD_BALANCE_TEST_SETUP' + ); + + let delegatee = arbitrary_user(); + lock_and_delegate(:lockable_token, :delegatee, amount: initial_supply); + + // Verify that the caller has balance of initial_supply for the votes token and zero balance + // of the locked token. + assert(erc20_lockable_interface.balance_of(account: caller()) == 0, 'UNEXPECTED_BALANCE'); + assert( + erc20_votes_lock_interface.balance_of(account: caller()) == initial_supply, + 'UNEXPECTED_BALANCE' + ); + // Verify that the votes_lock_token has balance of initial_supply for the locked token. + assert( + erc20_lockable_interface.balance_of(account: votes_lock_token) == initial_supply, + 'UNEXPECTED_BALANCE' + ); + + let erc20_votes_token_interface = get_erc20_votes_token(l2_token: votes_lock_token); + assert( + erc20_votes_token_interface.delegates(account: caller()) == delegatee, 'DELEGATE_FAILED' + ); + } + + #[test] + #[available_gas(30000000)] + #[should_panic( + expected: ( + 'u256_sub Overflow', 'ENTRYPOINT_FAILED', 'ENTRYPOINT_FAILED', 'ENTRYPOINT_FAILED', + ) + )] + fn test_lock_and_delegate_underflow() { + let initial_supply = 1000_u256; + let (lockable_token, votes_lock_token) = deploy_lock_and_votes_tokens(:initial_supply); + + // Store votes_lock_token as the locking contract. + prepare_and_set_locking_contract(:lockable_token, locking_contract: votes_lock_token); + + // Caller try to delegate more than his supply. + let delegatee = arbitrary_user(); + lock_and_delegate(:lockable_token, :delegatee, amount: initial_supply + 1); + } + + // Tests that the lock_and_delegate function can handle BoundedInt::max. + #[test] + #[available_gas(30000000)] + fn test_lock_and_delegate_max_bounded_int() { + let initial_supply = BoundedInt::max(); + let (lockable_token, votes_lock_token) = deploy_lock_and_votes_tokens(:initial_supply); + + // Store votes_lock_token as the locking contract. + prepare_and_set_locking_contract(:lockable_token, locking_contract: votes_lock_token); + + // Caller try to delegate all his balance which is BoundedInt::max. + let delegatee = arbitrary_user(); + lock_and_delegate(:lockable_token, :delegatee, amount: BoundedInt::max()); + } + + fn get_initial_supply() -> u256 { + 1000_u256 + } + + fn get_account_public_key() -> felt252 { + 0x76cee175ab9a015f17483e3ff0e0c21cbd4202b4e86519d9c1c0ae8514dd6a7 + } + + fn get_delegation_sig() -> Array { + array![ + 0x2cf06c32eb38ca5b1b0247d033bc479c6eb116fc7be0030061f40699d7b0f78, + 0x6db05d0eeac27827f777e625ca194276e86867f0e987f5669613afcdced2ba2 + ] + } + + fn get_delegatee() -> starknet::ContractAddress { + starknet::contract_address_const::<10>() + } + + fn get_expiry() -> u64 { + 123456_u64 + } + + fn get_nonce() -> felt252 { + 32 + } + + fn get_chain_id() -> felt252 { + 'SN_GOERLI' + } + + #[test] + #[available_gas(30000000)] + fn test_happy_flow_lock_and_delegate_by_sig() { + // Set chain id. + starknet::testing::set_chain_id(chain_id: get_chain_id()); + + // Account setup. + let account_address = deploy_account(public_key: get_account_public_key()); + + // Lockable token contract setup. + let (lockable_token, votes_lock_token) = deploy_lock_and_votes_tokens_with_owner( + initial_owner: account_address, initial_supply: get_initial_supply() + ); + + prepare_and_set_locking_contract(:lockable_token, locking_contract: votes_lock_token); + + // Set as not caller, to validate caller address isn't improperly used. + starknet::testing::set_caller_address(address: not_caller()); + + lock_and_delegate_by_sig( + lockable_token: lockable_token, + account: account_address, + delegatee: get_delegatee(), + amount: get_initial_supply(), + nonce: get_nonce(), + expiry: get_expiry(), + signature: get_delegation_sig() + ); + + // Validate delegation success. + let erc20_votes_token_interface = get_erc20_votes_token(l2_token: votes_lock_token); + assert( + erc20_votes_token_interface.delegates(account: account_address) == get_delegatee(), + 'DELEGATE_FAILED' + ); + } + + + #[test] + #[should_panic(expected: ('SIGNATURE_EXPIRED', 'ENTRYPOINT_FAILED',))] + #[available_gas(30000000)] + fn test_lock_and_delegate_by_sig_expired() { + starknet::testing::set_block_timestamp(get_expiry() + 1); + + // Lockable token contract setup. + let (lockable_token, votes_lock_token) = deploy_lock_and_votes_tokens( + initial_supply: get_initial_supply() + ); + + prepare_and_set_locking_contract(:lockable_token, locking_contract: votes_lock_token); + + // Invoke delegation with signature. + lock_and_delegate_by_sig( + lockable_token: lockable_token, + account: caller(), + delegatee: get_delegatee(), + amount: get_initial_supply(), + nonce: get_nonce(), + expiry: get_expiry(), + signature: get_delegation_sig() + ); + } + + #[test] + #[should_panic(expected: ('SIGNED_REQUEST_ALREADY_USED', 'ENTRYPOINT_FAILED',))] + #[available_gas(30000000)] + fn test_lock_and_delegate_by_sig_request_replay() { + // Set chain id. + starknet::testing::set_chain_id(chain_id: get_chain_id()); + + // Account setup. + let account_address = deploy_account(public_key: get_account_public_key()); + + // Lockable token contract setup. + let (lockable_token, votes_lock_token) = deploy_lock_and_votes_tokens_with_owner( + initial_owner: account_address, initial_supply: get_initial_supply() + ); + + prepare_and_set_locking_contract(:lockable_token, locking_contract: votes_lock_token); + + // Invoke delegation with signature. + lock_and_delegate_by_sig( + lockable_token: lockable_token, + account: account_address, + delegatee: get_delegatee(), + amount: get_initial_supply(), + nonce: get_nonce(), + expiry: 123456, + signature: get_delegation_sig() + ); + + // Invoke delegation with signature again. + lock_and_delegate_by_sig( + lockable_token: lockable_token, + account: account_address, + delegatee: get_delegatee(), + amount: get_initial_supply(), + nonce: get_nonce(), + expiry: get_expiry(), + signature: get_delegation_sig() + ); + } + + #[test] + #[should_panic(expected: ('SIGNATURE_VALIDATION_FAILED', 'ENTRYPOINT_FAILED',))] + #[available_gas(30000000)] + fn test_lock_and_delegate_by_sig_invalid_sig() { + // Set chain id. + starknet::testing::set_chain_id(chain_id: get_chain_id()); + + // Account setup. + let account_address = deploy_account(public_key: get_account_public_key()); + + // Lockable token contract setup. + let (lockable_token, votes_lock_token) = deploy_lock_and_votes_tokens_with_owner( + initial_owner: account_address, initial_supply: get_initial_supply() + ); + + prepare_and_set_locking_contract(:lockable_token, locking_contract: votes_lock_token); + + // Set as not caller, to validate caller address isn't improperly used. + starknet::testing::set_caller_address(address: not_caller()); + + // Invoke delegation with signature with modified data that invalidates the signature. + lock_and_delegate_by_sig( + lockable_token: lockable_token, + account: account_address, + delegatee: get_delegatee(), + amount: get_initial_supply(), + nonce: get_nonce() + 1, + expiry: get_expiry(), + signature: get_delegation_sig() + ); + } +} diff --git a/src/cairo/strk/erc20_votes_lock_test.cairo b/src/cairo/strk/erc20_votes_lock_test.cairo new file mode 100644 index 0000000..607a12a --- /dev/null +++ b/src/cairo/strk/erc20_votes_lock_test.cairo @@ -0,0 +1,614 @@ +#[cfg(test)] +mod lockable_token_test { + use src::mintable_lock_interface::ITokenLockDispatcherTrait; + use starknet::{ContractAddress, get_contract_address}; + use src::test_utils::test_utils::{ + deploy_lockable_token, initial_owner, get_erc20_token, caller, deploy_votes_lock, + get_token_lock_interface, pop_and_deserialize_last_event, not_caller, + set_contract_address_as_not_caller, set_contract_address_as_caller, + get_mintable_lock_interface, get_erc20_votes_token, arbitrary_user, + deploy_lock_and_votes_tokens + }; + use src::erc20_interface::{IERC20Dispatcher, IERC20DispatcherTrait}; + use src::mintable_lock_interface::{ + ILockingContract, ILockingContractDispatcher, ILockingContractDispatcherTrait, + ILockAndDelegate, ILockAndDelegateDispatcher, ILockAndDelegateDispatcherTrait, Locked, + Unlocked, IMintableLockDispatcher, IMintableLockDispatcherTrait + }; + use openzeppelin::governance::utils::interfaces::votes::{ + IVotesDispatcher, IVotesDispatcherTrait + }; + use openzeppelin::token::erc20::presets::erc20_votes_lock::ERC20VotesLock::{Event}; + use starknet::testing::set_contract_address; + use starknet::contract_address_const; + + + #[derive(Copy, Drop, PartialEq)] + enum VotesTokenFunction { + Lock, + Unlock, + LockAndDelegate, + } + + + fn _erc20_votes_lock() -> ContractAddress { + let initial_owner = starknet::contract_address_const::<10>(); + let locked_token = starknet::contract_address_const::<20>(); + deploy_votes_lock(:locked_token) + } + + + // Verifies that the difference between after and before is as expected. + fn check_change_post_action( + after: u256, before: u256, expected_diff: u256, after_is_greater: bool, err_code: felt252 + ) { + if after_is_greater { + assert(after - before == expected_diff, :err_code); + } else { + assert(before - after == expected_diff, :err_code); + } + } + + + fn lock_and_verify_total_supply_and_balance( + votes_lock_token: ContractAddress, lockable_token: ContractAddress, amount: u256 + ) { + apply_action_and_verify( + :votes_lock_token, + :lockable_token, + :amount, + action: VotesTokenFunction::Lock, + delegatee: Option::None + ); + } + + fn lock_delegate_and_verify_total_supply_and_balance( + votes_lock_token: ContractAddress, + lockable_token: ContractAddress, + amount: u256, + delegatee: ContractAddress + ) { + apply_action_and_verify( + :votes_lock_token, + :lockable_token, + :amount, + action: VotesTokenFunction::LockAndDelegate, + delegatee: Option::Some(delegatee), + ); + } + + fn unlock_and_verify_total_supply_and_balance( + votes_lock_token: ContractAddress, lockable_token: ContractAddress, amount: u256 + ) { + apply_action_and_verify( + :votes_lock_token, + :lockable_token, + :amount, + action: VotesTokenFunction::Unlock, + delegatee: Option::None + ); + } + + fn votes_token_action( + votes_lock_token: ContractAddress, + lockable_token: ContractAddress, + amount: u256, + action: VotesTokenFunction, + delegatee: Option, + ) { + let token_lock_interface = get_token_lock_interface(l2_token: votes_lock_token); + match action { + // Lock the token and verify that the event was emitted. + VotesTokenFunction::Lock => { + token_lock_interface.lock(:amount); + let emitted_event = pop_and_deserialize_last_event(address: votes_lock_token); + assert( + emitted_event == Event::Locked( + Locked { account: get_contract_address(), amount } + ), + 'LOCK_ERROR' + ); + }, + VotesTokenFunction::Unlock => { + // Unlock the token and verify that the event was emitted. + token_lock_interface.unlock(:amount); + let emitted_event = pop_and_deserialize_last_event(address: votes_lock_token); + assert( + emitted_event == Event::Unlocked( + Unlocked { account: get_contract_address(), amount } + ), + 'UNLOCK_ERROR' + ); + }, + VotesTokenFunction::LockAndDelegate => { + let delegate_account = delegatee.unwrap(); + // Lock and delegate from caller to delegatee. + let mintable_lock_interface = get_mintable_lock_interface( + l2_token: votes_lock_token + ); + set_contract_address(address: lockable_token); + mintable_lock_interface + .permissioned_lock_and_delegate( + account: caller(), delegatee: delegate_account, amount: amount + ); + set_contract_address(address: caller()); + } + } + } + + // Handles lock, lock_and_delegate and unlock. For all three cases, the function verifies that + // the events and that the balances and the supply are updated as expected. + fn apply_action_and_verify( + votes_lock_token: ContractAddress, + lockable_token: ContractAddress, + amount: u256, + action: VotesTokenFunction, + delegatee: Option, + ) { + // Get the erc20 interface for both tokens. + let erc20_lockable_interface = get_erc20_token(l2_token: lockable_token); + let erc20_votes_lock_interface = get_erc20_token(l2_token: votes_lock_token); + + let lockable_balance_before = erc20_lockable_interface + .balance_of(account: get_contract_address()); + let lockable_supply_before = erc20_lockable_interface.total_supply(); + + let votes_balance_before = erc20_votes_lock_interface + .balance_of(account: get_contract_address()); + let votes_supply_before = erc20_votes_lock_interface.total_supply(); + + let lockable_balance_of_votes_token_before = erc20_lockable_interface + .balance_of(account: votes_lock_token); + + votes_token_action(:votes_lock_token, :lockable_token, :amount, :action, :delegatee); + + // Store if the action is unlock or not. + let is_unlock = (action == VotesTokenFunction::Unlock); + + // Verify that the total supply of the lockable token was not changed. + check_change_post_action( + after: erc20_lockable_interface.total_supply(), + before: lockable_supply_before, + expected_diff: 0, + after_is_greater: !is_unlock, + err_code: 'LOCKABLE_SUPPLY_SHOULDNT_CHANGE' + ); + + // Verify that the total supply of the votes token increased/decreased by amount, when + // locking/unlocking accordingly. + check_change_post_action( + after: erc20_votes_lock_interface.total_supply(), + before: votes_supply_before, + expected_diff: amount, + after_is_greater: !is_unlock, + err_code: 'BAD_AMOUNT_OF_MINTED_TOKENS' + ); + + // Verify that the current contract address' balance of the lockable token was + // decreased/increased by amount, when locking/unlocking accordingly. + // NOTE: since in lock, the balance is decreased after_is_greater equals to unlock. + check_change_post_action( + after: erc20_lockable_interface.balance_of(account: get_contract_address()), + before: lockable_balance_before, + expected_diff: amount, + after_is_greater: is_unlock, + err_code: 'BAD_LOCKABLE_BALANCE' + ); + + // Verify that the current contract address' balance of the votes token was + // increased/decreased by amount, when locking/unlocking accordingly. + check_change_post_action( + after: erc20_votes_lock_interface.balance_of(account: get_contract_address()), + before: votes_balance_before, + expected_diff: amount, + after_is_greater: !is_unlock, + err_code: 'BAD_VOTES_BALANCE' + ); + + // Verify that votes_lock_token balance of the lockable token was increased/decreased by + // amount, when locking/unlocking accordingly. + check_change_post_action( + after: erc20_lockable_interface.balance_of(account: votes_lock_token), + before: lockable_balance_of_votes_token_before, + expected_diff: amount, + after_is_greater: !is_unlock, + err_code: 'VOTES_WRONG_BALANCE_OF_LOCKABLE' + ); + } + + fn increase_allowance( + erc20_token: ContractAddress, spender: ContractAddress, added_value: u256 + ) { + let erc20_lockable_interface = get_erc20_token(l2_token: erc20_token); + erc20_lockable_interface.increase_allowance(:spender, :added_value); + } + + fn transfer(erc20_token: ContractAddress, recipient: ContractAddress, amount: u256) { + let erc20_token_interface = get_erc20_token(l2_token: erc20_token); + erc20_token_interface.transfer(:recipient, :amount); + } + + fn assert_voting_power( + votes_lock_token: ContractAddress, account: ContractAddress, expected_amount: u256 + ) { + let erc20_votes_token_interface = get_erc20_votes_token(l2_token: votes_lock_token); + assert( + erc20_votes_token_interface.get_votes(:account) == expected_amount, + 'VOTES_ERROR_OF_DELEGATEE' + ); + } + + fn account_delgate_to_himself(votes_lock_token: ContractAddress, account: ContractAddress) { + let orig = get_contract_address(); + + set_contract_address(address: account); + let erc20_votes_token_interface = get_erc20_votes_token(l2_token: votes_lock_token); + erc20_votes_token_interface.delegate(delegatee: account); + + set_contract_address(address: orig); + } + + + #[test] + #[available_gas(30000000)] + fn test_deploy_votes_lock_token() { + _erc20_votes_lock(); + } + + #[test] + #[available_gas(30000000)] + fn test_happy_flow_votes_lock() { + let (lockable_token, votes_lock_token) = deploy_lock_and_votes_tokens( + initial_supply: 1000_u256 + ); + + let locked_amount = 100_u256; + increase_allowance( + erc20_token: lockable_token, spender: votes_lock_token, added_value: locked_amount + ); + lock_and_verify_total_supply_and_balance( + :votes_lock_token, :lockable_token, amount: locked_amount + ); + } + + // This test is very similar to test_happy_flow_votes_lock. It locks the same amount of tokens + // but in two consecutive locks. + #[test] + #[available_gas(30000000)] + fn test_happy_flow_votes_two_locks() { + let (lockable_token, votes_lock_token) = deploy_lock_and_votes_tokens( + initial_supply: 1000_u256 + ); + + let locked_amount = 100_u256; + increase_allowance( + erc20_token: lockable_token, spender: votes_lock_token, added_value: locked_amount + ); + lock_and_verify_total_supply_and_balance( + :votes_lock_token, :lockable_token, amount: locked_amount / 2 + ); + lock_and_verify_total_supply_and_balance( + :votes_lock_token, :lockable_token, amount: locked_amount / 2 + ); + } + + + #[test] + #[available_gas(30000000)] + #[should_panic(expected: ('u256_sub Overflow', 'ENTRYPOINT_FAILED', 'ENTRYPOINT_FAILED',))] + fn test_not_enough_allowance_votes_lock() { + let (lockable_token, votes_lock_token) = deploy_lock_and_votes_tokens( + initial_supply: 1000_u256 + ); + + let locked_amount = 100_u256; + increase_allowance( + erc20_token: lockable_token, spender: votes_lock_token, added_value: locked_amount + ); + + lock_and_verify_total_supply_and_balance( + :votes_lock_token, :lockable_token, amount: locked_amount + 1 + ); + } + + + // Tests where there are two consecutive locks. The first one should succeed and the second one + // should fail. + #[test] + #[available_gas(30000000)] + #[should_panic(expected: ('u256_sub Overflow', 'ENTRYPOINT_FAILED', 'ENTRYPOINT_FAILED',))] + fn test_not_enough_allowance_votes_two_locks() { + let (lockable_token, votes_lock_token) = deploy_lock_and_votes_tokens( + initial_supply: 1000_u256 + ); + + let locked_amount = 100_u256; + increase_allowance( + erc20_token: lockable_token, spender: votes_lock_token, added_value: locked_amount + ); + + lock_and_verify_total_supply_and_balance( + :votes_lock_token, :lockable_token, amount: locked_amount / 2 + ); + lock_and_verify_total_supply_and_balance( + :votes_lock_token, :lockable_token, amount: locked_amount / 2 + 1 + ); + } + + #[test] + #[available_gas(30000000)] + fn test_happy_flow_votes_unlock() { + let (lockable_token, votes_lock_token) = deploy_lock_and_votes_tokens( + initial_supply: 1000_u256 + ); + + let locked_amount = 1000_u256; + increase_allowance( + erc20_token: lockable_token, spender: votes_lock_token, added_value: locked_amount + ); + lock_and_verify_total_supply_and_balance( + :votes_lock_token, :lockable_token, amount: locked_amount + ); + + unlock_and_verify_total_supply_and_balance( + :votes_lock_token, :lockable_token, amount: locked_amount + ); + } + + // This test is very similar to test_happy_flow_votes_unlock. It unlocks the same amount of + // tokens but in two consecutive unlocks. + #[test] + #[available_gas(30000000)] + fn test_happy_flow_votes_two_unlocks() { + let (lockable_token, votes_lock_token) = deploy_lock_and_votes_tokens( + initial_supply: 1000_u256 + ); + + let locked_amount = 100_u256; + increase_allowance( + erc20_token: lockable_token, spender: votes_lock_token, added_value: locked_amount + ); + lock_and_verify_total_supply_and_balance( + :votes_lock_token, :lockable_token, amount: locked_amount + ); + + unlock_and_verify_total_supply_and_balance( + :votes_lock_token, :lockable_token, amount: locked_amount / 2 + ); + unlock_and_verify_total_supply_and_balance( + :votes_lock_token, :lockable_token, amount: locked_amount / 2 + ); + } + + #[test] + #[available_gas(30000000)] + #[should_panic(expected: ('u256_sub Overflow', 'ENTRYPOINT_FAILED',))] + fn test_unlock_more_than_locked() { + let (lockable_token, votes_lock_token) = deploy_lock_and_votes_tokens( + initial_supply: 1000_u256 + ); + let token_lock_interface = get_token_lock_interface(l2_token: votes_lock_token); + let erc20_lockable_interface = get_erc20_token(l2_token: lockable_token); + let erc20_votes_lock_interface = get_erc20_token(l2_token: votes_lock_token); + + let locked_amount = 1000_u256; + increase_allowance( + erc20_token: lockable_token, spender: votes_lock_token, added_value: locked_amount + ); + lock_and_verify_total_supply_and_balance( + :votes_lock_token, :lockable_token, amount: locked_amount + ); + unlock_and_verify_total_supply_and_balance( + :votes_lock_token, :lockable_token, amount: locked_amount + 1 + ); + } + + #[test] + #[available_gas(30000000)] + #[should_panic(expected: ('u256_sub Overflow', 'ENTRYPOINT_FAILED',))] + fn test_over_unlock_in_two_parts() { + let (lockable_token, votes_lock_token) = deploy_lock_and_votes_tokens( + initial_supply: 1000_u256 + ); + + let locked_amount = 100_u256; + increase_allowance( + erc20_token: lockable_token, spender: votes_lock_token, added_value: locked_amount + ); + lock_and_verify_total_supply_and_balance( + :votes_lock_token, :lockable_token, amount: locked_amount + ); + unlock_and_verify_total_supply_and_balance( + :votes_lock_token, :lockable_token, amount: locked_amount / 2 + ); + unlock_and_verify_total_supply_and_balance( + :votes_lock_token, :lockable_token, amount: locked_amount / 2 + 1 + ); + } + + #[test] + #[available_gas(30000000)] + fn test_happy_flow_votes_lock_unlock_two_accounts() { + let (lockable_token, votes_lock_token) = deploy_lock_and_votes_tokens( + initial_supply: 1000_u256 + ); + + let locked_amount = 1000_u256; + let funds_of_first_account = 100_u256; + let funds_of_second_account = locked_amount - funds_of_first_account; + transfer( + erc20_token: lockable_token, recipient: not_caller(), amount: funds_of_second_account + ); + + increase_allowance( + erc20_token: lockable_token, + spender: votes_lock_token, + added_value: funds_of_first_account + ); + + lock_and_verify_total_supply_and_balance( + :votes_lock_token, :lockable_token, amount: funds_of_first_account + ); + + set_contract_address_as_not_caller(); + increase_allowance( + erc20_token: lockable_token, + spender: votes_lock_token, + added_value: funds_of_second_account + ); + lock_and_verify_total_supply_and_balance( + :votes_lock_token, :lockable_token, amount: funds_of_second_account + ); + unlock_and_verify_total_supply_and_balance( + :votes_lock_token, :lockable_token, amount: funds_of_second_account + ); + + set_contract_address_as_caller(); + unlock_and_verify_total_supply_and_balance( + :votes_lock_token, :lockable_token, amount: funds_of_first_account + ); + } + + + // Flow of the test: + // 1. User A locks lockable (`votes_lock_token` are minted) + // 2. User A transfer `votes_lock_token` to user B. + // 3. User B unlock (`votes_lock_token` are burned). + #[test] + #[available_gas(30000000)] + fn test_happy_flow_lock_transfer_and_unlock() { + let (lockable_token, votes_lock_token) = deploy_lock_and_votes_tokens( + initial_supply: 1000_u256 + ); + let locked_amount = 100_u256; + increase_allowance( + erc20_token: lockable_token, spender: votes_lock_token, added_value: locked_amount + ); + lock_and_verify_total_supply_and_balance( + :votes_lock_token, :lockable_token, amount: locked_amount + ); + transfer(erc20_token: votes_lock_token, recipient: not_caller(), amount: locked_amount); + set_contract_address_as_not_caller(); + unlock_and_verify_total_supply_and_balance( + :votes_lock_token, :lockable_token, amount: locked_amount + ); + } + + + #[test] + #[available_gas(30000000)] + fn test_happy_flow_permissioned_lock_and_delegate() { + let (lockable_token, votes_lock_token) = deploy_lock_and_votes_tokens( + initial_supply: 1000_u256 + ); + + let delegatee = arbitrary_user(); + let locked_amount = 100_u256; + increase_allowance( + erc20_token: lockable_token, spender: votes_lock_token, added_value: locked_amount + ); + // Caller lock and delegate to delgatee. + lock_delegate_and_verify_total_supply_and_balance( + :votes_lock_token, :lockable_token, amount: locked_amount, :delegatee + ); + + let erc20_votes_lock_interface = get_erc20_token(l2_token: votes_lock_token); + let erc20_votes_token_interface = get_erc20_votes_token(l2_token: votes_lock_token); + + let delegatee = arbitrary_user(); + assert( + erc20_votes_lock_interface.balance_of(account: delegatee) == 0, + 'ERROR_VOTES_TOKEN_BAL_NO_CALLER' + ); + + assert( + erc20_votes_token_interface.delegates(account: caller()) == delegatee, + 'UNEXPECTED_DELEGATEE' + ); + assert( + erc20_votes_token_interface.get_votes(account: delegatee) == locked_amount, + 'VOTES_ERROR_AFTER_LOCK_N_DELEGA' + ); + } + + #[test] + #[available_gas(30000000)] + #[should_panic(expected: ('INVALID_CALLER', 'ENTRYPOINT_FAILED',))] + fn test_invalid_caller_permissioned_lock_and_delegate() { + let (lockable_token, votes_lock_token) = deploy_lock_and_votes_tokens( + initial_supply: 1000_u256 + ); + + // The caller to permissioned_lock_and_delegate should be the lockable token. Since this + // isn't the case, the call should fail. + let not_lockable_contract = contract_address_const::<987>(); + set_contract_address(address: not_lockable_contract); + let mintable_lock_interface = get_mintable_lock_interface(l2_token: votes_lock_token); + mintable_lock_interface + .permissioned_lock_and_delegate(account: caller(), delegatee: not_caller(), amount: 1); + } + + // Flow of the test: + // 1. User A locks `first_locked_amount` of `lockable_token`. + // 2. User A transfer `votes_lock_token` to user B. + // 3. User B delegates to himself (voting power accordingly). + // 4. User A performs lock_and_delegate of anohter amounts (voting power increased). + #[test] + #[available_gas(30000000)] + fn test_happy_flow_transfer_and_permissioned_and_delegate() { + let (lockable_token, votes_lock_token) = deploy_lock_and_votes_tokens( + initial_supply: 1000_u256 + ); + + let delegatee = arbitrary_user(); + let first_locked_amount = 100_u256; + increase_allowance( + erc20_token: lockable_token, spender: votes_lock_token, added_value: first_locked_amount + ); + lock_and_verify_total_supply_and_balance( + :votes_lock_token, :lockable_token, amount: first_locked_amount + ); + + transfer(erc20_token: votes_lock_token, recipient: delegatee, amount: first_locked_amount); + + assert_voting_power(:votes_lock_token, account: delegatee, expected_amount: 0); + account_delgate_to_himself(:votes_lock_token, account: delegatee); + assert_voting_power( + :votes_lock_token, account: delegatee, expected_amount: first_locked_amount + ); + + let second_locked_amount = 200_u256; + increase_allowance( + erc20_token: lockable_token, + spender: votes_lock_token, + added_value: second_locked_amount + ); + // Caller lock and delegate to delgatee. + lock_delegate_and_verify_total_supply_and_balance( + :votes_lock_token, :lockable_token, amount: second_locked_amount, :delegatee + ); + assert_voting_power( + :votes_lock_token, + account: delegatee, + expected_amount: first_locked_amount + second_locked_amount + ); + } + + #[test] + #[available_gas(30000000)] + #[should_panic(expected: ('u256_sub Overflow', 'ENTRYPOINT_FAILED', 'ENTRYPOINT_FAILED',))] + fn test_overdraft_permissioned_lock_and_delegate() { + let (lockable_token, votes_lock_token) = deploy_lock_and_votes_tokens( + initial_supply: 1000_u256 + ); + + let delegatee = arbitrary_user(); + let locked_amount = 100_u256; + increase_allowance( + erc20_token: lockable_token, spender: votes_lock_token, added_value: locked_amount + ); + lock_delegate_and_verify_total_supply_and_balance( + :votes_lock_token, :lockable_token, amount: locked_amount + 1, :delegatee + ); + } +} diff --git a/src/cairo/strk/lib.cairo b/src/cairo/strk/lib.cairo new file mode 100644 index 0000000..9bd15d2 --- /dev/null +++ b/src/cairo/strk/lib.cairo @@ -0,0 +1,7 @@ +// STRK Token (ERC20Lockable). +// Modules. +mod erc20_lockable; + +// Tests. +mod erc20_lockable_test; +mod erc20_votes_lockable_test; diff --git a/src/cairo/test_utils.cairo b/src/cairo/test_utils.cairo index 8fe8b5d..62e6839 100644 --- a/src/cairo/test_utils.cairo +++ b/src/cairo/test_utils.cairo @@ -1,3 +1,68 @@ +// A lean dummy account that implements `is_valid_signature`. +#[starknet::interface] +trait IsValidSignature { + fn is_valid_signature(self: @TState, hash: felt252, signature: Array) -> felt252; +} + +#[starknet::contract] +mod TestAccount { + use array::ArrayTrait; + use array::SpanTrait; + use ecdsa::check_ecdsa_signature; + + #[storage] + struct Storage { + public_key: felt252 + } + + #[constructor] + fn constructor(ref self: ContractState, _public_key: felt252) { + self.public_key.write(_public_key); + } + + // + // External + // + + #[external(v0)] + impl IsValidSignatureImpl of super::IsValidSignature { + fn is_valid_signature( + self: @ContractState, hash: felt252, signature: Array + ) -> felt252 { + if self._is_valid_signature(hash, signature.span()) { + starknet::VALIDATED + } else { + 0 + } + } + } + + // + // Internal + // + + #[generate_trait] + impl InternalImpl of InternalTrait { + fn _is_valid_signature( + self: @ContractState, hash: felt252, signature: Span + ) -> bool { + let valid_length = signature.len() == 2_u32; + + if valid_length { + check_ecdsa_signature( + message_hash: hash, + public_key: self.public_key.read(), + signature_r: *signature.at(0_u32), + signature_s: *signature.at(1_u32) + ) + } else { + false + } + } + } +} + + #[cfg(test)] mod test_utils { use array::ArrayTrait; @@ -7,38 +72,48 @@ mod test_utils { use serde::Serde; use starknet::{ContractAddress, EthAddress, syscalls::deploy_syscall, get_contract_address}; use starknet::class_hash::{ClassHash, Felt252TryIntoClassHash}; - use openzeppelin::token::erc20::presets::erc20votes::ERC20VotesPreset; + use openzeppelin::token::erc20::presets::erc20_votes_lock::ERC20VotesLock; use openzeppelin::token::erc20_v070::erc20::ERC20; + use src::strk::erc20_lockable::ERC20Lockable; - use super::super::mintable_token_interface::{ - IMintableTokenDispatcher, IMintableTokenDispatcherTrait + use openzeppelin::governance::utils::interfaces::votes::{ + IVotesDispatcher, IVotesDispatcherTrait }; - use super::super::erc20_interface::{IERC20Dispatcher, IERC20DispatcherTrait}; + use src::mintable_token_interface::{IMintableTokenDispatcher, IMintableTokenDispatcherTrait}; + use src::erc20_interface::{IERC20Dispatcher, IERC20DispatcherTrait}; use array::SpanTrait; - use super::super::token_bridge::TokenBridge; - use super::super::token_bridge::TokenBridge::{ + use src::token_bridge::TokenBridge; + use src::token_bridge::TokenBridge::{ Event, WithdrawalLimitDisabled, WithdrawalLimitEnabled, WithdrawInitiated }; - use super::super::token_test_setup::TokenTestSetup; - use super::super::token_test_setup_interface::{ + use src::token_test_setup::TokenTestSetup; + use src::token_test_setup_interface::{ ITokenTestSetupDispatcher, ITokenTestSetupDispatcherTrait }; - use super::super::stub_msg_receiver::StubMsgReceiver; + use src::stub_msg_receiver::StubMsgReceiver; - use super::super::token_bridge_interface::{ITokenBridgeDispatcher, ITokenBridgeDispatcherTrait}; - use super::super::token_bridge_admin_interface::{ + use src::token_bridge_interface::{ITokenBridgeDispatcher, ITokenBridgeDispatcherTrait}; + use src::token_bridge_admin_interface::{ ITokenBridgeAdminDispatcher, ITokenBridgeAdminDispatcherTrait }; - use super::super::replaceability_interface::{ + use src::replaceability_interface::{ IReplaceable, IReplaceableDispatcher, IReplaceableDispatcherTrait }; - use super::super::roles_interface::{IRolesDispatcher, IRolesDispatcherTrait}; - use super::super::access_control_interface::{ + use src::roles_interface::{IRolesDispatcher, IRolesDispatcherTrait}; + use src::access_control_interface::{ IAccessControlDispatcher, IAccessControlDispatcherTrait, RoleId, RoleAdminChanged, RoleGranted, RoleRevoked, }; + use super::super::mintable_lock_interface::{ + ILockAndDelegateDispatcher, ILockAndDelegateDispatcherTrait, ILockingContractDispatcher, + ILockingContractDispatcherTrait, ITokenLock, ITokenLockDispatcher, + ITokenLockDispatcherTrait, IMintableLock, IMintableLockDispatcher, + IMintableLockDispatcherTrait + }; + + use super::TestAccount; const DEFAULT_UPGRADE_DELAY: u64 = 12345; @@ -65,16 +140,16 @@ mod test_utils { } - fn get_roles(token_bridge_address: ContractAddress) -> IRolesDispatcher { - IRolesDispatcher { contract_address: token_bridge_address } + fn get_roles(contract_address: ContractAddress) -> IRolesDispatcher { + IRolesDispatcher { contract_address: contract_address } } fn get_replaceable(replaceable_address: ContractAddress) -> IReplaceableDispatcher { IReplaceableDispatcher { contract_address: replaceable_address } } - fn get_access_control(token_bridge_address: ContractAddress) -> IAccessControlDispatcher { - IAccessControlDispatcher { contract_address: token_bridge_address } + fn get_access_control(contract_address: ContractAddress) -> IAccessControlDispatcher { + IAccessControlDispatcher { contract_address: contract_address } } @@ -82,6 +157,35 @@ mod test_utils { IERC20Dispatcher { contract_address: l2_token } } + fn get_erc20_votes_token(l2_token: ContractAddress) -> IVotesDispatcher { + IVotesDispatcher { contract_address: l2_token } + } + + fn get_lock_and_delegate_interface(l2_token: ContractAddress) -> ILockAndDelegateDispatcher { + ILockAndDelegateDispatcher { contract_address: l2_token } + } + + fn get_locking_contract_interface(l2_token: ContractAddress) -> ILockingContractDispatcher { + ILockingContractDispatcher { contract_address: l2_token } + } + + fn get_token_lock_interface(l2_token: ContractAddress) -> ITokenLockDispatcher { + ITokenLockDispatcher { contract_address: l2_token } + } + + fn get_mintable_lock_interface(l2_token: ContractAddress) -> IMintableLockDispatcher { + IMintableLockDispatcher { contract_address: l2_token } + } + + fn arbitrary_address() -> ContractAddress { + starknet::contract_address_const::<3563>() + } + + + fn arbitrary_user() -> ContractAddress { + starknet::contract_address_const::<7171>() + } + fn caller() -> ContractAddress { starknet::contract_address_const::<15>() } @@ -103,14 +207,12 @@ mod test_utils { fn set_contract_address_as_caller() { - let caller_address = caller(); - starknet::testing::set_contract_address(address: caller_address); + starknet::testing::set_contract_address(address: caller()); } fn set_contract_address_as_not_caller() { - let not_caller_address = not_caller(); - starknet::testing::set_contract_address(address: not_caller_address); + starknet::testing::set_contract_address(address: not_caller()); } @@ -125,13 +227,16 @@ mod test_utils { u256 { low: DEFAULT_INITIAL_SUPPLY_LOW, high: DEFAULT_INITIAL_SUPPLY_HIGH } } + fn lockable_erc20_class_hash() -> ClassHash { + ERC20Lockable::TEST_CLASS_HASH.try_into().unwrap() + } fn stock_erc20_class_hash() -> ClassHash { ERC20::TEST_CLASS_HASH.try_into().unwrap() } - fn votes_erc20_class_hash() -> ClassHash { - ERC20VotesPreset::TEST_CLASS_HASH.try_into().unwrap() + fn erc20_votes_lock_class_hash() -> ClassHash { + ERC20VotesLock::TEST_CLASS_HASH.try_into().unwrap() } fn get_default_l1_addresses() -> (EthAddress, EthAddress, EthAddress) { @@ -161,31 +266,31 @@ mod test_utils { calldata.span() } - fn get_l2_votes_token_deployment_calldata( - initial_owner: ContractAddress, - permitted_minter: ContractAddress, - token_gov: ContractAddress, - initial_supply: u256, + fn get_votes_lock_deployment_calldata( + locked_token: ContractAddress, token_gov: ContractAddress, ) -> Span { // Set the constructor calldata. let mut calldata = ArrayTrait::new(); 'NAME'.serialize(ref calldata); 'SYMBOL'.serialize(ref calldata); 18_u8.serialize(ref calldata); - initial_supply.serialize(ref calldata); - initial_owner.serialize(ref calldata); - permitted_minter.serialize(ref calldata); + locked_token.serialize(ref calldata); token_gov.serialize(ref calldata); DEFAULT_UPGRADE_DELAY.serialize(ref calldata); calldata.span() } - fn simple_deploy_l2_token() -> ContractAddress { + fn simple_deploy_token() -> ContractAddress { let permitted_minter = starknet::contract_address_const::<9256>(); let initial_owner = initial_owner(); deploy_l2_token(:initial_owner, :permitted_minter, initial_supply: 1000_u256) } + fn simple_deploy_lockable_token() -> ContractAddress { + let initial_owner = initial_owner(); + deploy_lockable_token(:initial_owner, initial_supply: 1000_u256) + } + fn deploy_l2_token( initial_owner: ContractAddress, permitted_minter: ContractAddress, initial_supply: u256, ) -> ContractAddress { @@ -204,22 +309,54 @@ mod test_utils { l2_token } - fn deploy_l2_votes_token( - initial_owner: ContractAddress, permitted_minter: ContractAddress, initial_supply: u256, + fn deploy_lockable_token( + initial_owner: ContractAddress, initial_supply: u256, ) -> ContractAddress { - let calldata = get_l2_votes_token_deployment_calldata( - :initial_owner, :permitted_minter, token_gov: permitted_minter, :initial_supply + let calldata = get_l2_token_deployment_calldata( + :initial_owner, + permitted_minter: permitted_minter(), + token_gov: caller(), + :initial_supply ); // Set the caller address for all the functions calls (except the constructor). set_contract_address_as_caller(); // Deploy the contract. - let (l2_votes_token, _) = deploy_syscall(votes_erc20_class_hash(), 0, calldata, false) + let (token, _) = deploy_syscall(lockable_erc20_class_hash(), 0, calldata, false).unwrap(); + token + } + + fn deploy_votes_lock(locked_token: ContractAddress) -> ContractAddress { + let calldata = get_votes_lock_deployment_calldata(:locked_token, token_gov: locked_token); + + // Set the caller address for all the functions calls (except the constructor). + set_contract_address_as_caller(); + + // Deploy the contract. + let (erc20_votes_lock, _) = deploy_syscall( + erc20_votes_lock_class_hash(), 0, calldata, false + ) .unwrap(); - l2_votes_token + erc20_votes_lock + } + + + fn deploy_lock_and_votes_tokens(initial_supply: u256) -> (ContractAddress, ContractAddress) { + let lockable_token = deploy_lockable_token(initial_owner: caller(), :initial_supply); + let votes_lock_token = deploy_votes_lock(locked_token: lockable_token); + (lockable_token, votes_lock_token) } + fn deploy_lock_and_votes_tokens_with_owner( + initial_owner: ContractAddress, initial_supply: u256 + ) -> (ContractAddress, ContractAddress) { + let lockable_token = deploy_lockable_token(:initial_owner, :initial_supply); + let votes_lock_token = deploy_votes_lock(locked_token: lockable_token); + (lockable_token, votes_lock_token) + } + + fn deploy_upgraded_legacy_bridge( l1_token: EthAddress, l2_recipient: ContractAddress, token_mismatch: bool ) -> ContractAddress { @@ -290,15 +427,15 @@ mod test_utils { fn pop_and_deserialize_last_event, impl TDrop: Drop>( address: ContractAddress ) -> T { - let mut prev_log: T = starknet::testing::pop_log(address: address) - .expect('Event deserializion failed'); + let mut prev_log = starknet::testing::pop_log_raw(address: address) + .expect('Event queue is empty.'); loop { - match starknet::testing::pop_log::(:address) { + match starknet::testing::pop_log_raw(:address) { Option::Some(log) => { prev_log = log; }, Option::None(()) => { break; }, }; }; - prev_log + deserialize_event(raw_event: prev_log) } @@ -367,23 +504,23 @@ mod test_utils { } fn set_caller_as_upgrade_governor(replaceable_address: ContractAddress) { - let contract_roles = get_roles(token_bridge_address: replaceable_address); + let contract_roles = get_roles(contract_address: replaceable_address); contract_roles.register_upgrade_governor(account: caller()); } fn set_caller_as_app_role_admin_app_governor(token_bridge_address: ContractAddress) { - let token_bridge_roles = get_roles(:token_bridge_address); + let token_bridge_roles = get_roles(contract_address: token_bridge_address); token_bridge_roles.register_app_role_admin(account: caller()); token_bridge_roles.register_app_governor(account: caller()); } fn set_caller_as_security_admin(token_bridge_address: ContractAddress) { - let token_bridge_roles = get_roles(:token_bridge_address); + let token_bridge_roles = get_roles(contract_address: token_bridge_address); token_bridge_roles.register_security_admin(account: caller()); } fn set_caller_as_security_agent(token_bridge_address: ContractAddress) { - let token_bridge_roles = get_roles(:token_bridge_address); + let token_bridge_roles = get_roles(contract_address: token_bridge_address); token_bridge_roles.register_security_agent(account: caller()); } @@ -562,4 +699,23 @@ mod test_utils { amount: amount_to_deposit ); } + + fn deploy_account_internal( + account_contract_class_hash: ClassHash, public_key: felt252 + ) -> ContractAddress { + // Deploy the contract. + let calldata = array![public_key]; + let (account_address, _) = deploy_syscall( + account_contract_class_hash, 0, calldata.span(), false + ) + .unwrap(); + account_address + } + + fn deploy_account(public_key: felt252) -> ContractAddress { + deploy_account_internal( + account_contract_class_hash: TestAccount::TEST_CLASS_HASH.try_into().unwrap(), + :public_key + ) + } } diff --git a/src/cairo/token_bridge.cairo b/src/cairo/token_bridge.cairo index a0dceed..5011bcd 100644 --- a/src/cairo/token_bridge.cairo +++ b/src/cairo/token_bridge.cairo @@ -7,7 +7,9 @@ use zeroable::Zeroable; #[starknet::contract] mod TokenBridge { use super::super::err_msg::AccessErrors::{ - CALLER_MISSING_ROLE, ZERO_ADDRESS, ALREADY_INITIALIZED, ONLY_UPGRADE_GOVERNOR + CALLER_MISSING_ROLE, ZERO_ADDRESS, ALREADY_INITIALIZED, ONLY_APP_GOVERNOR, ONLY_OPERATOR, + ONLY_TOKEN_ADMIN, ONLY_UPGRADE_GOVERNOR, ONLY_SECURITY_ADMIN, ONLY_SECURITY_AGENT, + GOV_ADMIN_CANNOT_RENOUNCE }; use super::super::err_msg::ERC20Errors as ERC20Errors; use super::super::err_msg::ReplaceErrors as ReplaceErrors; @@ -591,7 +593,7 @@ mod TokenBridge { self.only_upgrade_governor(); // Validate implementation is not finalized. - assert(!self.is_finalized(), 'FINALIZED'); + assert(!self.is_finalized(), ReplaceErrors::FINALIZED); let now = get_block_timestamp(); let impl_activation_time = self.get_impl_activation_time(:implementation_data); @@ -599,10 +601,10 @@ mod TokenBridge { // Zero activation time means that this implementation & init vector combination // was not previously added. - assert(impl_activation_time.is_non_zero(), 'UNKNOWN_IMPLEMENTATION'); + assert(impl_activation_time.is_non_zero(), ReplaceErrors::UNKNOWN_IMPLEMENTATION); - assert(impl_activation_time <= now, 'NOT_ENABLED_YET'); - assert(now <= impl_expiration_time, 'IMPLEMENTATION_EXPIRED'); + 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 }); @@ -626,14 +628,14 @@ mod TokenBridge { function_selector: EIC_INITIALIZE_SELECTOR, calldata: calldata_wrapper.span() ); - assert(res.is_ok(), 'EIC_LIB_CALL_FAILED'); + 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(), 'REPLACE_CLASS_HASH_FAILED'); + assert(result.is_ok(), ReplaceErrors::REPLACE_CLASS_HASH_FAILED); // Remove implementation data, as it was comsumed. self.set_impl_activation_time(:implementation_data, activation_time: 0); @@ -643,13 +645,10 @@ mod TokenBridge { #[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); } @@ -810,6 +809,13 @@ mod TokenBridge { self._grant_role_and_emit(role: APP_ROLE_ADMIN, :account, :event); } + fn remove_app_role_admin(ref self: ContractState, account: ContractAddress) { + let event = Event::AppRoleAdminRemoved( + AppRoleAdminRemoved { removed_account: account, removed_by: get_caller_address() } + ); + self._revoke_role_and_emit(role: APP_ROLE_ADMIN, :account, :event); + } + fn register_security_admin(ref self: ContractState, account: ContractAddress) { let event = Event::SecurityAdminAdded( SecurityAdminAdded { added_account: account, added_by: get_caller_address() } @@ -817,13 +823,6 @@ mod TokenBridge { self._grant_role_and_emit(role: SECURITY_ADMIN, :account, :event); } - fn register_security_agent(ref self: ContractState, account: ContractAddress) { - let event = Event::SecurityAgentAdded( - SecurityAgentAdded { added_account: account, added_by: get_caller_address() } - ); - self._grant_role_and_emit(role: SECURITY_AGENT, :account, :event); - } - fn remove_security_admin(ref self: ContractState, account: ContractAddress) { let event = Event::SecurityAdminRemoved( SecurityAdminRemoved { removed_account: account, removed_by: get_caller_address() } @@ -831,6 +830,13 @@ mod TokenBridge { self._revoke_role_and_emit(role: SECURITY_ADMIN, :account, :event); } + fn register_security_agent(ref self: ContractState, account: ContractAddress) { + let event = Event::SecurityAgentAdded( + SecurityAgentAdded { added_account: account, added_by: get_caller_address() } + ); + self._grant_role_and_emit(role: SECURITY_AGENT, :account, :event); + } + fn remove_security_agent(ref self: ContractState, account: ContractAddress) { let event = Event::SecurityAgentRemoved( SecurityAgentRemoved { removed_account: account, removed_by: get_caller_address() } @@ -839,13 +845,6 @@ mod TokenBridge { } - fn remove_app_role_admin(ref self: ContractState, account: ContractAddress) { - let event = Event::AppRoleAdminRemoved( - AppRoleAdminRemoved { removed_account: account, removed_by: get_caller_address() } - ); - self._revoke_role_and_emit(role: APP_ROLE_ADMIN, :account, :event); - } - fn register_governance_admin(ref self: ContractState, account: ContractAddress) { let event = Event::GovernanceAdminAdded( GovernanceAdminAdded { added_account: account, added_by: get_caller_address() } @@ -910,7 +909,7 @@ mod TokenBridge { // TODO - change to GOVERNANCE_ADMIN_CANNOT_SELF_REMOVE when the 32 characters limitations // is off. fn renounce(ref self: ContractState, role: RoleId) { - assert(role != GOVERNANCE_ADMIN, 'GOV_ADMIN_CANNOT_SELF_REMOVE'); + assert(role != GOVERNANCE_ADMIN, GOV_ADMIN_CANNOT_RENOUNCE); self.renounce_role(:role, account: get_caller_address()) // TODO add another event? Currently there are two events when a role is removed but // only one if it was renounced. @@ -950,7 +949,7 @@ mod TokenBridge { fn _initialize_roles(ref self: ContractState) { let provisional_governance_admin = get_caller_address(); let un_initialized = self.get_role_admin(role: GOVERNANCE_ADMIN) == 0; - assert(un_initialized, 'ROLES_ALREADY_INITIALIZED'); + assert(un_initialized, ALREADY_INITIALIZED); self._grant_role(role: GOVERNANCE_ADMIN, account: provisional_governance_admin); self._set_role_admin(role: APP_GOVERNOR, admin_role: APP_ROLE_ADMIN); self._set_role_admin(role: APP_ROLE_ADMIN, admin_role: GOVERNANCE_ADMIN); @@ -965,24 +964,24 @@ mod TokenBridge { } fn only_app_governor(self: @ContractState) { - assert(self.is_app_governor(get_caller_address()), 'ONLY_APP_GOVERNOR'); + assert(self.is_app_governor(get_caller_address()), ONLY_APP_GOVERNOR); } fn only_operator(self: @ContractState) { - assert(self.is_operator(get_caller_address()), 'ONLY_OPERATOR'); + assert(self.is_operator(get_caller_address()), ONLY_OPERATOR); } fn only_token_admin(self: @ContractState) { - assert(self.is_token_admin(get_caller_address()), 'ONLY_TOKEN_ADMIN'); + assert(self.is_token_admin(get_caller_address()), ONLY_TOKEN_ADMIN); } fn only_upgrade_governor(self: @ContractState) { - assert(self.is_upgrade_governor(get_caller_address()), 'ONLY_UPGRADE_GOVERNOR'); + assert(self.is_upgrade_governor(get_caller_address()), ONLY_UPGRADE_GOVERNOR); } fn only_security_admin(self: @ContractState) { - assert(self.is_security_admin(get_caller_address()), 'ONLY_SECURITY_ADMIN'); + assert(self.is_security_admin(get_caller_address()), ONLY_SECURITY_ADMIN); } fn only_security_agent(self: @ContractState) { - assert(self.is_security_agent(get_caller_address()), 'ONLY_SECURITY_AGENT'); + assert(self.is_security_agent(get_caller_address()), ONLY_SECURITY_AGENT); } } diff --git a/src/cairo/token_bridge_admin_test.cairo b/src/cairo/token_bridge_admin_test.cairo index 669c32f..774c7ae 100644 --- a/src/cairo/token_bridge_admin_test.cairo +++ b/src/cairo/token_bridge_admin_test.cairo @@ -14,9 +14,8 @@ mod token_bridge_admin_test { set_contract_address_as_not_caller, pop_and_deserialize_last_event, get_token_bridge, get_token_bridge_admin, set_caller_as_app_role_admin_app_governor, deploy_token_bridge, stock_erc20_class_hash, get_default_l1_addresses, withdraw_and_validate, - set_caller_as_security_agent, set_caller_as_security_admin, _get_daily_withdrawal_limit, - enable_withdrawal_limit, disable_withdrawal_limit, default_amount, - deploy_new_token_and_deposit, + _get_daily_withdrawal_limit, enable_withdrawal_limit, disable_withdrawal_limit, + default_amount, deploy_new_token_and_deposit, }; use super::super::token_bridge_interface::{ITokenBridgeDispatcher, ITokenBridgeDispatcherTrait}; @@ -608,7 +607,7 @@ mod token_bridge_admin_test { #[test] #[should_panic(expected: ('ONLY_SECURITY_AGENT', 'ENTRYPOINT_FAILED',))] #[available_gas(30000000)] - fn test_apply_withdrawal_limit_not_app_governor() { + fn test_enable_withdrawal_limit_not_security_agent() { let token_bridge_address = deploy_token_bridge(); let token_bridge_admin = get_token_bridge_admin(:token_bridge_address); @@ -617,10 +616,25 @@ mod token_bridge_admin_test { token_bridge_admin.enable_withdrawal_limit(:l1_token); } + #[test] + #[should_panic(expected: ('ONLY_SECURITY_ADMIN', 'ENTRYPOINT_FAILED',))] + #[available_gas(30000000)] + fn test_disable_withdrawal_limit_not_security_admin() { + let token_bridge_address = deploy_token_bridge(); + let token_bridge_admin = get_token_bridge_admin(:token_bridge_address); + + // Use an arbitrary l1 token address. + let (_, l1_token, _) = get_default_l1_addresses(); + + // Change the contract address since the caller is the security admin. + set_contract_address_as_not_caller(); + token_bridge_admin.disable_withdrawal_limit(:l1_token); + } + #[test] #[should_panic(expected: ('TOKEN_NOT_IN_BRIDGE', 'ENTRYPOINT_FAILED',))] #[available_gas(30000000)] - fn test_apply_withdrawal_limit_token_not_in_bridge() { + fn test_enable_withdrawal_limit_token_not_in_bridge() { // Deploy the token bridge and set the caller as the app governer (and as App Role Admin). let token_bridge_admin = deploy_and_prepare(); let token_bridge_address = token_bridge_admin.contract_address; @@ -629,4 +643,17 @@ mod token_bridge_admin_test { let (_, l1_token, _) = get_default_l1_addresses(); enable_withdrawal_limit(:token_bridge_address, :l1_token); } + + #[test] + #[should_panic(expected: ('TOKEN_NOT_IN_BRIDGE', 'ENTRYPOINT_FAILED',))] + #[available_gas(30000000)] + fn test_disable_withdrawal_limit_token_not_in_bridge() { + // Deploy the token bridge and set the caller as the app governer (and as App Role Admin). + let token_bridge_admin = deploy_and_prepare(); + let token_bridge_address = token_bridge_admin.contract_address; + + // Use an arbitrary l1 token address (which was not deployed). + let (_, l1_token, _) = get_default_l1_addresses(); + disable_withdrawal_limit(:token_bridge_address, :l1_token); + } } diff --git a/src/cairo/token_bridge_test.cairo b/src/cairo/token_bridge_test.cairo index 2d32c10..2bf5757 100644 --- a/src/cairo/token_bridge_test.cairo +++ b/src/cairo/token_bridge_test.cairo @@ -42,7 +42,7 @@ mod token_bridge_test { get_erc20_token, deploy_l2_token, pop_and_deserialize_last_event, pop_last_k_events, deserialize_event, arbitrary_event, assert_role_granted_event, assert_role_revoked_event, validate_empty_event_queue, get_roles, get_access_control, deploy_token_bridge, - stock_erc20_class_hash, votes_erc20_class_hash, deploy_stub_msg_receiver, + stock_erc20_class_hash, erc20_votes_lock_class_hash, deploy_stub_msg_receiver, withdraw_and_validate, deploy_upgraded_legacy_bridge, get_token_bridge, get_token_bridge_admin, _get_daily_withdrawal_limit, disable_withdrawal_limit, enable_withdrawal_limit, set_caller_as_app_role_admin_app_governor, default_amount, @@ -445,14 +445,6 @@ mod token_bridge_test { _handle_token_deployment(:erc20_class_hash); } - #[test] - #[available_gas(30000000)] - fn test_successful_votes_erc20_handle_token_deployment() { - // Set Votes ERC20 class hash. - let erc20_class_hash = votes_erc20_class_hash(); - _handle_token_deployment(:erc20_class_hash); - } - fn _handle_token_deployment(erc20_class_hash: ClassHash) { let (l1_bridge_address, l1_token, _) = get_default_l1_addresses(); // Deploy the token bridge and set the caller as the app governer (and as App Role Admin). @@ -563,12 +555,16 @@ mod token_bridge_test { token_bridge_admin.set_l2_token_governance(caller()); let t1_roles = get_roles( - internal_deploy_token(:token_bridge_address, :l1_bridge_address, l1_token: l1_token1) + contract_address: internal_deploy_token( + :token_bridge_address, :l1_bridge_address, l1_token: l1_token1 + ) ); token_bridge_admin.set_l2_token_governance(not_caller()); let t2_roles = get_roles( - internal_deploy_token(:token_bridge_address, :l1_bridge_address, l1_token: l1_token2) + contract_address: internal_deploy_token( + :token_bridge_address, :l1_bridge_address, l1_token: l1_token2 + ) ); assert(t1_roles.is_governance_admin(caller()), 'l2_token1 Role not granted'); diff --git a/src/cairo/update_712_vars_eic.cairo b/src/cairo/update_712_vars_eic.cairo index f470296..941931e 100644 --- a/src/cairo/update_712_vars_eic.cairo +++ b/src/cairo/update_712_vars_eic.cairo @@ -3,7 +3,7 @@ #[starknet::contract] mod Update712VarsEIC { use super::super::replaceability_interface::IEICInitializable; - use openzeppelin::token::erc20::presets::erc20votes::ERC20VotesPreset::{ + use openzeppelin::token::erc20::presets::erc20_votes_lock::ERC20VotesLock::{ DAPP_NAME, DAPP_VERSION }; diff --git a/src/openzeppelin/token/erc20/erc20.cairo b/src/openzeppelin/token/erc20/erc20.cairo index 6144bba..608a215 100644 --- a/src/openzeppelin/token/erc20/erc20.cairo +++ b/src/openzeppelin/token/erc20/erc20.cairo @@ -35,9 +35,9 @@ mod ERC20 { /// Emitted when tokens are moved from address `from` to address `to`. #[derive(Copy, Drop, PartialEq, starknet::Event)] struct Transfer { - #[key] + // #[key] - Not indexed, to maintain backward compatibility. from: ContractAddress, - #[key] + // #[key] - Not indexed, to maintain backward compatibility. to: ContractAddress, value: u256 } @@ -46,9 +46,9 @@ mod ERC20 { /// to [approve](approve). `value` is the new allowance. #[derive(Copy, Drop, PartialEq, starknet::Event)] struct Approval { - #[key] + // #[key] - Not indexed, to maintain backward compatibility. owner: ContractAddress, - #[key] + // #[key] - Not indexed, to maintain backward compatibility. spender: ContractAddress, value: u256 } diff --git a/src/openzeppelin/token/erc20/presets.cairo b/src/openzeppelin/token/erc20/presets.cairo index 00a3144..2f39182 100644 --- a/src/openzeppelin/token/erc20/presets.cairo +++ b/src/openzeppelin/token/erc20/presets.cairo @@ -1,3 +1,3 @@ -mod erc20votes; +mod erc20_votes_lock; -use erc20votes::ERC20VotesPreset; +use erc20_votes_lock::ERC20VotesLock; diff --git a/src/openzeppelin/token/erc20/presets/erc20votes.cairo b/src/openzeppelin/token/erc20/presets/erc20_votes_lock.cairo similarity index 87% rename from src/openzeppelin/token/erc20/presets/erc20votes.cairo rename to src/openzeppelin/token/erc20/presets/erc20_votes_lock.cairo index 7ed0c6a..64cfb9e 100644 --- a/src/openzeppelin/token/erc20/presets/erc20votes.cairo +++ b/src/openzeppelin/token/erc20/presets/erc20_votes_lock.cairo @@ -3,52 +3,42 @@ /// ERC20 with the ERC20Votes extension. #[starknet::contract] -mod ERC20VotesPreset { +mod ERC20VotesLock { use core::result::ResultTrait; use src::access_control_interface::{ - IAccessControl, RoleId, RoleAdminChanged, RoleGranted, RoleRevoked}; + IAccessControl, RoleId, RoleAdminChanged, RoleGranted, RoleRevoked + }; use src::roles_interface::IMinimalRoles; use src::roles_interface::{ - GOVERNANCE_ADMIN, UPGRADE_GOVERNOR, - GovernanceAdminAdded, GovernanceAdminRemoved, UpgradeGovernorAdded, UpgradeGovernorRemoved + GOVERNANCE_ADMIN, UPGRADE_GOVERNOR, GovernanceAdminAdded, GovernanceAdminRemoved, + UpgradeGovernorAdded, UpgradeGovernorRemoved }; use src::err_msg::AccessErrors::{ - INVALID_MINTER, - CALLER_MISSING_ROLE, - ZERO_ADDRESS, - ALREADY_INITIALIZED, - ONLY_MINTER, - ONLY_UPGRADE_GOVERNOR, - ONLY_SELF_CAN_RENOUNCE, - GOV_ADMIN_CANNOT_RENOUNCE, + INVALID_TOKEN, CALLER_MISSING_ROLE, ZERO_ADDRESS, ALREADY_INITIALIZED, + ONLY_UPGRADE_GOVERNOR, ONLY_SELF_CAN_RENOUNCE, GOV_ADMIN_CANNOT_RENOUNCE, ZERO_ADDRESS_GOV_ADMIN, }; use src::err_msg::ReplaceErrors::{ - FINALIZED, - UNKNOWN_IMPLEMENTATION, - NOT_ENABLED_YET, - IMPLEMENTATION_EXPIRED, - EIC_LIB_CALL_FAILED, - REPLACE_CLASS_HASH_FAILED, + FINALIZED, UNKNOWN_IMPLEMENTATION, NOT_ENABLED_YET, IMPLEMENTATION_EXPIRED, + EIC_LIB_CALL_FAILED, REPLACE_CLASS_HASH_FAILED, }; use src::replaceability_interface::{ ImplementationData, IReplaceable, IReplaceableDispatcher, IReplaceableDispatcherTrait, - EIC_INITIALIZE_SELECTOR, IMPLEMENTATION_EXPIRATION, - ImplementationAdded, ImplementationRemoved, ImplementationReplaced, ImplementationFinalized + EIC_INITIALIZE_SELECTOR, IMPLEMENTATION_EXPIRATION, ImplementationAdded, + ImplementationRemoved, ImplementationReplaced, ImplementationFinalized }; use ERC20::InternalTrait; use starknet::{ - ContractAddress, - contract_address_const, - get_caller_address, - get_block_timestamp + ContractAddress, contract_address_const, get_block_timestamp, get_caller_address, + get_contract_address, }; use starknet::syscalls::library_call_syscall; use starknet::class_hash::{ClassHash, Felt252TryIntoClassHash}; - use src::mintable_token_interface::{IMintableToken, IMintableTokenCamel}; + use src::erc20_interface::{IERC20Dispatcher, IERC20DispatcherTrait}; + use src::mintable_lock_interface::{IMintableLock, ITokenLock, Locked, Unlocked}; use openzeppelin::governance::utils::interfaces::IVotes; use openzeppelin::token::erc20::ERC20; use openzeppelin::token::erc20::extensions::ERC20Votes; @@ -61,7 +51,7 @@ mod ERC20VotesPreset { #[storage] struct Storage { - permitted_minter: ContractAddress, + locked_token: ContractAddress, // --- Replaceability --- // Delay in seconds before performing an upgrade. upgrade_delay: u64, @@ -102,6 +92,9 @@ mod ERC20VotesPreset { GovernanceAdminRemoved: GovernanceAdminRemoved, UpgradeGovernorAdded: UpgradeGovernorAdded, UpgradeGovernorRemoved: UpgradeGovernorRemoved, + // --- Token Lock --- + Locked: Locked, + Unlocked: Unlocked, } // @@ -135,9 +128,7 @@ mod ERC20VotesPreset { name: felt252, symbol: felt252, decimals: u8, - initial_supply: u256, - recipient: ContractAddress, - permitted_minter: ContractAddress, + locked_token: ContractAddress, provisional_governance_admin: ContractAddress, upgrade_delay: u64, ) { @@ -146,92 +137,14 @@ mod ERC20VotesPreset { let mut erc20_state = ERC20::unsafe_new_contract_state(); ERC20::InternalImpl::initializer(ref erc20_state, name, symbol, decimals); - ERC20::InternalImpl::_mint::( - ref erc20_state, recipient, initial_supply - ); - assert(permitted_minter.is_non_zero(), INVALID_MINTER); - self.permitted_minter.write(permitted_minter); + assert(locked_token.is_non_zero(), INVALID_TOKEN); + self.locked_token.write(locked_token); self._initialize_roles(:provisional_governance_admin); self.upgrade_delay.write(upgrade_delay); } #[generate_trait] - impl InternalFunctions of IInternalFunctions { - // --- Replaceability --- - fn is_finalized(self: @ContractState) -> bool { - self.finalized.read() - } - - fn finalize(ref self: ContractState) { - self.finalized.write(true); - } - - 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); - } - - // --- Access Control --- - fn assert_only_role(self: @ContractState, role: RoleId) { - let authorized: bool = self.has_role(:role, account: get_caller_address()); - assert(authorized, 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 }); - } - + impl RolesInternal of _RolesInternal { // --- Roles --- fn _grant_role_and_emit( ref self: ContractState, role: RoleId, account: ContractAddress, event: Event @@ -278,26 +191,34 @@ mod ERC20VotesPreset { // #[external(v0)] - impl MintableToken of IMintableToken { - fn permissioned_mint(ref self: ContractState, account: ContractAddress, amount: u256) { - assert(get_caller_address() == self.permitted_minter.read(), ONLY_MINTER); - let mut unsafe_state = ERC20::unsafe_new_contract_state(); - unsafe_state._mint::(recipient: account, :amount); - } - fn permissioned_burn(ref self: ContractState, account: ContractAddress, amount: u256) { - assert(get_caller_address() == self.permitted_minter.read(), ONLY_MINTER); - let mut unsafe_state = ERC20::unsafe_new_contract_state(); - unsafe_state._burn::(:account, :amount); + impl MintableLock of IMintableLock { + fn permissioned_lock_and_delegate( + ref self: ContractState, + account: ContractAddress, + delegatee: ContractAddress, + amount: u256 + ) { + // Only locked token. + assert(get_caller_address() == self.locked_token.read(), 'INVALID_CALLER'); + + // Lock. + self._lock(:account, :amount); + + // Delegate. + let mut unsafe_state = ERC20Votes::unsafe_new_contract_state(); + ERC20Votes::InternalImpl::_delegate(ref unsafe_state, :account, :delegatee); } } #[external(v0)] - impl MintableTokenCamelImpl of IMintableTokenCamel { - fn permissionedMint(ref self: ContractState, account: ContractAddress, amount: u256) { - MintableToken::permissioned_mint(ref self, account, amount); + impl TokenLock of ITokenLock { + fn lock(ref self: ContractState, amount: u256) { + let account = get_caller_address(); + self._lock(:account, :amount); } - fn permissionedBurn(ref self: ContractState, account: ContractAddress, amount: u256) { - MintableToken::permissioned_burn(ref self, account, amount); + fn unlock(ref self: ContractState, amount: u256) { + let account = get_caller_address(); + self._unlock(:account, :amount); } } @@ -308,6 +229,37 @@ mod ERC20VotesPreset { poseidon::poseidon_hash_span(hash_input.span()) } + #[generate_trait] + impl ReplaceableInternal of _ReplaceableInternal { + fn is_finalized(self: @ContractState) -> bool { + self.finalized.read() + } + + fn finalize(ref self: ContractState) { + self.finalized.write(true); + } + + 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); + } + + 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) + } + + 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 { @@ -416,7 +368,32 @@ mod ERC20VotesPreset { } #[generate_trait] - impl AccessControlImplInternal of IAccessControlInternal { + impl LockImpl of _LockImpl { + fn _lock(ref self: ContractState, account: ContractAddress, amount: u256) { + let _this = get_contract_address(); + IERC20Dispatcher { + contract_address: self.locked_token.read() + }.transfer_from(sender: account, recipient: _this, :amount); + let mut unsafe_state = ERC20::unsafe_new_contract_state(); + unsafe_state._mint::(recipient: account, :amount); + + self.emit(Locked { account: account, amount: amount }); + } + + fn _unlock(ref self: ContractState, account: ContractAddress, amount: u256) { + let mut unsafe_state = ERC20::unsafe_new_contract_state(); + unsafe_state._burn::(:account, :amount); + + IERC20Dispatcher { + contract_address: self.locked_token.read() + }.transfer(recipient: account, :amount); + + self.emit(Unlocked { account: account, amount: amount }); + } + } + + #[generate_trait] + impl AccessControlImplInternal of _AccessControlImplInternal { fn grant_role(ref self: ContractState, role: RoleId, account: ContractAddress) { let admin = self.get_role_admin(:role); self.assert_only_role(role: admin); @@ -435,6 +412,49 @@ mod ERC20VotesPreset { } } + #[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, 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 { @@ -535,9 +555,9 @@ mod ERC20VotesPreset { let mut unsafe_state = ERC20::unsafe_new_contract_state(); let caller = starknet::get_caller_address(); ERC20::InternalImpl::_spend_allowance(ref unsafe_state, sender, caller, amount); - ERC20::InternalImpl::_transfer::( - ref unsafe_state, sender, recipient, amount - ); + ERC20::InternalImpl::_transfer::< + ERC20VotesHooksImpl + >(ref unsafe_state, sender, recipient, amount); true } diff --git a/src/openzeppelin/token/erc20_v070/erc20.cairo b/src/openzeppelin/token/erc20_v070/erc20.cairo index 42a2181..a2d54db 100644 --- a/src/openzeppelin/token/erc20_v070/erc20.cairo +++ b/src/openzeppelin/token/erc20_v070/erc20.cairo @@ -9,7 +9,6 @@ //! 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; @@ -19,17 +18,18 @@ mod ERC20 { use openzeppelin::token::erc20::interface::IERC20CamelOnly; use src::mintable_token_interface::{IMintableToken, IMintableTokenCamel}; use src::access_control_interface::{ - IAccessControl, RoleId, RoleAdminChanged, RoleGranted, RoleRevoked}; + IAccessControl, RoleId, RoleAdminChanged, RoleGranted, RoleRevoked + }; use src::roles_interface::IMinimalRoles; use src::roles_interface::{ - GOVERNANCE_ADMIN, UPGRADE_GOVERNOR, - GovernanceAdminAdded, GovernanceAdminRemoved, UpgradeGovernorAdded, UpgradeGovernorRemoved + 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 + EIC_INITIALIZE_SELECTOR, IMPLEMENTATION_EXPIRATION, ImplementationAdded, + ImplementationRemoved, ImplementationReplaced, ImplementationFinalized }; use starknet::ContractAddress; use starknet::class_hash::{ClassHash, Felt252TryIntoClassHash}; @@ -44,10 +44,8 @@ mod ERC20 { 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, @@ -57,13 +55,11 @@ mod ERC20 { 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] @@ -90,9 +86,9 @@ mod ERC20 { /// Emitted when tokens are moved from address `from` to address `to`. #[derive(Copy, Drop, PartialEq, starknet::Event)] struct Transfer { - #[key] + // #[key] - Not indexed, to maintain backward compatibility. from: ContractAddress, - #[key] + // #[key] - Not indexed, to maintain backward compatibility. to: ContractAddress, value: u256 } @@ -101,9 +97,9 @@ mod ERC20 { /// to [approve](approve). `value` is the new allowance. #[derive(Copy, Drop, PartialEq, starknet::Event)] struct Approval { - #[key] + // #[key] - Not indexed, to maintain backward compatibility. owner: ContractAddress, - #[key] + // #[key] - Not indexed, to maintain backward compatibility. spender: ContractAddress, value: u256 } @@ -132,82 +128,7 @@ mod ERC20 { #[generate_trait] - impl InternalFunctions of IInternalFunctions { - // --- Replaceability --- - fn is_finalized(self: @ContractState) -> bool { - self.finalized.read() - } - - fn finalize(ref self: ContractState) { - self.finalized.write(true); - } - - 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); - } - - // --- Access Control --- - 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 }); - } - + impl RolesInternal of _RolesInternal { // --- Roles --- fn _grant_role_and_emit( ref self: ContractState, role: RoleId, account: ContractAddress, event: Event @@ -239,8 +160,8 @@ mod ERC20 { 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); + 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); @@ -248,8 +169,8 @@ mod ERC20 { fn only_upgrade_governor(self: @ContractState) { assert( - self.is_upgrade_governor(get_caller_address()), - AccessErrors::ONLY_UPGRADE_GOVERNOR); + self.is_upgrade_governor(get_caller_address()), AccessErrors::ONLY_UPGRADE_GOVERNOR + ); } } @@ -286,6 +207,44 @@ mod ERC20 { 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 { @@ -412,6 +371,49 @@ mod ERC20 { } } + #[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 {