diff --git a/Scarb.toml b/Scarb.toml index d33fb78..44e30c1 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -15,7 +15,7 @@ snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v [[target.starknet-contract]] casm = true -build-external-contracts = ["piltover::appchain::appchain"] +build-external-contracts = ["piltover::messaging::mock::messaging_mock"] [scripts] test = "snforge test" diff --git a/scripts/my_script/Scarb.lock b/scripts/my_script/Scarb.lock deleted file mode 100644 index 8184873..0000000 --- a/scripts/my_script/Scarb.lock +++ /dev/null @@ -1,14 +0,0 @@ -# Code generated by scarb DO NOT EDIT. -version = 1 - -[[package]] -name = "my_script" -version = "0.1.0" -dependencies = [ - "sncast_std", -] - -[[package]] -name = "sncast_std" -version = "0.26.0" -source = "git+https://github.com/foundry-rs/starknet-foundry?tag=v0.26.0#50eb589db65e113efe4f09241feb59b574228c7e" diff --git a/scripts/my_script/Scarb.toml b/scripts/my_script/Scarb.toml deleted file mode 100644 index 1772854..0000000 --- a/scripts/my_script/Scarb.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "my_script" -version = "0.1.0" -edition = "2023_11" - -# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html - -[dependencies] -sncast_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v0.26.0" } -starknet = ">=2.6.4" diff --git a/scripts/my_script/src/lib.cairo b/scripts/my_script/src/lib.cairo deleted file mode 100644 index 3276bf6..0000000 --- a/scripts/my_script/src/lib.cairo +++ /dev/null @@ -1 +0,0 @@ -mod my_script; diff --git a/src/bridge/interface.cairo b/src/bridge/interface.cairo index 8c8d2ef..65265b3 100644 --- a/src/bridge/interface.cairo +++ b/src/bridge/interface.cairo @@ -73,7 +73,7 @@ pub trait ITokenBridge { appchain_recipient: ContractAddress, nonce: felt252 ); - fn get_remaining_intraday_allowance(self: @TContractState, token: ContractAddress) -> u256; + fn get_max_total_balance(self: @TContractState, token: ContractAddress) -> u256; } #[starknet::interface] diff --git a/src/bridge/token_bridge.cairo b/src/bridge/token_bridge.cairo index aa87664..7fa8f02 100644 --- a/src/bridge/token_bridge.cairo +++ b/src/bridge/token_bridge.cairo @@ -1,6 +1,6 @@ #[starknet::contract] pub mod TokenBridge { - use openzeppelin::access::ownable::ownable::OwnableComponent::InternalTrait; + use starknet_bridge::withdrawal_limit::component::WithdrawalLimitComponent::InternalTrait; use core::option::OptionTrait; use core::traits::TryInto; use core::starknet::event::EventEmitter; @@ -86,12 +86,13 @@ pub mod TokenBridge { pub const CANNOT_DEACTIVATE: felt252 = 'Cannot deactivate and block'; pub const CANNOT_BLOCK: felt252 = 'Cannot block'; pub const INVALID_RECIPIENT: felt252 = 'Invalid recipient'; + pub const MAX_BALANCE_EXCEEDED: felt252 = 'Max Balance Exceeded'; } #[derive(Drop, starknet::Event)] #[event] - enum Event { + pub enum Event { TokenEnrollmentInitiated: TokenEnrollmentInitiated, TokenDeactivated: TokenDeactivated, TokenBlocked: TokenBlocked, @@ -117,129 +118,130 @@ pub mod TokenBridge { } #[derive(Drop, starknet::Event)] - struct TokenDeactivated { - token: ContractAddress + pub struct TokenDeactivated { + pub token: ContractAddress } #[derive(Drop, starknet::Event)] - struct TokenBlocked { - token: ContractAddress + pub struct TokenBlocked { + pub token: ContractAddress } #[derive(Drop, starknet::Event)] - struct TokenEnrollmentInitiated { - token: ContractAddress, - deployment_message_hash: MessageHash + pub struct TokenEnrollmentInitiated { + pub token: ContractAddress, + pub deployment_message_hash: MessageHash } #[derive(Drop, starknet::Event)] - struct Deposit { + pub struct Deposit { #[key] - sender: ContractAddress, + pub sender: ContractAddress, #[key] - token: ContractAddress, - amount: u256, + pub token: ContractAddress, + pub amount: u256, #[key] - appchain_recipient: ContractAddress, - nonce: Nonce, + pub appchain_recipient: ContractAddress, + pub nonce: felt252, } #[derive(Drop, starknet::Event)] - struct DepositWithMessage { + pub struct DepositWithMessage { #[key] - sender: ContractAddress, + pub sender: ContractAddress, #[key] - token: ContractAddress, - amount: u256, + pub token: ContractAddress, + pub amount: u256, #[key] - appchain_recipient: ContractAddress, - message: Span, - nonce: Nonce, + pub appchain_recipient: ContractAddress, + pub message: Span, + pub nonce: felt252, } #[derive(Drop, starknet::Event)] struct DepositCancelRequest { #[key] - sender: ContractAddress, + pub sender: ContractAddress, #[key] - token: ContractAddress, - amount: u256, + pub token: ContractAddress, + pub amount: u256, #[key] - appchain_recipient: ContractAddress, - nonce: Nonce, + pub appchain_recipient: ContractAddress, + pub nonce: felt252, } #[derive(Drop, starknet::Event)] struct DepositWithMessageCancelRequest { #[key] - sender: ContractAddress, + pub sender: ContractAddress, #[key] - token: ContractAddress, - amount: u256, + pub token: ContractAddress, + pub amount: u256, #[key] - appchain_recipient: ContractAddress, - message: Span, - nonce: felt252 + pub appchain_recipient: ContractAddress, + pub message: Span, + pub nonce: felt252 } #[derive(Drop, starknet::Event)] - struct DepositReclaimed { + pub struct DepositReclaimed { #[key] - sender: ContractAddress, + pub sender: ContractAddress, #[key] - token: ContractAddress, - amount: u256, + pub token: ContractAddress, + pub amount: u256, #[key] - appchain_recipient: ContractAddress, - nonce: Nonce + pub appchain_recipient: ContractAddress, + pub nonce: felt252 } #[derive(Drop, starknet::Event)] - struct DepositWithMessageReclaimed { + pub struct DepositWithMessageReclaimed { #[key] - sender: ContractAddress, + pub sender: ContractAddress, #[key] - token: ContractAddress, - amount: u256, + pub token: ContractAddress, + pub amount: u256, #[key] - appchain_recipient: ContractAddress, - message: Span, - nonce: Nonce + pub appchain_recipient: ContractAddress, + pub message: Span, + pub nonce: felt252 } #[derive(Drop, starknet::Event)] - struct Withdrawal { + pub struct Withdrawal { #[key] - recipient: ContractAddress, + pub recipient: ContractAddress, #[key] - token: ContractAddress, - amount: u256, + pub token: ContractAddress, + pub amount: u256, } #[derive(Drop, starknet::Event)] - struct WithdrawalLimitEnabled { + pub struct WithdrawalLimitEnabled { #[key] - sender: ContractAddress, + pub sender: ContractAddress, #[key] - token: ContractAddress, + pub token: ContractAddress, } #[derive(Drop, starknet::Event)] - struct WithdrawalLimitDisabled { + pub struct WithdrawalLimitDisabled { #[key] - sender: ContractAddress, + pub sender: ContractAddress, #[key] - token: ContractAddress, + pub token: ContractAddress, } #[derive(Drop, starknet::Event)] - struct SetMaxTotalBalance { + pub struct SetMaxTotalBalance { #[key] - token: ContractAddress, - value: u256 + pub token: ContractAddress, + pub value: u256 } + #[derive(Drop, starknet::Event)] pub struct SetAppchainBridge { pub appchain_bridge: ContractAddress @@ -322,7 +324,10 @@ pub mod TokenBridge { self.is_servicing_token(token); let caller = get_caller_address(); let dispatcher = IERC20Dispatcher { contract_address: token }; - assert(dispatcher.balance_of(caller) == amount, 'Not enough balance'); + + let current_balance: u256 = dispatcher.balance_of(get_contract_address()); + let max_total_balance = self.get_max_total_balance(token); + assert(current_balance + amount < max_total_balance, Errors::MAX_BALANCE_EXCEEDED); dispatcher.transfer_from(caller, get_contract_address(), amount); } } @@ -368,7 +373,7 @@ pub mod TokenBridge { self.ownable.assert_only_owner(); self.appchain_bridge.write(appchain_bridge); - self.emit(SetAppchainBridge { appchain_bridge: appchain_bridge }); + self.emit(SetAppchainBridge { appchain_bridge }); } // @param token The address of the token contract to be deactivated. @@ -460,16 +465,19 @@ pub mod TokenBridge { .sn_to_appchain_messages(deployment_message_hash); assert(nonce.is_non_zero(), Errors::DEPLOYMENT_MESSAGE_DOES_NOT_EXIST); - let token_status = TokenSettings { + // Reading existing settings as withdrawal_limit_applied and max_total_balance + // can be set before the token is enrolled. + + let old_settings = self.token_settings.read(token); + let new_settings = TokenSettings { token_status: TokenStatus::Pending, deployment_message_hash: deployment_message_hash, pending_deployment_expiration: get_block_timestamp() + constants::MAX_PENDING_DURATION.try_into().unwrap(), - max_total_balance: core::integer::BoundedInt::max(), - withdrawal_limit_applied: false + ..old_settings }; - self.token_settings.write(token, token_status); + self.token_settings.write(token, new_settings); self.emit(TokenEnrollmentInitiated { token, deployment_message_hash }); } @@ -516,7 +524,6 @@ pub mod TokenBridge { appchain_recipient: ContractAddress, message: Span ) { - self.reentrancy_guard.start(); self.accept_deposit(token, amount); let nonce = self .send_deposit_message( @@ -576,10 +583,9 @@ pub mod TokenBridge { assert(recipient.is_non_zero(), Errors::INVALID_RECIPIENT); self.consume_message(token, amount, recipient); - let settings = self.token_settings.read(token); - // TODO: Consume quota from here - // DEP(byteZorvin): Complete the withdrawal component in cairo - if (settings.withdrawal_limit_applied) {} + + self.withdrawal.consume_withdrawal_quota(token, amount); + let tokenDispatcher = IERC20Dispatcher { contract_address: token }; tokenDispatcher.transfer(recipient, amount); self.reentrancy_guard.end(); @@ -730,23 +736,12 @@ pub mod TokenBridge { self.token_settings.read(token).token_status == TokenStatus::Active } - // /** - // Returns the remaining amount of withdrawal allowed for this day. - // If the daily allowance was not yet set, it is calculated and returned. - // If the withdraw limit is not enabled for that token - the uint256.max is returned. - // */ - // function getRemainingIntradayAllowance(address token) external view returns (uint256) { - // return - // tokenSettings()[token].withdrawalLimitApplied - // ? WithdrawalLimit.getRemainingIntradayAllowance(token) - // : type(uint256).max; - // } - fn get_remaining_intraday_allowance(self: @ContractState, token: ContractAddress) -> u256 { - if (self.token_settings.read(token).withdrawal_limit_applied) { + fn get_max_total_balance(self: @ContractState, token: ContractAddress) -> u256 { + let max_total_balance = self.token_settings.read(token).max_total_balance; + if (max_total_balance == 0) { return core::integer::BoundedInt::max(); } - // TODO: Write the WithdrawalLimit functionality - return 0; + return max_total_balance; } } diff --git a/src/lib.cairo b/src/lib.cairo index abfba84..7362755 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -2,6 +2,14 @@ pub mod bridge { pub mod token_bridge; pub mod interface; pub mod types; + + pub use token_bridge::TokenBridge; + pub use interface::{ + ITokenBridge, ITokenBridgeAdmin, IWithdrawalLimitStatus, ITokenBridgeDispatcher, + ITokenBridgeAdminDispatcher, IWithdrawalLimitStatusDispatcher, + IWithdrawalLimitStatusDispatcherTrait, ITokenBridgeDispatcherTrait, + ITokenBridgeAdminDispatcherTrait + }; } pub mod withdrawal_limit { diff --git a/src/withdrawal_limit/component.cairo b/src/withdrawal_limit/component.cairo index 7615642..ebf0ab3 100644 --- a/src/withdrawal_limit/component.cairo +++ b/src/withdrawal_limit/component.cairo @@ -1,8 +1,7 @@ #[starknet::component] pub mod WithdrawalLimitComponent { use starknet::{ContractAddress, get_block_timestamp, get_contract_address}; - use starknet_bridge::bridge::interface::IWithdrawalLimitStatus; - use starknet_bridge::constants; + use starknet_bridge::{constants, bridge::IWithdrawalLimitStatus}; use core::integer::BoundedInt; use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; diff --git a/tests/constants.cairo b/tests/constants.cairo new file mode 100644 index 0000000..cb8f018 --- /dev/null +++ b/tests/constants.cairo @@ -0,0 +1,14 @@ +use starknet::{ContractAddress, contract_address_const}; + +pub fn OWNER() -> ContractAddress { + contract_address_const::<'OWNER'>() +} + +pub fn L3_BRIDGE_ADDRESS() -> ContractAddress { + contract_address_const::<'l3_bridge_address'>() +} + + +// 5 days as the delay time (5 * 86400 = 432000) +pub const DELAY_TIME: felt252 = 432000; + diff --git a/tests/lib.cairo b/tests/lib.cairo index 3b7b7da..178f4a1 100644 --- a/tests/lib.cairo +++ b/tests/lib.cairo @@ -1,2 +1,3 @@ -mod token_bridge_test; +pub mod token_bridge_test; +pub mod constants; diff --git a/tests/token_bridge_test.cairo b/tests/token_bridge_test.cairo index 1d6b9d9..f108a04 100644 --- a/tests/token_bridge_test.cairo +++ b/tests/token_bridge_test.cairo @@ -1,65 +1,341 @@ -#[cfg(test)] -mod tests { - use starknet_bridge::bridge::interface::ITokenBridgeDispatcherTrait; - use core::serde::Serde; - use core::result::ResultTrait; - use core::option::OptionTrait; - use core::traits::TryInto; - use snforge_std as snf; - use snforge_std::{ContractClassTrait}; - use starknet::{ContractAddress, storage::StorageMemberAccessTrait}; - use starknet_bridge::mocks::erc20::ERC20; - use starknet_bridge::bridge::interface::{ - ITokenBridgeAdmin, ITokenBridge, ITokenBridgeDispatcher - }; - use starknet::contract_address::{contract_address_const}; - - fn deploy_token_bridge() -> (ITokenBridgeDispatcher, ContractAddress) { - let appchainCH = snf::declare("appchain").unwrap(); - let (appchainContract, _) = appchainCH - .deploy(@array!['owner'.try_into().unwrap(), 0, 0, 0]) - .unwrap(); - let l3_bridge_address = contract_address_const::<'l3_bridge_address'>(); - - let token_bridgeCH = snf::declare("TokenBridge").unwrap(); - - let mut calldata = ArrayTrait::new(); - l3_bridge_address.serialize(ref calldata); - appchainContract.serialize(ref calldata); - let (token_bridge_address, _) = token_bridgeCH.deploy(@calldata).unwrap(); - - (ITokenBridgeDispatcher { contract_address: token_bridge_address }, token_bridge_address) - } - - fn deploy_erc20(name: ByteArray, symbol: ByteArray) -> ContractAddress { - let recipient: felt252 = 'owner'.try_into().unwrap(); - - let erc20Ch = snf::declare("ERC20").unwrap(); - let mut constructor_args = ArrayTrait::new(); - name.serialize(ref constructor_args); - symbol.serialize(ref constructor_args); - let fixed_supply: u256 = 1000000000; - fixed_supply.serialize(ref constructor_args); - recipient.serialize(ref constructor_args); - - let (usdc, _) = erc20Ch.deploy(@constructor_args).unwrap(); - return usdc; - } -// #[test] -// fn constructor_ok() { -// deploy_token_bridge(); -// } -// -// #[test] -// fn test_enroll_token() { -// let (token_bridge, _) = deploy_token_bridge(); -// let usdc = deploy_erc20("USDC", "USDC"); -// let l3_bridge_address = token_bridge.appchain_bridge(); -// assert( -// l3_bridge_address == contract_address_const::<'l3_bridge_address'>(), -// 'L3 Bridge address incorrect' -// ); -// token_bridge.enroll_token(usdc); -// } +use openzeppelin::access::ownable::interface::IOwnableTwoStepDispatcherTrait; +use core::array::ArrayTrait; +use core::serde::Serde; +use core::result::ResultTrait; +use core::option::OptionTrait; +use core::traits::TryInto; +use snforge_std as snf; +use snforge_std::{ContractClassTrait, EventSpy, EventSpyTrait, EventSpyAssertionsTrait}; +use starknet::{ContractAddress, storage::StorageMemberAccessTrait}; +use starknet_bridge::mocks::erc20::ERC20; +use starknet_bridge::bridge::{ + ITokenBridgeDispatcher, ITokenBridgeDispatcherTrait, ITokenBridgeAdminDispatcher, + ITokenBridgeAdminDispatcherTrait, IWithdrawalLimitStatusDispatcher, + IWithdrawalLimitStatusDispatcherTrait, TokenBridge, TokenBridge::Event, + types::{TokenStatus, TokenSettings} +}; +use openzeppelin::access::ownable::{ + OwnableComponent, OwnableComponent::Event as OwnableEvent, + interface::{IOwnableTwoStepDispatcher, IOwnableDispatcherTrait} +}; +use starknet::contract_address::{contract_address_const}; +use super::constants::{OWNER, L3_BRIDGE_ADDRESS, DELAY_TIME}; + + +fn deploy_erc20(name: ByteArray, symbol: ByteArray) -> ContractAddress { + let erc20_class_hash = snf::declare("ERC20").unwrap(); + let mut constructor_args = ArrayTrait::new(); + name.serialize(ref constructor_args); + symbol.serialize(ref constructor_args); + let fixed_supply: u256 = 1000000000; + fixed_supply.serialize(ref constructor_args); + OWNER().serialize(ref constructor_args); + + let (usdc, _) = erc20_class_hash.deploy(@constructor_args).unwrap(); + return usdc; +} + + +fn deploy_token_bridge() -> (ITokenBridgeDispatcher, EventSpy) { + // Deploy messaging mock with 5 days cancellation delay + let messaging_mock_class_hash = snf::declare("messaging_mock").unwrap(); + // Deploying with 5 days as the delay time (5 * 86400 = 432000) + let (messaging_contract_address, _) = messaging_mock_class_hash + .deploy(@array![DELAY_TIME]) + .unwrap(); + + // Declare l3 bridge address + let appchain_bridge_address = L3_BRIDGE_ADDRESS(); + + // Declare owner + let owner = OWNER(); + + let token_bridge_class_hash = snf::declare("TokenBridge").unwrap(); + + // Deploy the bridge + let mut calldata = ArrayTrait::new(); + appchain_bridge_address.serialize(ref calldata); + messaging_contract_address.serialize(ref calldata); + owner.serialize(ref calldata); + + let (token_bridge_address, _) = token_bridge_class_hash.deploy(@calldata).unwrap(); + + let token_bridge = ITokenBridgeDispatcher { contract_address: token_bridge_address }; + let token_bridge_ownable = IOwnableTwoStepDispatcher { contract_address: token_bridge_address }; + + let mut spy = snf::spy_events(); + assert(owner == token_bridge_ownable.owner(), 'Incorrect owner'); + + (token_bridge, spy) +} + + +/// Returns the state of a component for testing. This must be used +/// to test internal functions or directly access the storage. +/// You can't spy event with this. Use deploy instead. +fn mock_state_testing() -> TokenBridge::ContractState { + TokenBridge::contract_state_for_testing() +} + +#[test] +fn constructor_ok() { + deploy_token_bridge(); +} + +#[test] +fn set_appchain_bridge_ok() { + let (token_bridge, mut spy) = deploy_token_bridge(); + let token_bridge_admin = ITokenBridgeAdminDispatcher { + contract_address: token_bridge.contract_address + }; + let token_bridge = ITokenBridgeDispatcher { contract_address: token_bridge.contract_address }; + + // Assert for old bridge address + let old_appchain_bridge_address = token_bridge.appchain_bridge(); + assert(old_appchain_bridge_address == L3_BRIDGE_ADDRESS(), 'L3 Bridge address incorrect'); + + let owner = OWNER(); + // Cheat for the owner + snf::start_cheat_caller_address(token_bridge.contract_address, owner); + + // Set and check new bridge + let new_appchain_bridge_address = contract_address_const::<'l3_bridge_address_new'>(); + token_bridge_admin.set_appchain_token_bridge(new_appchain_bridge_address); + assert( + token_bridge.appchain_bridge() == new_appchain_bridge_address, 'Appchain bridge not set' + ); + snf::stop_cheat_caller_address(token_bridge.contract_address); + + let expected_event = TokenBridge::SetAppchainBridge { + appchain_bridge: new_appchain_bridge_address + }; + spy + .assert_emitted( + @array![(token_bridge.contract_address, Event::SetAppchainBridge(expected_event))] + ); +} + +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn set_appchain_bridge_not_owner() { + let (token_bridge, _) = deploy_token_bridge(); + let token_bridge_admin = ITokenBridgeAdminDispatcher { + contract_address: token_bridge.contract_address + }; + let token_bridge = ITokenBridgeDispatcher { contract_address: token_bridge.contract_address }; + + // Assert for old bridge address + let old_appchain_bridge_address = token_bridge.appchain_bridge(); + assert(old_appchain_bridge_address == L3_BRIDGE_ADDRESS(), 'L3 Bridge address incorrect'); + + // Set and check new bridge + let new_appchain_bridge_address = contract_address_const::<'l3_bridge_address_new'>(); + token_bridge_admin.set_appchain_token_bridge(new_appchain_bridge_address); + assert( + token_bridge.appchain_bridge() == new_appchain_bridge_address, 'Appchain bridge not set' + ); +} + +#[test] +fn enroll_token_ok() { + let (token_bridge, _) = deploy_token_bridge(); + + let usdc_address = deploy_erc20("USDC", "USDC"); + + let old_status = token_bridge.get_status(usdc_address); + assert(old_status == TokenStatus::Unknown, 'Should be unknown before'); + + token_bridge.enroll_token(usdc_address); + + let new_status = token_bridge.get_status(usdc_address); + assert(new_status == TokenStatus::Pending, 'Should be pending now'); +} + +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn set_max_total_balance_not_owner() { + let (token_bridge, _) = deploy_token_bridge(); + let token_bridge_admin = ITokenBridgeAdminDispatcher { + contract_address: token_bridge.contract_address + }; + + let usdc_address = deploy_erc20("USDC", "USDC"); + let decimals = 1000_000; + token_bridge_admin.set_max_total_balance(usdc_address, 50 * decimals); +} + + +#[test] +fn set_max_total_balance_ok() { + let (token_bridge, mut spy) = deploy_token_bridge(); + let token_bridge_admin = ITokenBridgeAdminDispatcher { + contract_address: token_bridge.contract_address + }; + + let usdc_address = deploy_erc20("usdc", "usdc"); + + let owner = OWNER(); + // Cheat for the owner + snf::start_cheat_caller_address(token_bridge.contract_address, owner); + + let decimals = 1000_000; + token_bridge_admin.set_max_total_balance(usdc_address, 50 * decimals); + + snf::stop_cheat_caller_address(token_bridge.contract_address); + + let expected_event = TokenBridge::SetMaxTotalBalance { + token: usdc_address, value: 50 * decimals + }; + + spy + .assert_emitted( + @array![(token_bridge.contract_address, Event::SetMaxTotalBalance(expected_event))] + ); +} + + +#[test] +fn block_token_ok() { + let (token_bridge, mut spy) = deploy_token_bridge(); + let token_bridge_admin = ITokenBridgeAdminDispatcher { + contract_address: token_bridge.contract_address + }; + + let usdc_address = deploy_erc20("usdc", "usdc"); + + let owner = OWNER(); + // Cheat for the owner + snf::start_cheat_caller_address(token_bridge.contract_address, owner); + token_bridge_admin.block_token(usdc_address); + + let expected_event = TokenBridge::TokenBlocked { token: usdc_address }; + spy + .assert_emitted( + @array![(token_bridge.contract_address, Event::TokenBlocked(expected_event))] + ); + + assert(token_bridge.get_status(usdc_address) == TokenStatus::Blocked, 'Should be blocked'); + + snf::stop_cheat_caller_address(token_bridge.contract_address); +} + + +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn block_token_not_owner() { + let (token_bridge, _) = deploy_token_bridge(); + let token_bridge_admin = ITokenBridgeAdminDispatcher { + contract_address: token_bridge.contract_address + }; + + let usdc_address = deploy_erc20("usdc", "usdc"); + + token_bridge_admin.block_token(usdc_address); +} + +#[test] +#[should_panic(expected: ('Cannot block',))] +fn block_token_pending() { + let (token_bridge, _) = deploy_token_bridge(); + let token_bridge_admin = ITokenBridgeAdminDispatcher { + contract_address: token_bridge.contract_address + }; + + let owner = OWNER(); + let usdc_address = deploy_erc20("usdc", "usdc"); + token_bridge.enroll_token(usdc_address); + + snf::start_cheat_caller_address(token_bridge.contract_address, owner); + token_bridge_admin.block_token(usdc_address); +} + +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn enable_withdrawal_limit_not_owner() { + let (token_bridge, _) = deploy_token_bridge(); + let token_bridge_admin = ITokenBridgeAdminDispatcher { + contract_address: token_bridge.contract_address + }; + + let usdc_address = deploy_erc20("USDC", "USDC"); + token_bridge_admin.enable_withdrawal_limit(usdc_address); +} + +#[test] +fn enable_withdrawal_limit_ok() { + let (token_bridge, _) = deploy_token_bridge(); + let token_bridge_admin = ITokenBridgeAdminDispatcher { + contract_address: token_bridge.contract_address + }; + let withdrawal_limit = IWithdrawalLimitStatusDispatcher { + contract_address: token_bridge.contract_address + }; + + let owner = OWNER(); + snf::start_cheat_caller_address(token_bridge.contract_address, owner); + + let usdc_address = deploy_erc20("USDC", "USDC"); + token_bridge_admin.enable_withdrawal_limit(usdc_address); + + snf::stop_cheat_caller_address(token_bridge.contract_address); + + assert(withdrawal_limit.is_withdrawal_limit_applied(usdc_address), 'Limit not applied'); +} + +#[test] +fn disable_withdrwal_limit_ok() { + let (token_bridge, _) = deploy_token_bridge(); + let token_bridge_admin = ITokenBridgeAdminDispatcher { + contract_address: token_bridge.contract_address + }; + let withdrawal_limit = IWithdrawalLimitStatusDispatcher { + contract_address: token_bridge.contract_address + }; + + let owner = OWNER(); + snf::start_cheat_caller_address(token_bridge.contract_address, owner); + + let usdc_address = deploy_erc20("USDC", "USDC"); + token_bridge_admin.enable_withdrawal_limit(usdc_address); + + // Withdrawal limit is now applied + assert(withdrawal_limit.is_withdrawal_limit_applied(usdc_address), 'Limit not applied'); + + token_bridge_admin.disable_withdrawal_limit(usdc_address); + + assert( + withdrawal_limit.is_withdrawal_limit_applied(usdc_address) == false, 'Limit not applied' + ); + snf::stop_cheat_caller_address(token_bridge.contract_address); +} + +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn disable_withdrwal_not_owner() { + let (token_bridge, _) = deploy_token_bridge(); + let token_bridge_admin = ITokenBridgeAdminDispatcher { + contract_address: token_bridge.contract_address + }; + + let withdrawal_limit = IWithdrawalLimitStatusDispatcher { + contract_address: token_bridge.contract_address + }; + + let owner = OWNER(); + snf::start_cheat_caller_address(token_bridge.contract_address, owner); + + let usdc_address = deploy_erc20("USDC", "USDC"); + token_bridge_admin.enable_withdrawal_limit(usdc_address); + + // Withdrawal limit is now applied + assert(withdrawal_limit.is_withdrawal_limit_applied(usdc_address), 'Limit not applied'); + + snf::stop_cheat_caller_address(token_bridge.contract_address); + + token_bridge_admin.disable_withdrawal_limit(usdc_address); + + assert( + withdrawal_limit.is_withdrawal_limit_applied(usdc_address) == false, 'Limit not applied' + ); }