diff --git a/Scarb.toml b/Scarb.toml index 44e30c1..a61e77c 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -15,7 +15,6 @@ snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v [[target.starknet-contract]] casm = true -build-external-contracts = ["piltover::messaging::mock::messaging_mock"] [scripts] test = "snforge test" diff --git a/src/bridge/interface.cairo b/src/bridge/interface.cairo index 65265b3..6be7ac4 100644 --- a/src/bridge/interface.cairo +++ b/src/bridge/interface.cairo @@ -5,7 +5,10 @@ use starknet_bridge::bridge::types::{TokenStatus, TokenSettings}; pub trait ITokenBridgeAdmin { fn set_appchain_token_bridge(ref self: TContractState, appchain_bridge: ContractAddress); fn block_token(ref self: TContractState, token: ContractAddress); + fn unblock_token(ref self: TContractState, token: ContractAddress); fn deactivate_token(ref self: TContractState, token: ContractAddress); + fn reactivate_token(ref self: TContractState, token: ContractAddress); + fn enable_withdrawal_limit(ref self: TContractState, token: ContractAddress); fn disable_withdrawal_limit(ref self: TContractState, token: ContractAddress); fn set_max_total_balance( diff --git a/src/bridge/token_bridge.cairo b/src/bridge/token_bridge.cairo index 7fa8f02..19171d0 100644 --- a/src/bridge/token_bridge.cairo +++ b/src/bridge/token_bridge.cairo @@ -83,8 +83,10 @@ pub mod TokenBridge { pub const ZERO_DEPOSIT: felt252 = 'Zero amount'; pub const ALREADY_ENROLLED: felt252 = 'Already enrolled'; pub const DEPLOYMENT_MESSAGE_DOES_NOT_EXIST: felt252 = 'Deployment message inexistent'; - pub const CANNOT_DEACTIVATE: felt252 = 'Cannot deactivate and block'; - pub const CANNOT_BLOCK: felt252 = 'Cannot block'; + pub const NOT_ACTIVE: felt252 = 'Token not active'; + pub const NOT_DEACTIVATED: felt252 = 'Token not deactivated'; + pub const NOT_BLOCKED: felt252 = 'Token not blocked'; + pub const NOT_UNKNOWN: felt252 = 'Only unknown can be blocked'; pub const INVALID_RECIPIENT: felt252 = 'Invalid recipient'; pub const MAX_BALANCE_EXCEEDED: felt252 = 'Max Balance Exceeded'; } @@ -94,8 +96,11 @@ pub mod TokenBridge { #[event] pub enum Event { TokenEnrollmentInitiated: TokenEnrollmentInitiated, + TokenActivated: TokenActivated, TokenDeactivated: TokenDeactivated, TokenBlocked: TokenBlocked, + TokenReactivated: TokenReactivated, + TokenUnblocked: TokenUnblocked, Deposit: Deposit, DepositWithMessage: DepositWithMessage, DepostiCancelRequest: DepositCancelRequest, @@ -117,6 +122,11 @@ pub mod TokenBridge { ReentrancyGuardEvent: ReentrancyGuardComponent::Event, } + #[derive(Drop, starknet::Event)] + pub struct TokenActivated { + pub token: ContractAddress + } + #[derive(Drop, starknet::Event)] pub struct TokenDeactivated { pub token: ContractAddress @@ -127,6 +137,18 @@ pub mod TokenBridge { pub token: ContractAddress } + + #[derive(Drop, starknet::Event)] + pub struct TokenUnblocked { + pub token: ContractAddress + } + + + #[derive(Drop, starknet::Event)] + pub struct TokenReactivated { + pub token: ContractAddress + } + #[derive(Drop, starknet::Event)] pub struct TokenEnrollmentInitiated { pub token: ContractAddress, @@ -382,7 +404,7 @@ pub mod TokenBridge { // Throws an error if the token is not enrolled or if the sender is not the manager. fn block_token(ref self: ContractState, token: ContractAddress) { self.ownable.assert_only_owner(); - assert(self.get_status(token) == TokenStatus::Unknown, Errors::CANNOT_BLOCK); + assert(self.get_status(token) == TokenStatus::Unknown, Errors::NOT_UNKNOWN); let new_settings = TokenSettings { token_status: TokenStatus::Blocked, ..self.token_settings.read(token) @@ -391,14 +413,22 @@ pub mod TokenBridge { self.emit(TokenBlocked { token }); } + fn unblock_token(ref self: ContractState, token: ContractAddress) { + self.ownable.assert_only_owner(); + assert(self.get_status(token) == TokenStatus::Blocked, Errors::NOT_BLOCKED); + + let new_settings = TokenSettings { + token_status: TokenStatus::Unknown, ..self.token_settings.read(token) + }; + self.token_settings.write(token, new_settings); + self.emit(TokenUnblocked { token }); + } + fn deactivate_token(ref self: ContractState, token: ContractAddress) { self.ownable.assert_only_owner(); let status = self.get_status(token); - assert( - status == TokenStatus::Active || status == TokenStatus::Pending, - Errors::CANNOT_DEACTIVATE - ); + assert(status == TokenStatus::Active, Errors::NOT_ACTIVE); let new_settings = TokenSettings { token_status: TokenStatus::Deactivated, ..self.token_settings.read(token) @@ -406,9 +436,22 @@ pub mod TokenBridge { self.token_settings.write(token, new_settings); self.emit(TokenDeactivated { token }); - self.emit(TokenBlocked { token }); } + fn reactivate_token(ref self: ContractState, token: ContractAddress) { + self.ownable.assert_only_owner(); + let status = self.get_status(token); + assert(status == TokenStatus::Deactivated, Errors::NOT_DEACTIVATED); + + let new_settings = TokenSettings { + token_status: TokenStatus::Active, ..self.token_settings.read(token) + }; + self.token_settings.write(token, new_settings); + + self.emit(TokenReactivated { token }); + } + + fn enable_withdrawal_limit(ref self: ContractState, token: ContractAddress) { self.ownable.assert_only_owner(); let new_settings = TokenSettings { @@ -566,6 +609,7 @@ pub mod TokenBridge { if (nonce.is_zero()) { let new_settings = TokenSettings { token_status: TokenStatus::Active, ..settings }; self.token_settings.write(token, new_settings); + self.emit(TokenActivated { token }); } else if (get_block_timestamp() > settings.pending_deployment_expiration) { let new_settings = TokenSettings { token_status: TokenStatus::Unknown, ..settings }; self.token_settings.write(token, new_settings); diff --git a/src/lib.cairo b/src/lib.cairo index 7362755..35b2563 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -21,5 +21,6 @@ pub mod constants; pub mod mocks { pub mod erc20; + pub mod messaging; } diff --git a/src/mocks/messaging.cairo b/src/mocks/messaging.cairo new file mode 100644 index 0000000..f084d44 --- /dev/null +++ b/src/mocks/messaging.cairo @@ -0,0 +1,45 @@ +use piltover::messaging::output_process::MessageToAppchain; +#[starknet::interface] +pub trait IMockMessaging { + fn update_state_for_message(ref self: TState, message_hash: felt252); +} + +#[starknet::contract] +mod messaging_mock { + use piltover::messaging::{ + output_process::MessageToAppchain, messaging_cpt, + messaging_cpt::InternalTrait as MessagingInternal, IMessaging + }; + use starknet::ContractAddress; + use super::IMockMessaging; + + component!(path: messaging_cpt, storage: messaging, event: MessagingEvent); + + #[abi(embed_v0)] + impl MessagingImpl = messaging_cpt::MessagingImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + messaging: messaging_cpt::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + MessagingEvent: messaging_cpt::Event + } + + #[constructor] + fn constructor(ref self: ContractState, cancellation_delay_secs: u64) { + self.messaging.initialize(cancellation_delay_secs); + } + + #[abi(embed_v0)] + impl MockMessagingImpl of IMockMessaging { + fn update_state_for_message(ref self: ContractState, message_hash: felt252) { + self.messaging.sn_to_appc_messages.write(message_hash, 0); + } + } +} diff --git a/tests/constants.cairo b/tests/constants.cairo index cb8f018..8f56b1c 100644 --- a/tests/constants.cairo +++ b/tests/constants.cairo @@ -8,6 +8,9 @@ pub fn L3_BRIDGE_ADDRESS() -> ContractAddress { contract_address_const::<'l3_bridge_address'>() } +pub fn USDC_MOCK_ADDRESS() -> ContractAddress { + contract_address_const::<'Usdc address'>() +} // 5 days as the delay time (5 * 86400 = 432000) pub const DELAY_TIME: felt252 = 432000; diff --git a/tests/token_bridge_test.cairo b/tests/token_bridge_test.cairo index f108a04..7726ca0 100644 --- a/tests/token_bridge_test.cairo +++ b/tests/token_bridge_test.cairo @@ -1,4 +1,4 @@ -use openzeppelin::access::ownable::interface::IOwnableTwoStepDispatcherTrait; +use starknet_bridge::bridge::token_bridge::TokenBridge::__member_module_token_settings::InternalContractMemberStateTrait; use core::array::ArrayTrait; use core::serde::Serde; use core::result::ResultTrait; @@ -7,19 +7,21 @@ 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::mocks::{ + messaging::{IMockMessagingDispatcherTrait, IMockMessagingDispatcher}, erc20::ERC20 +}; use starknet_bridge::bridge::{ - ITokenBridgeDispatcher, ITokenBridgeDispatcherTrait, ITokenBridgeAdminDispatcher, - ITokenBridgeAdminDispatcherTrait, IWithdrawalLimitStatusDispatcher, + ITokenBridge, ITokenBridgeAdmin, ITokenBridgeDispatcher, ITokenBridgeDispatcherTrait, + ITokenBridgeAdminDispatcher, ITokenBridgeAdminDispatcherTrait, IWithdrawalLimitStatusDispatcher, IWithdrawalLimitStatusDispatcherTrait, TokenBridge, TokenBridge::Event, types::{TokenStatus, TokenSettings} }; use openzeppelin::access::ownable::{ OwnableComponent, OwnableComponent::Event as OwnableEvent, - interface::{IOwnableTwoStepDispatcher, IOwnableDispatcherTrait} + interface::{IOwnableTwoStepDispatcher, IOwnableTwoStepDispatcherTrait} }; use starknet::contract_address::{contract_address_const}; -use super::constants::{OWNER, L3_BRIDGE_ADDRESS, DELAY_TIME}; +use super::constants::{OWNER, L3_BRIDGE_ADDRESS, USDC_MOCK_ADDRESS, DELAY_TIME}; fn deploy_erc20(name: ByteArray, symbol: ByteArray) -> ContractAddress { @@ -35,8 +37,9 @@ fn deploy_erc20(name: ByteArray, symbol: ByteArray) -> ContractAddress { return usdc; } - -fn deploy_token_bridge() -> (ITokenBridgeDispatcher, EventSpy) { +fn deploy_token_bridge_with_messaging() -> ( + ITokenBridgeDispatcher, EventSpy, IMockMessagingDispatcher +) { // 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) @@ -61,16 +64,20 @@ fn deploy_token_bridge() -> (ITokenBridgeDispatcher, EventSpy) { 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 messaging_mock = IMockMessagingDispatcher { contract_address: messaging_contract_address }; let mut spy = snf::spy_events(); - assert(owner == token_bridge_ownable.owner(), 'Incorrect owner'); + (token_bridge, spy, messaging_mock) +} + +fn deploy_token_bridge() -> (ITokenBridgeDispatcher, EventSpy) { + let (token_bridge, spy, _) = deploy_token_bridge_with_messaging(); (token_bridge, spy) } -/// Returns the state of a component for testing. This must be used +/// Returns the state of a contract 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 { @@ -79,7 +86,11 @@ fn mock_state_testing() -> TokenBridge::ContractState { #[test] fn constructor_ok() { - deploy_token_bridge(); + let (token_bridge, _) = deploy_token_bridge(); + let token_bridge_ownable = IOwnableTwoStepDispatcher { + contract_address: token_bridge.contract_address + }; + assert(OWNER() == token_bridge_ownable.owner(), 'Incorrect owner'); } #[test] @@ -196,57 +207,45 @@ fn set_max_total_balance_ok() { #[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 mut mock = mock_state_testing(); + let usdc_address = USDC_MOCK_ADDRESS(); - let owner = OWNER(); - // Cheat for the owner - snf::start_cheat_caller_address(token_bridge.contract_address, owner); - token_bridge_admin.block_token(usdc_address); + mock.ownable.Ownable_owner.write(OWNER()); + snf::cheat_caller_address_global(OWNER()); - 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); + mock.block_token(usdc_address); + assert(mock.get_status(usdc_address) == TokenStatus::Blocked, 'Token not blocked'); } #[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 mut mock = mock_state_testing(); + let usdc_address = USDC_MOCK_ADDRESS(); - let usdc_address = deploy_erc20("usdc", "usdc"); + mock.ownable.Ownable_owner.write(OWNER()); + snf::cheat_caller_address_global(snf::test_address()); - token_bridge_admin.block_token(usdc_address); + mock.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 - }; +#[should_panic(expected: ('Only unknown can be blocked',))] +fn block_token_not_unknown() { + let mut mock = mock_state_testing(); + let usdc_address = USDC_MOCK_ADDRESS(); - let owner = OWNER(); - let usdc_address = deploy_erc20("usdc", "usdc"); - token_bridge.enroll_token(usdc_address); + // Setting the token active + let old_settings = mock.token_settings.read(usdc_address); + mock + .token_settings + .write(usdc_address, TokenSettings { token_status: TokenStatus::Active, ..old_settings }); - snf::start_cheat_caller_address(token_bridge.contract_address, owner); - token_bridge_admin.block_token(usdc_address); + mock.ownable.Ownable_owner.write(OWNER()); + snf::cheat_caller_address_global(OWNER()); + + mock.block_token(usdc_address); } #[test] @@ -339,3 +338,116 @@ fn disable_withdrwal_not_owner() { ); } +#[test] +fn unblock_token_ok() { + let mut mock = mock_state_testing(); + let usdc_address = USDC_MOCK_ADDRESS(); + + // Setting the token active + let old_settings = mock.token_settings.read(usdc_address); + mock + .token_settings + .write(usdc_address, TokenSettings { token_status: TokenStatus::Blocked, ..old_settings }); + + mock.ownable.Ownable_owner.write(OWNER()); + snf::cheat_caller_address_global(OWNER()); + + mock.unblock_token(usdc_address); + assert(mock.get_status(usdc_address) == TokenStatus::Unknown, 'Not unblocked'); +} + +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn unblock_token_not_owner() { + let mut mock = mock_state_testing(); + let usdc_address = USDC_MOCK_ADDRESS(); + + // Setting the token active + let old_settings = mock.token_settings.read(usdc_address); + mock + .token_settings + .write(usdc_address, TokenSettings { token_status: TokenStatus::Blocked, ..old_settings }); + + mock.ownable.Ownable_owner.write(OWNER()); + snf::cheat_caller_address_global(snf::test_address()); + + mock.unblock_token(usdc_address); +} + + +#[test] +#[should_panic(expected: ('Token not blocked',))] +fn unblock_token_not_blocked() { + let mut mock = mock_state_testing(); + let usdc_address = USDC_MOCK_ADDRESS(); + + // Setting the token active + let old_settings = mock.token_settings.read(usdc_address); + mock + .token_settings + .write(usdc_address, TokenSettings { token_status: TokenStatus::Active, ..old_settings }); + + mock.ownable.Ownable_owner.write(OWNER()); + snf::cheat_caller_address_global(OWNER()); + + mock.unblock_token(usdc_address); +} + +#[test] +fn reactivate_token_ok() { + let mut mock = mock_state_testing(); + let usdc_address = USDC_MOCK_ADDRESS(); + + // Setting the token active + let old_settings = mock.token_settings.read(usdc_address); + mock + .token_settings + .write( + usdc_address, TokenSettings { token_status: TokenStatus::Deactivated, ..old_settings } + ); + + mock.ownable.Ownable_owner.write(OWNER()); + + snf::cheat_caller_address_global(OWNER()); + + mock.reactivate_token(usdc_address); + assert(mock.get_status(usdc_address) == TokenStatus::Active, 'Did not reactivate'); +} + +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn reactivate_token_not_owner() { + let mut mock = mock_state_testing(); + let usdc_address = USDC_MOCK_ADDRESS(); + + // Setting the token active + let old_settings = mock.token_settings.read(usdc_address); + mock + .token_settings + .write( + usdc_address, TokenSettings { token_status: TokenStatus::Deactivated, ..old_settings } + ); + + snf::cheat_caller_address_global(snf::test_address()); + + mock.reactivate_token(usdc_address); +} + +#[test] +#[should_panic(expected: ('Token not deactivated',))] +fn reactivate_token_not_deactivated() { + let mut mock = mock_state_testing(); + let usdc_address = USDC_MOCK_ADDRESS(); + + // Setting the token active + let old_settings = mock.token_settings.read(usdc_address); + mock + .token_settings + .write(usdc_address, TokenSettings { token_status: TokenStatus::Blocked, ..old_settings }); + + mock.ownable.Ownable_owner.write(OWNER()); + snf::cheat_caller_address_global(OWNER()); + + mock.reactivate_token(usdc_address); +} +