diff --git a/Scarb.lock b/Scarb.lock index ea3a323..b655893 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -146,7 +146,7 @@ dependencies = [ [[package]] name = "starkware_utils" version = "1.0.0" -source = "git+https://github.com/starkware-libs/starkware-starknet-utils?rev=e11de236fe1e36c9556690749f834b6d0e1c4d49#e11de236fe1e36c9556690749f834b6d0e1c4d49" +source = "git+https://github.com/starkware-libs/starkware-starknet-utils?rev=123332b0895ce9116fb2eaa2469739250c3f733d#123332b0895ce9116fb2eaa2469739250c3f733d" dependencies = [ "openzeppelin", ] @@ -154,7 +154,7 @@ dependencies = [ [[package]] name = "starkware_utils_testing" version = "1.0.0" -source = "git+https://github.com/starkware-libs/starkware-starknet-utils?rev=e11de236fe1e36c9556690749f834b6d0e1c4d49#e11de236fe1e36c9556690749f834b6d0e1c4d49" +source = "git+https://github.com/starkware-libs/starkware-starknet-utils?rev=123332b0895ce9116fb2eaa2469739250c3f733d#123332b0895ce9116fb2eaa2469739250c3f733d" dependencies = [ "openzeppelin", "snforge_std", diff --git a/Scarb.toml b/Scarb.toml index 1afffba..1f725dc 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -15,8 +15,8 @@ snforge_std = "0.49.0" assert_macros = "2.12.2" openzeppelin = "2.0.0" openzeppelin_testing = "4.2.0" -starkware_utils = { git ="https://github.com/starkware-libs/starkware-starknet-utils", rev="e11de236fe1e36c9556690749f834b6d0e1c4d49" } -starkware_utils_testing = { git="https://github.com/starkware-libs/starkware-starknet-utils", rev="e11de236fe1e36c9556690749f834b6d0e1c4d49" } +starkware_utils = { git ="https://github.com/starkware-libs/starkware-starknet-utils", rev="123332b0895ce9116fb2eaa2469739250c3f733d" } +starkware_utils_testing = { git="https://github.com/starkware-libs/starkware-starknet-utils", rev="123332b0895ce9116fb2eaa2469739250c3f733d" } [workspace.tool.fmt] sort-module-level-items = true diff --git a/packages/usdc_migration/src/events.cairo b/packages/usdc_migration/src/events.cairo index 8b13789..d3f810c 100644 --- a/packages/usdc_migration/src/events.cairo +++ b/packages/usdc_migration/src/events.cairo @@ -1 +1,14 @@ +pub mod USDCMigrationEvents { + use starknet::ContractAddress; + #[derive(Drop, starknet::Event, Debug, PartialEq)] + pub struct USDCMigrated { + #[key] + pub user: ContractAddress, + #[key] + pub from_token: ContractAddress, + #[key] + pub to_token: ContractAddress, + pub amount: u256, + } +} diff --git a/packages/usdc_migration/src/interface.cairo b/packages/usdc_migration/src/interface.cairo index 6cfaa2c..add7737 100644 --- a/packages/usdc_migration/src/interface.cairo +++ b/packages/usdc_migration/src/interface.cairo @@ -1,5 +1,8 @@ #[starknet::interface] -pub trait IUSDCMigration { //interface +pub trait IUSDCMigration { + /// Exchanges (1:1) `amount` of legacy token for new token. + /// Precondition: Sufficient allowance of legacy token. + fn swap_to_new(ref self: T, amount: u256); } #[starknet::interface] diff --git a/packages/usdc_migration/src/tests/test_usdc_migration.cairo b/packages/usdc_migration/src/tests/test_usdc_migration.cairo index 5a19df4..1182e16 100644 --- a/packages/usdc_migration/src/tests/test_usdc_migration.cairo +++ b/packages/usdc_migration/src/tests/test_usdc_migration.cairo @@ -7,16 +7,29 @@ use openzeppelin::upgrades::interface::{ IUpgradeableSafeDispatcherTrait, }; use openzeppelin::upgrades::upgradeable::UpgradeableComponent::Errors as UpgradeableErrors; -use snforge_std::{DeclareResultTrait, TokenTrait}; +use snforge_std::{DeclareResultTrait, EventSpyTrait, EventsFilterTrait, TokenTrait, spy_events}; use starkware_utils::constants::MAX_U256; -use starkware_utils_testing::test_utils::{assert_panic_with_felt_error, cheat_caller_address_once}; +use starkware_utils::erc20::erc20_errors::Erc20Error; +use starkware_utils::errors::Describable; +use starkware_utils_testing::event_test_utils::assert_number_of_events; +use starkware_utils_testing::test_utils::{ + assert_expected_event_emitted, assert_panic_with_error, assert_panic_with_felt_error, + cheat_caller_address_once, +}; +use usdc_migration::events::USDCMigrationEvents::USDCMigrated; use usdc_migration::interface::{ IUSDCMigrationConfigDispatcher, IUSDCMigrationConfigDispatcherTrait, IUSDCMigrationConfigSafeDispatcher, IUSDCMigrationConfigSafeDispatcherTrait, + IUSDCMigrationDispatcher, IUSDCMigrationDispatcherTrait, IUSDCMigrationSafeDispatcher, + IUSDCMigrationSafeDispatcherTrait, +}; +use usdc_migration::tests::test_utils::constants::{ + INITIAL_CONTRACT_SUPPLY, INITIAL_SUPPLY, LEGACY_THRESHOLD, +}; +use usdc_migration::tests::test_utils::{ + deploy_usdc_migration, generic_test_fixture, load_contract_address, load_u256, new_user, + supply_contract, }; -use usdc_migration::tests::test_utils::constants::LEGACY_THRESHOLD; -use usdc_migration::tests::test_utils::{deploy_usdc_migration, load_contract_address, load_u256}; - #[test] fn test_constructor() { let cfg = deploy_usdc_migration(); @@ -116,3 +129,86 @@ fn test_upgrade_assertions() { let result = upgradeable_safe_dispatcher.upgrade(Zero::zero()); assert_panic_with_felt_error(result, UpgradeableErrors::INVALID_CLASS); } + +#[test] +fn test_swap_to_new() { + let cfg = generic_test_fixture(); + let amount = INITIAL_CONTRACT_SUPPLY / 10; + let user = new_user(:cfg, id: 0, legacy_supply: amount); + let usdc_migration_contract = cfg.usdc_migration_contract; + let usdc_migration_dispatcher = IUSDCMigrationDispatcher { + contract_address: usdc_migration_contract, + }; + let legacy_token_address = cfg.legacy_token.contract_address(); + let new_token_address = cfg.new_token.contract_address(); + let legacy_dispatcher = IERC20Dispatcher { contract_address: legacy_token_address }; + let new_dispatcher = IERC20Dispatcher { contract_address: new_token_address }; + + // Spy events. + let mut spy = spy_events(); + + // Approve and migrate. + cheat_caller_address_once(contract_address: legacy_token_address, caller_address: user); + legacy_dispatcher.approve(spender: usdc_migration_contract, :amount); + cheat_caller_address_once(contract_address: usdc_migration_contract, caller_address: user); + usdc_migration_dispatcher.swap_to_new(:amount); + + // Assert user balances are correct. + assert_eq!(legacy_dispatcher.balance_of(account: user), 0); + assert_eq!(new_dispatcher.balance_of(account: user), amount); + + // Assert contract balances are correct. + assert_eq!(legacy_dispatcher.balance_of(account: usdc_migration_contract), amount); + assert_eq!( + new_dispatcher.balance_of(account: usdc_migration_contract), + INITIAL_CONTRACT_SUPPLY - amount, + ); + + // Assert event is emitted. + let events = spy.get_events().emitted_by(contract_address: usdc_migration_contract).events; + assert_number_of_events(actual: events.len(), expected: 1, message: "migrate"); + assert_expected_event_emitted( + spied_event: events[0], + expected_event: USDCMigrated { + user, from_token: legacy_token_address, to_token: new_token_address, amount, + }, + expected_event_selector: @selector!("USDCMigrated"), + expected_event_name: "USDCMigrated", + ); +} + +#[test] +#[feature("safe_dispatcher")] +fn test_swap_to_new_assertions() { + let cfg = deploy_usdc_migration(); + let amount = INITIAL_SUPPLY / 10; + let user = new_user(:cfg, id: 0, legacy_supply: 0); + let usdc_migration_contract = cfg.usdc_migration_contract; + let usdc_migration_safe_dispatcher = IUSDCMigrationSafeDispatcher { + contract_address: usdc_migration_contract, + }; + let legacy_token_address = cfg.legacy_token.contract_address(); + let legacy_dispatcher = IERC20Dispatcher { contract_address: legacy_token_address }; + + // Insufficient user balance. + cheat_caller_address_once(contract_address: legacy_token_address, caller_address: user); + legacy_dispatcher.approve(spender: usdc_migration_contract, :amount); + cheat_caller_address_once(contract_address: cfg.usdc_migration_contract, caller_address: user); + let res = usdc_migration_safe_dispatcher.swap_to_new(:amount); + assert_panic_with_error(res, Erc20Error::INSUFFICIENT_BALANCE.describe()); + + // Insufficient allowance. + supply_contract(target: user, token: cfg.legacy_token, :amount); + cheat_caller_address_once(contract_address: legacy_token_address, caller_address: user); + legacy_dispatcher.approve(spender: usdc_migration_contract, amount: amount / 2); + cheat_caller_address_once(contract_address: usdc_migration_contract, caller_address: user); + let res = usdc_migration_safe_dispatcher.swap_to_new(:amount); + assert_panic_with_error(res, Erc20Error::INSUFFICIENT_ALLOWANCE.describe()); + + // Insufficient contract balance. + cheat_caller_address_once(contract_address: legacy_token_address, caller_address: user); + legacy_dispatcher.approve(spender: usdc_migration_contract, :amount); + cheat_caller_address_once(contract_address: cfg.usdc_migration_contract, caller_address: user); + let res = usdc_migration_safe_dispatcher.swap_to_new(:amount); + assert_panic_with_error(res, Erc20Error::INSUFFICIENT_BALANCE.describe()); +} diff --git a/packages/usdc_migration/src/tests/test_utils.cairo b/packages/usdc_migration/src/tests/test_utils.cairo index d138e97..e4fc5fd 100644 --- a/packages/usdc_migration/src/tests/test_utils.cairo +++ b/packages/usdc_migration/src/tests/test_utils.cairo @@ -1,5 +1,11 @@ -use constants::{INITIAL_SUPPLY, L1_RECIPIENT, LEGACY_THRESHOLD, OWNER_ADDRESS, STARKGATE_ADDRESS}; -use snforge_std::{ContractClassTrait, CustomToken, DeclareResultTrait, Token, TokenTrait}; +use constants::{ + INITIAL_CONTRACT_SUPPLY, INITIAL_SUPPLY, L1_RECIPIENT, LEGACY_THRESHOLD, OWNER_ADDRESS, + STARKGATE_ADDRESS, +}; +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; +use snforge_std::{ + ContractClassTrait, CustomToken, DeclareResultTrait, Token, TokenTrait, set_balance, +}; use starknet::{ContractAddress, EthAddress}; use starkware_utils_testing::test_utils::{Deployable, TokenConfig}; @@ -23,6 +29,7 @@ pub(crate) mod constants { * 10_u256.pow(6); // 140 * million * decimals // TODO: Change to the real value. pub const LEGACY_THRESHOLD: u256 = 100_000; + pub const INITIAL_CONTRACT_SUPPLY: u256 = INITIAL_SUPPLY / 20; pub fn OWNER_ADDRESS() -> ContractAddress { 'OWNER_ADDRESS'.try_into().unwrap() } @@ -34,6 +41,14 @@ pub(crate) mod constants { } } +pub(crate) fn generic_test_fixture() -> USDCMigrationCfg { + let cfg = deploy_usdc_migration(); + supply_contract( + target: cfg.usdc_migration_contract, token: cfg.new_token, amount: INITIAL_CONTRACT_SUPPLY, + ); + cfg +} + fn deploy_tokens() -> (Token, Token) { let legacy_config = TokenConfig { name: "Legacy-USDC", @@ -86,6 +101,22 @@ pub(crate) fn deploy_usdc_migration() -> USDCMigrationCfg { } } +pub(crate) fn new_user(cfg: USDCMigrationCfg, id: u8, legacy_supply: u256) -> ContractAddress { + let user_address = _generate_user_address(:id); + set_balance(target: user_address, new_balance: legacy_supply, token: cfg.legacy_token); + user_address +} + +fn _generate_user_address(id: u8) -> ContractAddress { + ('USER_ADDRESS' + id.into()).try_into().unwrap() +} + +pub(crate) fn supply_contract(target: ContractAddress, token: Token, amount: u256) { + let current_balance = IERC20Dispatcher { contract_address: token.contract_address() } + .balance_of(account: target); + set_balance(:target, new_balance: current_balance + amount, :token); +} + // TODO: Move to starkware_utils_testing. pub(crate) fn load_contract_address( target: ContractAddress, storage_address: felt252, diff --git a/packages/usdc_migration/src/usdc_migration.cairo b/packages/usdc_migration/src/usdc_migration.cairo index 45817bb..395801e 100644 --- a/packages/usdc_migration/src/usdc_migration.cairo +++ b/packages/usdc_migration/src/usdc_migration.cairo @@ -4,9 +4,13 @@ pub mod USDCMigration { use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; use openzeppelin::upgrades::interface::IUpgradeable; use openzeppelin::upgrades::upgradeable::UpgradeableComponent; - use starknet::storage::StoragePointerWriteAccess; - use starknet::{ClassHash, ContractAddress, EthAddress}; + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + use starknet::{ + ClassHash, ContractAddress, EthAddress, get_caller_address, get_contract_address, + }; use starkware_utils::constants::MAX_U256; + use starkware_utils::erc20::erc20_utils::CheckedIERC20DispatcherTrait; + use usdc_migration::events::USDCMigrationEvents::USDCMigrated; use usdc_migration::interface::{IUSDCMigration, IUSDCMigrationConfig}; component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); @@ -37,6 +41,7 @@ pub mod USDCMigration { pub enum Event { OwnableEvent: OwnableComponent::Event, UpgradeableEvent: UpgradeableComponent::Event, + USDCMigrated: USDCMigrated, } #[constructor] @@ -77,6 +82,15 @@ pub mod USDCMigration { #[abi(embed_v0)] pub impl USDCMigrationImpl of IUSDCMigration { //impl logic + fn swap_to_new(ref self: ContractState, amount: u256) { + self + ._swap( + from_token: self.legacy_token_dispatcher.read(), + to_token: self.new_token_dispatcher.read(), + :amount, + ); + // TODO: send to l1 if threshold is reached. + } } #[abi(embed_v0)] @@ -91,4 +105,29 @@ pub mod USDCMigration { // TODO: Send to L1 here according the new threshold? } } + + #[generate_trait] + impl USDCMigrationInternalImpl of USDCMigrationInternalTrait { + fn _swap( + ref self: ContractState, + from_token: IERC20Dispatcher, + to_token: IERC20Dispatcher, + amount: u256, + ) { + let contract_address = get_contract_address(); + let user = get_caller_address(); + from_token.checked_transfer_from(sender: user, recipient: contract_address, :amount); + to_token.checked_transfer(recipient: user, :amount); + + self + .emit( + USDCMigrated { + user, + from_token: from_token.contract_address, + to_token: to_token.contract_address, + amount, + }, + ); + } + } }