diff --git a/.tool-versions b/.tool-versions index bdd2d06..fbe9b81 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ scarb 2.6.5 -starknet-foundry 0.26.0 +starknet-foundry 0.27.0 diff --git a/Scarb.lock b/Scarb.lock index 597c2e3..aee7da4 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -16,8 +16,8 @@ dependencies = [ [[package]] name = "snforge_std" -version = "0.26.0" -source = "git+https://github.com/foundry-rs/starknet-foundry?tag=v0.26.0#50eb589db65e113efe4f09241feb59b574228c7e" +version = "0.27.0" +source = "git+https://github.com/foundry-rs/starknet-foundry?tag=v0.27.0#2d99b7c00678ef0363881ee0273550c44a9263de" [[package]] name = "starknet_bridge" diff --git a/Scarb.toml b/Scarb.toml index a61e77c..34967a1 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -11,10 +11,11 @@ starknet = "2.6.4" piltover = { git = "https://github.com/byteZorvin/piltover", branch="bridge-testing"} [dev-dependencies] -snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v0.26.0" } +snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v0.27.0" } [[target.starknet-contract]] casm = true + [scripts] test = "snforge test" diff --git a/src/bridge/tests/messaging_test.cairo b/src/bridge/tests/messaging_test.cairo new file mode 100644 index 0000000..3d65fa3 --- /dev/null +++ b/src/bridge/tests/messaging_test.cairo @@ -0,0 +1,241 @@ +use piltover::messaging::interface::IMessagingDispatcherTrait; +use starknet_bridge::bridge::token_bridge::TokenBridge::{ + __member_module_appchain_bridge::InternalContractMemberStateTrait, + __member_module_token_settings::InternalContractMemberStateTrait as tokenSettingsStateTrait, + TokenBridgeInternal +}; +use snforge_std as snf; +use snforge_std::ContractClassTrait; +use starknet::{ContractAddress, storage::StorageMemberAccessTrait}; +use starknet_bridge::mocks::{ + messaging::{IMockMessagingDispatcherTrait, IMockMessagingDispatcher}, erc20::ERC20, hash +}; +use piltover::messaging::interface::IMessagingDispatcher; +use starknet_bridge::bridge::{ + ITokenBridge, ITokenBridgeAdmin, ITokenBridgeDispatcher, ITokenBridgeDispatcherTrait, + ITokenBridgeAdminDispatcher, ITokenBridgeAdminDispatcherTrait, IWithdrawalLimitStatusDispatcher, + IWithdrawalLimitStatusDispatcherTrait, TokenBridge, TokenBridge::Event, + types::{TokenStatus, TokenSettings}, + tests::constants::{OWNER, L3_BRIDGE_ADDRESS, USDC_MOCK_ADDRESS, DELAY_TIME} +}; +use openzeppelin::{ + token::erc20::interface::{IERC20MetadataDispatcher, IERC20MetadataDispatcherTrait}, + access::ownable::{ + OwnableComponent, OwnableComponent::Event as OwnableEvent, + interface::{IOwnableTwoStepDispatcher, IOwnableTwoStepDispatcherTrait} + } +}; +use starknet_bridge::bridge::tests::utils::message_payloads; +use starknet::contract_address::{contract_address_const}; +use starknet_bridge::constants; + + +/// 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. +pub fn mock_state_testing() -> TokenBridge::ContractState { + TokenBridge::contract_state_for_testing() +} + +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; +} + + +#[test] +fn deploy_message_payload_ok() { + let usdc_address = deploy_erc20("USDC", "USDC"); + let calldata = TokenBridge::deployment_message_payload(usdc_address); + + let expected_calldata: Span = array![ + 3346236667719676623895870229889359551507408296949803518172317961543243553075, // usdc_address + 0, + 1431520323, // -- USDC + 4, + 0, + 1431520323, // USDC + 4, + 18 // Decimals + ] + .span(); + assert(calldata == expected_calldata, 'Incorrect serialisation'); +} + +#[test] +fn deposit_message_payload_with_message_false_ok() { + let usdc_address = USDC_MOCK_ADDRESS(); + let calldata = TokenBridge::deposit_message_payload( + usdc_address, 100, snf::test_address(), false, array![].span() + ); + + let expected_calldata = array![ + 26445726369279219922997965683, 0, 469394814521890341860918960550914, 100, 0 + ] + .span(); + assert(calldata == expected_calldata, 'Incorrect serialization'); +} + + +#[test] +fn send_deploy_message_ok() { + let mut mock = mock_state_testing(); + let usdc_address = deploy_erc20("USDC", "USDC"); + + // 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(); + + let messaging = IMessagingDispatcher { contract_address: messaging_contract_address }; + + snf::start_cheat_caller_address_global(snf::test_address()); + TokenBridge::constructor(ref mock, L3_BRIDGE_ADDRESS(), messaging_contract_address, OWNER()); + + mock.send_deploy_message(usdc_address); + let hash = hash::compute_message_hash_sn_to_appc( + 1, + L3_BRIDGE_ADDRESS(), + constants::HANDLE_TOKEN_DEPLOYMENT_SELECTOR, + message_payloads::deployment_message_payload(usdc_address) + ); + assert(messaging.sn_to_appchain_messages(hash) == 1, 'Message not recieved'); +} + +#[test] +#[should_panic(expected: ('L3 bridge not set',))] +fn send_deploy_message_bridge_unset() { + let mut mock = mock_state_testing(); + let usdc_address = USDC_MOCK_ADDRESS(); + + mock.send_deploy_message(usdc_address); +} + +#[test] +fn send_deposit_message_ok() { + let mut mock = mock_state_testing(); + let usdc_address = USDC_MOCK_ADDRESS(); + + // 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(); + let messaging = IMessagingDispatcher { contract_address: messaging_contract_address }; + TokenBridge::constructor(ref mock, L3_BRIDGE_ADDRESS(), messaging_contract_address, OWNER()); + + let no_message: Span = array![].span(); + snf::start_cheat_caller_address_global(snf::test_address()); + mock + .send_deposit_message( + usdc_address, + 100, + snf::test_address(), + no_message, + constants::HANDLE_TOKEN_DEPOSIT_SELECTOR + ); + + let hash = hash::compute_message_hash_sn_to_appc( + 1, + L3_BRIDGE_ADDRESS(), + constants::HANDLE_TOKEN_DEPOSIT_SELECTOR, + message_payloads::deposit_message_payload( + usdc_address, 100, snf::test_address(), snf::test_address(), false, array![].span() + ) + ); + + assert(messaging.sn_to_appchain_messages(hash) == 1, 'Message not recieved'); +} + +#[test] +#[should_panic(expected: ('L3 bridge not set',))] +fn send_deposit_message_bridge_unset() { + let mut mock = mock_state_testing(); + let usdc_address = USDC_MOCK_ADDRESS(); + + let no_message: Span = array![].span(); + mock + .send_deposit_message( + usdc_address, + 100, + snf::test_address(), + no_message, + constants::HANDLE_TOKEN_DEPOSIT_SELECTOR + ); +} + +#[test] +fn consume_message_ok() { + let mut mock = mock_state_testing(); + let usdc_address = USDC_MOCK_ADDRESS(); + + // 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(); + + TokenBridge::constructor(ref mock, L3_BRIDGE_ADDRESS(), messaging_contract_address, OWNER()); + + let messaging_mock = IMockMessagingDispatcher { contract_address: messaging_contract_address }; + // Register a withdraw message from appchain to piltover + messaging_mock + .process_message_to_starknet( + L3_BRIDGE_ADDRESS(), + snf::test_address(), + message_payloads::withdraw_message_payload_from_appchain( + usdc_address, 100, snf::test_address() + ) + ); + + mock.consume_message(usdc_address, 100, snf::test_address()); +} + +#[test] +#[should_panic(expected: ('INVALID_MESSAGE_TO_CONSUME',))] +fn consume_message_no_message() { + let mut mock = mock_state_testing(); + let usdc_address = USDC_MOCK_ADDRESS(); + + // 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(); + + TokenBridge::constructor(ref mock, L3_BRIDGE_ADDRESS(), messaging_contract_address, OWNER()); + + mock.consume_message(usdc_address, 100, snf::test_address()); +} + +#[test] +#[should_panic(expected: ('L3 bridge not set',))] +fn consume_message_bridge_unset() { + let mut mock = mock_state_testing(); + let usdc_address = USDC_MOCK_ADDRESS(); + + mock.consume_message(usdc_address, 100, snf::test_address()); +} + +#[test] +#[should_panic(expected: ('Invalid recipient',))] +fn consume_message_zero_recipient() { + let mut mock = mock_state_testing(); + let usdc_address = USDC_MOCK_ADDRESS(); + + mock.appchain_bridge.write(L3_BRIDGE_ADDRESS()); + mock.consume_message(usdc_address, 100, contract_address_const::<0>()); +} diff --git a/src/bridge/tests/test_bridge.cairo b/src/bridge/tests/test_bridge.cairo deleted file mode 100644 index fa8bb69..0000000 --- a/src/bridge/tests/test_bridge.cairo +++ /dev/null @@ -1,143 +0,0 @@ -use starknet_bridge::bridge::token_bridge::TokenBridge::__member_module_token_settings::InternalContractMemberStateTrait; -use snforge_std as snf; -use starknet::{ContractAddress, storage::StorageMemberAccessTrait}; -use starknet_bridge::mocks::{ - messaging::{IMockMessagingDispatcherTrait, IMockMessagingDispatcher}, erc20::ERC20 -}; -use starknet_bridge::bridge::{ - 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, IOwnableTwoStepDispatcherTrait} -}; -use starknet::contract_address::{contract_address_const}; - -use starknet_bridge::bridge::tests::constants::{ - OWNER, L3_BRIDGE_ADDRESS, USDC_MOCK_ADDRESS, DELAY_TIME -}; - - -/// 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. -pub fn mock_state_testing() -> TokenBridge::ContractState { - TokenBridge::contract_state_for_testing() -} - -#[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); -} - diff --git a/src/bridge/tests/token_actions_test.cairo b/src/bridge/tests/token_actions_test.cairo index 1895e09..c8d973f 100644 --- a/src/bridge/tests/token_actions_test.cairo +++ b/src/bridge/tests/token_actions_test.cairo @@ -1,9 +1,14 @@ -use starknet_bridge::bridge::token_bridge::TokenBridge::__member_module_token_settings::InternalContractMemberStateTrait; +use piltover::messaging::interface::IMessagingDispatcherTrait; +use starknet_bridge::bridge::token_bridge::TokenBridge::__member_module_appchain_bridge::InternalContractMemberStateTrait; +use starknet_bridge::bridge::token_bridge::TokenBridge::TokenBridgeInternal; +use starknet_bridge::bridge::token_bridge::TokenBridge::__member_module_token_settings::InternalContractMemberStateTrait as tokenSettingsStateTrait; use snforge_std as snf; +use snforge_std::ContractClassTrait; use starknet::{ContractAddress, storage::StorageMemberAccessTrait}; use starknet_bridge::mocks::{ messaging::{IMockMessagingDispatcherTrait, IMockMessagingDispatcher}, erc20::ERC20 }; +use piltover::messaging::interface::IMessagingDispatcher; use starknet_bridge::bridge::{ ITokenBridge, ITokenBridgeAdmin, ITokenBridgeDispatcher, ITokenBridgeDispatcherTrait, ITokenBridgeAdminDispatcher, ITokenBridgeAdminDispatcherTrait, IWithdrawalLimitStatusDispatcher, @@ -14,7 +19,10 @@ use openzeppelin::access::ownable::{ OwnableComponent, OwnableComponent::Event as OwnableEvent, interface::{IOwnableTwoStepDispatcher, IOwnableTwoStepDispatcherTrait} }; +use starknet_bridge::bridge::tests::utils::message_payloads; +use starknet_bridge::mocks::hash; use starknet::contract_address::{contract_address_const}; +use starknet_bridge::constants; use starknet_bridge::bridge::tests::constants::{ OWNER, L3_BRIDGE_ADDRESS, USDC_MOCK_ADDRESS, DELAY_TIME @@ -28,13 +36,71 @@ pub fn mock_state_testing() -> TokenBridge::ContractState { TokenBridge::contract_state_for_testing() } +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; +} + +#[test] +fn deactivate_token_ok() { + let mut mock = mock_state_testing(); + let usdc_address = USDC_MOCK_ADDRESS(); + + mock.ownable.Ownable_owner.write(OWNER()); + snf::start_cheat_caller_address_global(OWNER()); + + // 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.deactivate_token(usdc_address); + assert(mock.get_status(usdc_address) == TokenStatus::Deactivated, 'Token not deactivated'); +} + +#[test] +#[should_panic(expected: ('Token not active',))] +fn deactivate_token_not_active() { + let mut mock = mock_state_testing(); + let usdc_address = USDC_MOCK_ADDRESS(); + + mock.ownable.Ownable_owner.write(OWNER()); + snf::start_cheat_caller_address_global(OWNER()); + + mock.deactivate_token(usdc_address); + assert(mock.get_status(usdc_address) == TokenStatus::Deactivated, 'Token not deactivated'); +} + + +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn deactivate_token_not_owner() { + let mut mock = mock_state_testing(); + let usdc_address = USDC_MOCK_ADDRESS(); + + mock.ownable.Ownable_owner.write(OWNER()); + snf::start_cheat_caller_address_global(snf::test_address()); + + mock.deactivate_token(usdc_address); + assert(mock.get_status(usdc_address) == TokenStatus::Deactivated, 'Token not deactivated'); +} + #[test] fn block_token_ok() { let mut mock = mock_state_testing(); let usdc_address = USDC_MOCK_ADDRESS(); mock.ownable.Ownable_owner.write(OWNER()); - snf::cheat_caller_address_global(OWNER()); + snf::start_cheat_caller_address_global(OWNER()); mock.block_token(usdc_address); assert(mock.get_status(usdc_address) == TokenStatus::Blocked, 'Token not blocked'); @@ -48,7 +114,7 @@ fn block_token_not_owner() { let usdc_address = USDC_MOCK_ADDRESS(); mock.ownable.Ownable_owner.write(OWNER()); - snf::cheat_caller_address_global(snf::test_address()); + snf::start_cheat_caller_address_global(snf::test_address()); mock.block_token(usdc_address); } @@ -66,7 +132,7 @@ fn block_token_not_unknown() { .write(usdc_address, TokenSettings { token_status: TokenStatus::Active, ..old_settings }); mock.ownable.Ownable_owner.write(OWNER()); - snf::cheat_caller_address_global(OWNER()); + snf::start_cheat_caller_address_global(OWNER()); mock.block_token(usdc_address); } @@ -83,7 +149,7 @@ fn unblock_token_ok() { .write(usdc_address, TokenSettings { token_status: TokenStatus::Blocked, ..old_settings }); mock.ownable.Ownable_owner.write(OWNER()); - snf::cheat_caller_address_global(OWNER()); + snf::start_cheat_caller_address_global(OWNER()); mock.unblock_token(usdc_address); assert(mock.get_status(usdc_address) == TokenStatus::Unknown, 'Not unblocked'); @@ -102,7 +168,7 @@ fn unblock_token_not_owner() { .write(usdc_address, TokenSettings { token_status: TokenStatus::Blocked, ..old_settings }); mock.ownable.Ownable_owner.write(OWNER()); - snf::cheat_caller_address_global(snf::test_address()); + snf::start_cheat_caller_address_global(snf::test_address()); mock.unblock_token(usdc_address); } @@ -121,7 +187,7 @@ fn unblock_token_not_blocked() { .write(usdc_address, TokenSettings { token_status: TokenStatus::Active, ..old_settings }); mock.ownable.Ownable_owner.write(OWNER()); - snf::cheat_caller_address_global(OWNER()); + snf::start_cheat_caller_address_global(OWNER()); mock.unblock_token(usdc_address); } @@ -141,7 +207,7 @@ fn reactivate_token_ok() { mock.ownable.Ownable_owner.write(OWNER()); - snf::cheat_caller_address_global(OWNER()); + snf::start_cheat_caller_address_global(OWNER()); mock.reactivate_token(usdc_address); assert(mock.get_status(usdc_address) == TokenStatus::Active, 'Did not reactivate'); @@ -161,7 +227,7 @@ fn reactivate_token_not_owner() { usdc_address, TokenSettings { token_status: TokenStatus::Deactivated, ..old_settings } ); - snf::cheat_caller_address_global(snf::test_address()); + snf::start_cheat_caller_address_global(snf::test_address()); mock.reactivate_token(usdc_address); } @@ -179,8 +245,68 @@ fn reactivate_token_not_deactivated() { .write(usdc_address, TokenSettings { token_status: TokenStatus::Blocked, ..old_settings }); mock.ownable.Ownable_owner.write(OWNER()); - snf::cheat_caller_address_global(OWNER()); + snf::start_cheat_caller_address_global(OWNER()); mock.reactivate_token(usdc_address); } +#[test] +#[should_panic(expected: ('Incorrect token status',))] +fn enroll_token_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::Blocked, ..old_settings }); + + mock.ownable.Ownable_owner.write(OWNER()); + + snf::start_cheat_caller_address_global(OWNER()); + mock.enroll_token(usdc_address); +} + + +#[test] +fn get_status_ok() { + let mut mock = mock_state_testing(); + let usdc_address = USDC_MOCK_ADDRESS(); + + assert(mock.get_status(usdc_address) == TokenStatus::Unknown, 'Incorrect status'); + + // 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 }); + + assert(mock.get_status(usdc_address) == TokenStatus::Active, 'Incorrect status'); + + // Setting the token + let old_settings = mock.token_settings.read(usdc_address); + mock + .token_settings + .write(usdc_address, TokenSettings { token_status: TokenStatus::Blocked, ..old_settings }); + + assert(mock.get_status(usdc_address) == TokenStatus::Blocked, 'Incorrect status'); +} + + +#[test] +fn is_servicing_token_ok() { + let mut mock = mock_state_testing(); + let usdc_address = USDC_MOCK_ADDRESS(); + + assert(mock.is_servicing_token(usdc_address) == false, 'Should not be servicing'); + // 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 }); + + assert(mock.is_servicing_token(usdc_address) == true, 'Should be servicing'); +} + diff --git a/src/bridge/tests/utils/message_payloads.cairo b/src/bridge/tests/utils/message_payloads.cairo new file mode 100644 index 0000000..0c786f2 --- /dev/null +++ b/src/bridge/tests/utils/message_payloads.cairo @@ -0,0 +1,49 @@ +use starknet::ContractAddress; +use starknet_bridge::constants; + +use openzeppelin::token::erc20::interface::{ + IERC20MetadataDispatcher, IERC20MetadataDispatcherTrait +}; + +pub fn deposit_message_payload( + token: ContractAddress, + amount: u256, + caller: ContractAddress, + appchain_recipient: ContractAddress, + is_with_message: bool, + message: Span +) -> Span { + let mut payload = ArrayTrait::new(); + token.serialize(ref payload); + caller.serialize(ref payload); + appchain_recipient.serialize(ref payload); + amount.serialize(ref payload); + if (is_with_message) { + message.serialize(ref payload); + } + + return payload.span(); +} + +pub fn deployment_message_payload(token: ContractAddress) -> Span { + // Create the calldata that will be sent to on_receive. l2_token, amount and + // depositor are the fields from the deposit context. + let mut calldata = ArrayTrait::new(); + let dispatcher = IERC20MetadataDispatcher { contract_address: token }; + token.serialize(ref calldata); + dispatcher.name().serialize(ref calldata); + dispatcher.symbol().serialize(ref calldata); + dispatcher.decimals().serialize(ref calldata); + calldata.span() +} + +pub fn withdraw_message_payload_from_appchain( + token: ContractAddress, amount: u256, recipient: ContractAddress +) -> Span { + let mut message_payload = ArrayTrait::new(); + constants::TRANSFER_FROM_APPCHAIN.serialize(ref message_payload); + recipient.serialize(ref message_payload); + token.serialize(ref message_payload); + amount.serialize(ref message_payload); + message_payload.span() +} diff --git a/src/bridge/token_bridge.cairo b/src/bridge/token_bridge.cairo index 19171d0..546f257 100644 --- a/src/bridge/token_bridge.cairo +++ b/src/bridge/token_bridge.cairo @@ -81,14 +81,16 @@ pub mod TokenBridge { pub mod Errors { pub const APPCHAIN_BRIDGE_NOT_SET: felt252 = 'L3 bridge not set'; pub const ZERO_DEPOSIT: felt252 = 'Zero amount'; - pub const ALREADY_ENROLLED: felt252 = 'Already enrolled'; + pub const ALREADY_ENROLLED: felt252 = 'Incorrect token status'; pub const DEPLOYMENT_MESSAGE_DOES_NOT_EXIST: felt252 = 'Deployment message inexistent'; 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 NOT_SERVICING: felt252 = 'Only servicing tokens'; pub const INVALID_RECIPIENT: felt252 = 'Invalid recipient'; pub const MAX_BALANCE_EXCEEDED: felt252 = 'Max Balance Exceeded'; + pub const TOKENS_NOT_TRANSFERRED: felt252 = 'Tokens not transferred'; } @@ -182,7 +184,7 @@ pub mod TokenBridge { } #[derive(Drop, starknet::Event)] - struct DepositCancelRequest { + pub struct DepositCancelRequest { #[key] pub sender: ContractAddress, #[key] @@ -194,7 +196,7 @@ pub mod TokenBridge { } #[derive(Drop, starknet::Event)] - struct DepositWithMessageCancelRequest { + pub struct DepositWithMessageCancelRequest { #[key] pub sender: ContractAddress, #[key] @@ -271,7 +273,7 @@ pub mod TokenBridge { #[constructor] - fn constructor( + pub fn constructor( ref self: ContractState, appchain_bridge: ContractAddress, messaging_contract: ContractAddress, @@ -286,7 +288,7 @@ pub mod TokenBridge { #[generate_trait] - impl TokenBridgeInternalImpl of TokenBridgeInternal { + pub impl TokenBridgeInternalImpl of TokenBridgeInternal { fn send_deploy_message(self: @ContractState, token: ContractAddress) -> felt252 { assert(self.appchain_bridge().is_non_zero(), Errors::APPCHAIN_BRIDGE_NOT_SET); @@ -323,16 +325,18 @@ pub mod TokenBridge { token, amount, appchain_recipient, is_with_message, message ) ); - return nonce; + nonce } fn consume_message( self: @ContractState, token: ContractAddress, amount: u256, recipient: ContractAddress ) { + assert(recipient.is_non_zero(), Errors::INVALID_RECIPIENT); + let appchain_bridge = self.appchain_bridge(); assert(appchain_bridge.is_non_zero(), Errors::APPCHAIN_BRIDGE_NOT_SET); let mut payload = ArrayTrait::new(); - constants::TRANSFER_FROM_STARKNET.serialize(ref payload); + constants::TRANSFER_FROM_APPCHAIN.serialize(ref payload); recipient.serialize(ref payload); token.serialize(ref payload); amount.serialize(ref payload); @@ -343,19 +347,26 @@ pub mod TokenBridge { } fn accept_deposit(self: @ContractState, token: ContractAddress, amount: u256) { - self.is_servicing_token(token); + assert(self.is_servicing_token(token), Errors::NOT_SERVICING); let caller = get_caller_address(); let dispatcher = IERC20Dispatcher { contract_address: token }; 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); + + let this_address = get_contract_address(); + let initial_balance = dispatcher.balance_of(this_address); + dispatcher.transfer_from(caller, this_address, amount); + assert( + dispatcher.balance_of(this_address) == initial_balance + amount, + Errors::TOKENS_NOT_TRANSFERRED + ); } } - fn deposit_message_payload( + pub fn deposit_message_payload( token: ContractAddress, amount: u256, appchain_recipient: ContractAddress, @@ -376,7 +387,7 @@ pub mod TokenBridge { } - fn deployment_message_payload(token: ContractAddress) -> Span { + pub fn deployment_message_payload(token: ContractAddress) -> Span { // Create the calldata that will be sent to on_receive. l2_token, amount and // depositor are the fields from the deposit context. let mut calldata = ArrayTrait::new(); @@ -543,17 +554,7 @@ pub mod TokenBridge { ); let caller = get_caller_address(); - self - .emit( - DepositWithMessage { - sender: caller, - token, - amount, - appchain_recipient, - message: no_message, - nonce, - } - ); + self.emit(Deposit { sender: caller, token, amount, appchain_recipient, nonce }); self.check_deployment_status(token); self.reentrancy_guard.end(); @@ -624,10 +625,10 @@ pub mod TokenBridge { recipient: ContractAddress ) { self.reentrancy_guard.start(); - assert(recipient.is_non_zero(), Errors::INVALID_RECIPIENT); self.consume_message(token, amount, recipient); + assert(recipient.is_non_zero(), Errors::INVALID_RECIPIENT); self.withdrawal.consume_withdrawal_quota(token, amount); let tokenDispatcher = IERC20Dispatcher { contract_address: token }; diff --git a/src/bridge/types.cairo b/src/bridge/types.cairo index 394608a..8c9ae2d 100644 --- a/src/bridge/types.cairo +++ b/src/bridge/types.cairo @@ -1,6 +1,6 @@ use piltover::messaging::messaging_cpt::{MessageHash, Nonce}; -#[derive(Serde, Drop, starknet::Store, PartialEq)] +#[derive(Serde, Drop, starknet::Store, PartialEq, Display, Debug)] pub enum TokenStatus { #[default] Unknown, diff --git a/src/constants.cairo b/src/constants.cairo index e91f272..60f7449 100644 --- a/src/constants.cairo +++ b/src/constants.cairo @@ -10,7 +10,9 @@ pub const HANDLE_DEPOSIT_WITH_MESSAGE_SELECTOR: felt252 = pub const HANDLE_TOKEN_DEPLOYMENT_SELECTOR: felt252 = 1737780302748468118210503507461757847859991634169290761669750067796330642876; pub const MAX_PENDING_DURATION: felt252 = 5 * 86400; -pub const TRANSFER_FROM_STARKNET: felt252 = 0; + +// Renaming from TRANSFER_FROM_STARKNET to TRANSFER_FROM_APPCHAIN. +pub const TRANSFER_FROM_APPCHAIN: felt252 = 0; // Withdrawal limit pub const SECONDS_IN_DAY: u64 = 86400; diff --git a/src/lib.cairo b/src/lib.cairo index 5ab81a7..b9cac6e 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -6,8 +6,11 @@ pub mod bridge { #[cfg(test)] pub mod tests { pub mod constants; - mod test_bridge; mod token_actions_test; + mod messaging_test; + pub mod utils { + pub mod message_payloads; + } } pub use token_bridge::TokenBridge; @@ -29,5 +32,6 @@ pub mod constants; pub mod mocks { pub mod erc20; pub mod messaging; + pub mod hash; } diff --git a/src/mocks/erc20.cairo b/src/mocks/erc20.cairo index cdd7656..399c75f 100644 --- a/src/mocks/erc20.cairo +++ b/src/mocks/erc20.cairo @@ -56,9 +56,11 @@ pub mod ERC20 { self.erc20.mint(recipient, fixed_supply); } + #[generate_trait] + #[abi(per_item)] impl IERC20Impl of IERC20Trait { - #[abi(per_item)] + #[external(v0)] fn mint(ref self: ContractState, recipient: ContractAddress, amount: u256) { assert(amount < 100 * DECIMALS, 'Max 100 tokens only.'); self.erc20.mint(recipient, amount); diff --git a/src/mocks/hash.cairo b/src/mocks/hash.cairo new file mode 100644 index 0000000..3f9343e --- /dev/null +++ b/src/mocks/hash.cairo @@ -0,0 +1,68 @@ +//! SPDX-License-Identifier: MIT +//! +//! Hash utilities. +use starknet::ContractAddress; + +/// Computes the hash of a message that is sent from Starknet to the Appchain. +/// +/// +/// +/// # Arguments +/// +/// * `nonce` - Nonce of the message. +/// * `to_address` - Contract address to send the message to on the Appchain. +/// * `selector` - The `l1_handler` function selector of the contract on the Appchain +/// to execute. +/// * `payload` - The message payload. +/// +/// # Returns +/// +/// The hash of the message from Starknet to the Appchain. +pub fn compute_message_hash_sn_to_appc( + nonce: felt252, to_address: ContractAddress, selector: felt252, payload: Span +) -> felt252 { + let mut hash_data = array![nonce, to_address.into(), selector,]; + + let mut i = 0_usize; + loop { + if i == payload.len() { + break; + } + hash_data.append((*payload[i])); + i += 1; + }; + + core::poseidon::poseidon_hash_span(hash_data.span()) +} + +/// Computes the hash of a message that is sent from the Appchain to Starknet. +/// +/// +/// +/// # Arguments +/// +/// * `from_address` - Contract address of the message sender on the Appchain. +/// * `to_address` - Contract address to send the message to on the Appchain. +/// * `payload` - The message payload. +/// +/// # Returns +/// +/// The hash of the message from the Appchain to Starknet. +pub fn compute_message_hash_appc_to_sn( + from_address: ContractAddress, to_address: ContractAddress, payload: Span +) -> felt252 { + let mut hash_data: Array = array![ + from_address.into(), to_address.into(), payload.len().into(), + ]; + + let mut i = 0_usize; + loop { + if i == payload.len() { + break; + } + hash_data.append((*payload[i])); + i += 1; + }; + + core::poseidon::poseidon_hash_span(hash_data.span()) +} diff --git a/src/mocks/messaging.cairo b/src/mocks/messaging.cairo index f084d44..b0c8bfa 100644 --- a/src/mocks/messaging.cairo +++ b/src/mocks/messaging.cairo @@ -1,7 +1,14 @@ use piltover::messaging::output_process::MessageToAppchain; +use starknet::ContractAddress; #[starknet::interface] pub trait IMockMessaging { fn update_state_for_message(ref self: TState, message_hash: felt252); + fn process_last_message_to_appchain( + ref self: TState, to_address: ContractAddress, selector: felt252, payload: Span + ); + fn process_message_to_starknet( + ref self: TState, from: ContractAddress, to_address: ContractAddress, payload: Span + ); } #[starknet::contract] @@ -11,6 +18,8 @@ mod messaging_mock { messaging_cpt::InternalTrait as MessagingInternal, IMessaging }; use starknet::ContractAddress; + use starknet_bridge::mocks::hash; + use starknet_bridge::constants; use super::IMockMessaging; component!(path: messaging_cpt, storage: messaging, event: MessagingEvent); @@ -36,10 +45,35 @@ mod messaging_mock { 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); } + + fn process_last_message_to_appchain( + ref self: ContractState, + to_address: ContractAddress, + selector: felt252, + payload: Span + ) { + let nonce = self.messaging.sn_to_appc_nonce.read(); + let message_hash = hash::compute_message_hash_sn_to_appc( + nonce, to_address, selector, payload + ); + self.update_state_for_message(message_hash); + } + + fn process_message_to_starknet( + ref self: ContractState, + from: ContractAddress, + to_address: ContractAddress, + payload: Span + ) { + let message_hash = hash::compute_message_hash_appc_to_sn(from, to_address, payload); + let ref_count = self.messaging.appc_to_sn_messages.read(message_hash); + self.messaging.appc_to_sn_messages.write(message_hash, ref_count + 1); + } } } diff --git a/src/withdrawal_limit/component.cairo b/src/withdrawal_limit/component.cairo index ebf0ab3..e2f600b 100644 --- a/src/withdrawal_limit/component.cairo +++ b/src/withdrawal_limit/component.cairo @@ -83,7 +83,9 @@ pub mod WithdrawalLimitComponent { token: ContractAddress, amount_to_withdraw: u256 ) { - assert(self.get_contract().is_withdrawal_limit_applied(:token), 'LIMIT_NOT_ENABLED'); + if (!self.get_contract().is_withdrawal_limit_applied(:token)) { + return; + } let remaining_withdrawal_quota = self.get_remaining_withdrawal_quota(token); assert(remaining_withdrawal_quota >= amount_to_withdraw, 'LIMIT_EXCEEDED'); diff --git a/tests/deposit_reclaim_test.cairo b/tests/deposit_reclaim_test.cairo new file mode 100644 index 0000000..a5805cf --- /dev/null +++ b/tests/deposit_reclaim_test.cairo @@ -0,0 +1,276 @@ +use core::num::traits::zero::Zero; +use starknet_bridge::bridge::token_bridge::TokenBridge::__member_module_token_settings::InternalContractMemberStateTrait; +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, EventsFilterTrait, EventSpyAssertionsTrait +}; +use starknet::{ContractAddress, storage::StorageMemberAccessTrait}; +use starknet_bridge::mocks::{ + messaging::{IMockMessagingDispatcherTrait, IMockMessagingDispatcher}, erc20::ERC20 +}; +use piltover::messaging::{IMessaging, IMessagingDispatcher, IMessagingDispatcherTrait}; +use starknet_bridge::bridge::{ + 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, IOwnableTwoStepDispatcherTrait} +}; + +use openzeppelin::token::erc20::interface::{IERC20, IERC20Dispatcher, IERC20DispatcherTrait}; +use starknet::contract_address::{contract_address_const}; +use super::constants::{OWNER, L3_BRIDGE_ADDRESS, DELAY_TIME}; +use super::setup::{ + deploy_erc20, deploy_token_bridge_with_messaging, deploy_token_bridge, enroll_token_and_settle +}; + +fn setup() -> (ITokenBridgeDispatcher, EventSpy, ContractAddress, IMockMessagingDispatcher) { + let (token_bridge, mut spy, messaging_mock) = deploy_token_bridge_with_messaging(); + let usdc_address = deploy_erc20("usdc", "usdc"); + enroll_token_and_settle(token_bridge, messaging_mock, usdc_address); + (token_bridge, spy, usdc_address, messaging_mock) +} + +#[test] +fn deposit_reclaim_ok() { + let (token_bridge, mut spy, usdc_address, _) = setup(); + let usdc = IERC20Dispatcher { contract_address: usdc_address }; + + usdc.approve(token_bridge.contract_address, 100); + token_bridge.deposit(usdc_address, 100, snf::test_address()); + + snf::start_cheat_block_timestamp_global(5); + token_bridge.deposit_cancel_request(usdc_address, 100, snf::test_address(), 2); + + snf::start_cheat_block_timestamp_global( + starknet::get_block_timestamp() + DELAY_TIME.try_into().unwrap() + 10 + ); + let initial_user_balance = usdc.balance_of(snf::test_address()); + token_bridge.deposit_reclaim(usdc_address, 100, snf::test_address(), 2); + assert( + usdc.balance_of(snf::test_address()) == initial_user_balance + 100, 'deposit not recieved' + ); + + let expected_deposit_cancel = TokenBridge::DepositCancelRequest { + sender: snf::test_address(), + token: usdc_address, + amount: 100, + appchain_recipient: snf::test_address(), + nonce: 2 + }; + + let expected_deposit_reclaim = TokenBridge::DepositReclaimed { + sender: snf::test_address(), + token: usdc_address, + amount: 100, + appchain_recipient: snf::test_address(), + nonce: 2 + }; + + spy + .assert_emitted( + @array![ + ( + token_bridge.contract_address, + Event::DepostiCancelRequest(expected_deposit_cancel) + ), + (token_bridge.contract_address, Event::DepositReclaimed(expected_deposit_reclaim)) + ] + ); +} + +#[test] +#[should_panic(expected: ('CANCELLATION_NOT_ALLOWED_YET',))] +fn deposit_reclaim_delay_not_reached() { + let (token_bridge, _, usdc_address, _) = setup(); + let usdc = IERC20Dispatcher { contract_address: usdc_address }; + + usdc.approve(token_bridge.contract_address, 100); + token_bridge.deposit(usdc_address, 100, snf::test_address()); + + snf::start_cheat_block_timestamp_global(5); + token_bridge.deposit_cancel_request(usdc_address, 100, snf::test_address(), 2); + + token_bridge.deposit_reclaim(usdc_address, 100, snf::test_address(), 2); +} + + +#[test] +#[should_panic(expected: ('CANCELLATION_NOT_REQUESTED',))] +fn deposit_reclaim_not_cancelled() { + let (token_bridge, _, usdc_address, _) = setup(); + let usdc = IERC20Dispatcher { contract_address: usdc_address }; + + usdc.approve(token_bridge.contract_address, 100); + token_bridge.deposit(usdc_address, 100, snf::test_address()); + + snf::start_cheat_block_timestamp_global( + starknet::get_block_timestamp() + DELAY_TIME.try_into().unwrap() + 10 + ); + + token_bridge.deposit_reclaim(usdc_address, 100, snf::test_address(), 2); +} + +#[test] +#[should_panic(expected: ('NO_MESSAGE_TO_CANCEL',))] +fn deposit_reclaim_different_user() { + let (token_bridge, _, usdc_address, _) = setup(); + let usdc = IERC20Dispatcher { contract_address: usdc_address }; + + usdc.approve(token_bridge.contract_address, 100); + token_bridge.deposit(usdc_address, 100, snf::test_address()); + + snf::start_cheat_block_timestamp_global(5); + token_bridge.deposit_cancel_request(usdc_address, 100, snf::test_address(), 2); + + snf::start_cheat_block_timestamp_global( + starknet::get_block_timestamp() + DELAY_TIME.try_into().unwrap() + 10 + ); + + snf::start_cheat_caller_address_global(contract_address_const::<'user2'>()); + token_bridge.deposit_reclaim(usdc_address, 100, snf::test_address(), 2); +} + + +#[test] +fn deposit_with_message_reclaim_ok() { + let (token_bridge, mut spy, usdc_address, _) = setup(); + let usdc = IERC20Dispatcher { contract_address: usdc_address }; + + let mut calldata = ArrayTrait::new(); + 'param1'.serialize(ref calldata); + 'param2'.serialize(ref calldata); + + usdc.approve(token_bridge.contract_address, 100); + token_bridge.deposit_with_message(usdc_address, 100, snf::test_address(), calldata.span()); + + snf::start_cheat_block_timestamp_global(5); + token_bridge + .deposit_with_message_cancel_request( + usdc_address, 100, snf::test_address(), calldata.span(), 2 + ); + + snf::start_cheat_block_timestamp_global( + starknet::get_block_timestamp() + DELAY_TIME.try_into().unwrap() + 10 + ); + + let initial_user_balance = usdc.balance_of(snf::test_address()); + token_bridge + .deposit_with_message_reclaim(usdc_address, 100, snf::test_address(), calldata.span(), 2); + assert( + usdc.balance_of(snf::test_address()) == initial_user_balance + 100, 'deposit not recieved' + ); + + let expected_deposit_cancel = TokenBridge::DepositWithMessageCancelRequest { + sender: snf::test_address(), + token: usdc_address, + amount: 100, + appchain_recipient: snf::test_address(), + message: calldata.span(), + nonce: 2 + }; + + let expected_deposit_reclaim = TokenBridge::DepositWithMessageReclaimed { + sender: snf::test_address(), + token: usdc_address, + amount: 100, + appchain_recipient: snf::test_address(), + message: calldata.span(), + nonce: 2 + }; + + spy + .assert_emitted( + @array![ + ( + token_bridge.contract_address, + Event::DepositWithMessageCancelRequest(expected_deposit_cancel) + ), + ( + token_bridge.contract_address, + Event::DepositWithMessageReclaimed(expected_deposit_reclaim) + ) + ] + ); +} + +#[test] +#[should_panic(expected: ('CANCELLATION_NOT_ALLOWED_YET',))] +fn deposit_with_message_reclaim_delay_not_reached() { + let (token_bridge, _, usdc_address, _) = setup(); + let usdc = IERC20Dispatcher { contract_address: usdc_address }; + + let mut calldata = ArrayTrait::new(); + 'param1'.serialize(ref calldata); + 'param2'.serialize(ref calldata); + + usdc.approve(token_bridge.contract_address, 100); + token_bridge.deposit_with_message(usdc_address, 100, snf::test_address(), calldata.span()); + + snf::start_cheat_block_timestamp_global(5); + token_bridge + .deposit_with_message_cancel_request( + usdc_address, 100, snf::test_address(), calldata.span(), 2 + ); + + token_bridge + .deposit_with_message_reclaim(usdc_address, 100, snf::test_address(), calldata.span(), 2); +} + + +#[test] +#[should_panic(expected: ('CANCELLATION_NOT_REQUESTED',))] +fn deposit_wtih_message_reclaim_not_cancelled() { + let (token_bridge, _, usdc_address, _) = setup(); + let usdc = IERC20Dispatcher { contract_address: usdc_address }; + + let mut calldata = ArrayTrait::new(); + 'param1'.serialize(ref calldata); + 'param2'.serialize(ref calldata); + + usdc.approve(token_bridge.contract_address, 100); + token_bridge.deposit_with_message(usdc_address, 100, snf::test_address(), calldata.span()); + + snf::start_cheat_block_timestamp_global( + starknet::get_block_timestamp() + DELAY_TIME.try_into().unwrap() + 10 + ); + + token_bridge + .deposit_with_message_reclaim(usdc_address, 100, snf::test_address(), calldata.span(), 2); +} + +#[test] +#[should_panic(expected: ('NO_MESSAGE_TO_CANCEL',))] +fn deposit_reclaim_with_message_different_user() { + let (token_bridge, _, usdc_address, _) = setup(); + let usdc = IERC20Dispatcher { contract_address: usdc_address }; + + let mut calldata = ArrayTrait::new(); + 'param1'.serialize(ref calldata); + 'param2'.serialize(ref calldata); + + usdc.approve(token_bridge.contract_address, 100); + token_bridge.deposit_with_message(usdc_address, 100, snf::test_address(), calldata.span()); + + snf::start_cheat_block_timestamp_global(5); + token_bridge + .deposit_with_message_cancel_request( + usdc_address, 100, snf::test_address(), calldata.span(), 2 + ); + + snf::start_cheat_block_timestamp_global( + starknet::get_block_timestamp() + DELAY_TIME.try_into().unwrap() + 10 + ); + + snf::start_cheat_caller_address_global(contract_address_const::<'user2'>()); + token_bridge + .deposit_with_message_reclaim(usdc_address, 100, snf::test_address(), calldata.span(), 2); +} diff --git a/tests/deposit_test.cairo b/tests/deposit_test.cairo new file mode 100644 index 0000000..26ba426 --- /dev/null +++ b/tests/deposit_test.cairo @@ -0,0 +1,348 @@ +use core::num::traits::zero::Zero; +use starknet_bridge::bridge::token_bridge::TokenBridge::__member_module_token_settings::InternalContractMemberStateTrait; +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, EventsFilterTrait, EventSpyAssertionsTrait +}; +use starknet::{ContractAddress, storage::StorageMemberAccessTrait}; +use starknet_bridge::mocks::{ + messaging::{IMockMessagingDispatcherTrait, IMockMessagingDispatcher}, erc20::ERC20 +}; +use piltover::messaging::{IMessaging, IMessagingDispatcher, IMessagingDispatcherTrait}; +use starknet_bridge::bridge::{ + 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, IOwnableTwoStepDispatcherTrait} +}; + +use openzeppelin::token::erc20::interface::{IERC20, IERC20Dispatcher, IERC20DispatcherTrait}; +use starknet::contract_address::{contract_address_const}; +use super::constants::{OWNER, L3_BRIDGE_ADDRESS, DELAY_TIME}; +use super::setup::{ + deploy_erc20, deploy_token_bridge_with_messaging, deploy_token_bridge, enroll_token_and_settle +}; + +fn setup() -> (ITokenBridgeDispatcher, EventSpy, ContractAddress, IMockMessagingDispatcher) { + let (token_bridge, mut spy, messaging_mock) = deploy_token_bridge_with_messaging(); + let usdc_address = deploy_erc20("usdc", "usdc"); + enroll_token_and_settle(token_bridge, messaging_mock, usdc_address); + (token_bridge, spy, usdc_address, messaging_mock) +} + +#[test] +fn deposit_ok() { + let (token_bridge, mut spy, usdc_address, _) = setup(); + + let usdc = IERC20Dispatcher { contract_address: usdc_address }; + let initial_bridge_balance = usdc.balance_of(token_bridge.contract_address); + usdc.approve(token_bridge.contract_address, 100); + token_bridge.deposit(usdc_address, 100, snf::test_address()); + + assert( + usdc.balance_of(token_bridge.contract_address) == initial_bridge_balance + 100, + 'incorrect amount recieved' + ); + + let expected_deposit = TokenBridge::Deposit { + sender: snf::test_address(), + token: usdc_address, + amount: 100, + appchain_recipient: snf::test_address(), + nonce: 2 + }; + + spy.assert_emitted(@array![(token_bridge.contract_address, Event::Deposit(expected_deposit))]); +} + + +#[test] +#[should_panic(expected: ('ERC20: insufficient balance',))] +fn deposit_insufficient_balance() { + let (token_bridge, _, usdc_address, _) = setup(); + let usdc = IERC20Dispatcher { contract_address: usdc_address }; + + usdc.approve(token_bridge.contract_address, 200); + token_bridge.deposit(usdc_address, 200, snf::test_address()); +} + +#[test] +#[should_panic(expected: ('ERC20: insufficient allowance',))] +fn deposit_insufficient_allowance() { + let (token_bridge, _, usdc_address, _) = setup(); + token_bridge.deposit(usdc_address, 100, snf::test_address()); +} + + +#[test] +#[should_panic(expected: ('Only servicing tokens',))] +fn deposit_deactivated() { + let (token_bridge, _, usdc_address, _) = setup(); + let token_bridge_admin = ITokenBridgeAdminDispatcher { + contract_address: token_bridge.contract_address + }; + + snf::start_cheat_caller_address(token_bridge.contract_address, OWNER()); + token_bridge_admin.deactivate_token(usdc_address); + snf::stop_cheat_caller_address(OWNER()); + + token_bridge.deposit(usdc_address, 100, snf::test_address()); +} + + +#[test] +fn deposit_with_message_ok() { + let (token_bridge, mut spy, usdc_address, _) = setup(); + + let usdc = IERC20Dispatcher { contract_address: usdc_address }; + let mut calldata = ArrayTrait::new(); + 'param1'.serialize(ref calldata); + 'param2'.serialize(ref calldata); + + let initial_bridge_balance = usdc.balance_of(token_bridge.contract_address); + usdc.approve(token_bridge.contract_address, 100); + token_bridge.deposit_with_message(usdc_address, 100, snf::test_address(), calldata.span()); + + assert( + usdc.balance_of(token_bridge.contract_address) == initial_bridge_balance + 100, + 'incorrect amount recieved' + ); + + let expected_deposit_with_message = TokenBridge::DepositWithMessage { + sender: snf::test_address(), + token: usdc_address, + amount: 100, + appchain_recipient: snf::test_address(), + message: calldata.span(), + nonce: 2 + }; + + spy + .assert_emitted( + @array![ + ( + token_bridge.contract_address, + Event::DepositWithMessage(expected_deposit_with_message) + ) + ] + ); +} + +#[test] +fn deposit_with_message_empty_message_ok() { + let (token_bridge, mut spy, usdc_address, _) = setup(); + let usdc = IERC20Dispatcher { contract_address: usdc_address }; + + let mut calldata = ArrayTrait::new(); + 'param1'.serialize(ref calldata); + 'param2'.serialize(ref calldata); + + let initial_bridge_balance = usdc.balance_of(token_bridge.contract_address); + usdc.approve(token_bridge.contract_address, 100); + token_bridge.deposit_with_message(usdc_address, 100, snf::test_address(), calldata.span()); + + assert( + usdc.balance_of(token_bridge.contract_address) == initial_bridge_balance + 100, + 'incorrect amount recieved' + ); + + let expected_deposit_with_message = TokenBridge::DepositWithMessage { + sender: snf::test_address(), + token: usdc_address, + amount: 100, + appchain_recipient: snf::test_address(), + message: calldata.span(), + nonce: 2 + }; + + spy + .assert_emitted( + @array![ + ( + token_bridge.contract_address, + Event::DepositWithMessage(expected_deposit_with_message) + ) + ] + ); +} + +#[test] +#[should_panic(expected: ('ERC20: insufficient balance',))] +fn deposit_with_message_insufficient_balance() { + let (token_bridge, _, usdc_address, _) = setup(); + + let usdc = IERC20Dispatcher { contract_address: usdc_address }; + usdc.approve(token_bridge.contract_address, 200); + + let mut calldata = ArrayTrait::new(); + 'param1'.serialize(ref calldata); + 'param2'.serialize(ref calldata); + token_bridge.deposit_with_message(usdc_address, 200, snf::test_address(), calldata.span()); +} + +#[test] +#[should_panic(expected: ('ERC20: insufficient allowance',))] +fn deposit_with_message_insufficient_allowance() { + let (token_bridge, _, usdc_address, _) = setup(); + let mut calldata = ArrayTrait::new(); + 'param1'.serialize(ref calldata); + 'param2'.serialize(ref calldata); + token_bridge.deposit_with_message(usdc_address, 100, snf::test_address(), calldata.span()); +} + +#[test] +#[should_panic(expected: ('Only servicing tokens',))] +fn deposit_with_message_deactivated() { + let (token_bridge, _, usdc_address, _) = setup(); + let token_bridge_admin = ITokenBridgeAdminDispatcher { + contract_address: token_bridge.contract_address + }; + + snf::start_cheat_caller_address(token_bridge.contract_address, OWNER()); + token_bridge_admin.deactivate_token(usdc_address); + snf::stop_cheat_caller_address(OWNER()); + + let mut calldata = ArrayTrait::new(); + 'param1'.serialize(ref calldata); + 'param2'.serialize(ref calldata); + token_bridge.deposit_with_message(usdc_address, 100, snf::test_address(), calldata.span()); +} + + +#[test] +fn deposit_cancel_request_ok() { + let (token_bridge, mut spy, usdc_address, _) = setup(); + let usdc = IERC20Dispatcher { contract_address: usdc_address }; + + usdc.approve(token_bridge.contract_address, 100); + token_bridge.deposit(usdc_address, 100, snf::test_address()); + + token_bridge.deposit_cancel_request(usdc_address, 100, snf::test_address(), 2); + + let expected_deposit_cancel = TokenBridge::DepositCancelRequest { + sender: snf::test_address(), + token: usdc_address, + amount: 100, + appchain_recipient: snf::test_address(), + nonce: 2 + }; + + spy + .assert_emitted( + @array![ + ( + token_bridge.contract_address, + Event::DepostiCancelRequest(expected_deposit_cancel) + ) + ] + ); +} + + +#[test] +#[should_panic(expected: ('NO_MESSAGE_TO_CANCEL',))] +fn deposit_cancel_request_no_deposit() { + let (token_bridge, _, usdc_address, _) = setup(); + token_bridge.deposit_cancel_request(usdc_address, 100, snf::test_address(), 2); +} + +#[test] +#[should_panic(expected: ('NO_MESSAGE_TO_CANCEL',))] +fn deposit_cancel_request_different_user() { + let (token_bridge, _, usdc_address, _) = setup(); + let usdc = IERC20Dispatcher { contract_address: usdc_address }; + + usdc.approve(token_bridge.contract_address, 100); + token_bridge.deposit(usdc_address, 100, snf::test_address()); + + snf::start_cheat_caller_address( + token_bridge.contract_address, contract_address_const::<'user2'>() + ); + token_bridge.deposit_cancel_request(usdc_address, 100, snf::test_address(), 2); +} + + +#[test] +fn deposit_with_message_cancel_request_ok() { + let (token_bridge, mut spy, usdc_address, _) = setup(); + let usdc = IERC20Dispatcher { contract_address: usdc_address }; + + let mut calldata = ArrayTrait::new(); + 'param1'.serialize(ref calldata); + 'param2'.serialize(ref calldata); + + usdc.approve(token_bridge.contract_address, 100); + token_bridge.deposit_with_message(usdc_address, 100, snf::test_address(), calldata.span()); + + token_bridge + .deposit_with_message_cancel_request( + usdc_address, 100, snf::test_address(), calldata.span(), 2 + ); + + let expected_deposit_cancel = TokenBridge::DepositWithMessageCancelRequest { + sender: snf::test_address(), + token: usdc_address, + amount: 100, + appchain_recipient: snf::test_address(), + message: calldata.span(), + nonce: 2 + }; + + spy + .assert_emitted( + @array![ + ( + token_bridge.contract_address, + Event::DepositWithMessageCancelRequest(expected_deposit_cancel) + ) + ] + ); +} + + +#[test] +#[should_panic(expected: ('NO_MESSAGE_TO_CANCEL',))] +fn deposit_with_message_cancel_request_no_deposit() { + let (token_bridge, _, usdc_address, _) = setup(); + + let mut calldata = ArrayTrait::new(); + 'param1'.serialize(ref calldata); + 'param2'.serialize(ref calldata); + + token_bridge + .deposit_with_message_cancel_request( + usdc_address, 100, snf::test_address(), calldata.span(), 2 + ); +} + +#[test] +#[should_panic(expected: ('NO_MESSAGE_TO_CANCEL',))] +fn deposit_with_message_cancel_request_different_user() { + let (token_bridge, _, usdc_address, _) = setup(); + let usdc = IERC20Dispatcher { contract_address: usdc_address }; + + let mut calldata = ArrayTrait::new(); + 'param1'.serialize(ref calldata); + 'param2'.serialize(ref calldata); + + usdc.approve(token_bridge.contract_address, 100); + token_bridge.deposit_with_message(usdc_address, 100, snf::test_address(), calldata.span()); + + snf::start_cheat_caller_address( + token_bridge.contract_address, contract_address_const::<'user2'>() + ); + token_bridge + .deposit_with_message_cancel_request( + usdc_address, 100, snf::test_address(), calldata.span(), 2 + ); +} + diff --git a/tests/enroll_token_test.cairo b/tests/enroll_token_test.cairo new file mode 100644 index 0000000..90b7a06 --- /dev/null +++ b/tests/enroll_token_test.cairo @@ -0,0 +1,73 @@ +use starknet_bridge::bridge::token_bridge::TokenBridge::__member_module_token_settings::InternalContractMemberStateTrait; +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::{ + messaging::{IMockMessagingDispatcherTrait, IMockMessagingDispatcher}, erc20::ERC20, hash +}; +use starknet_bridge::bridge::{ + 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, IOwnableTwoStepDispatcherTrait} +}; +use starknet::contract_address::{contract_address_const}; +use super::constants::{OWNER, L3_BRIDGE_ADDRESS, USDC_MOCK_ADDRESS, DELAY_TIME}; +use super::setup::{deploy_erc20, deploy_token_bridge}; +use starknet_bridge::bridge::tests::utils::message_payloads; +use starknet_bridge::constants; + + +#[test] +fn enroll_token_ok() { + let (token_bridge, mut spy) = 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 payload = message_payloads::deployment_message_payload(usdc_address); + let message_hash = hash::compute_message_hash_sn_to_appc( + 1, L3_BRIDGE_ADDRESS(), constants::HANDLE_TOKEN_DEPLOYMENT_SELECTOR, payload + ); + + let expected_event = TokenBridge::TokenEnrollmentInitiated { + token: usdc_address, deployment_message_hash: message_hash + }; + + let new_status = token_bridge.get_status(usdc_address); + assert(new_status == TokenStatus::Pending, 'Should be pending now'); + spy + .assert_emitted( + @array![ + (token_bridge.contract_address, Event::TokenEnrollmentInitiated(expected_event)) + ] + ); +} + +#[test] +#[should_panic(expected: ('Incorrect token status',))] +fn enroll_token_already_enrolled() { + let (token_bridge, _) = deploy_token_bridge(); + + let usdc_address = deploy_erc20("USDC", "USDC"); + token_bridge.enroll_token(usdc_address); + + let new_status = token_bridge.get_status(usdc_address); + assert(new_status == TokenStatus::Pending, 'Should be pending now'); + + token_bridge.enroll_token(usdc_address); +} + diff --git a/tests/lib.cairo b/tests/lib.cairo index 212f2d0..6c2b38c 100644 --- a/tests/lib.cairo +++ b/tests/lib.cairo @@ -1,5 +1,9 @@ -pub mod token_bridge_test; +mod token_bridge_test; use starknet_bridge::bridge::tests::constants; -pub mod setup; -pub mod token_actions_test; +mod setup; +mod withdrawal_limit_bridge_test; +mod enroll_token_test; +mod deposit_test; +mod withdraw_test; +mod deposit_reclaim_test; diff --git a/tests/setup.cairo b/tests/setup.cairo index fefa873..8ccaede 100644 --- a/tests/setup.cairo +++ b/tests/setup.cairo @@ -1,5 +1,7 @@ use snforge_std as snf; -use snforge_std::{ContractClassTrait, EventSpy, EventSpyTrait, EventSpyAssertionsTrait}; +use snforge_std::{ + ContractClassTrait, EventSpy, EventSpyTrait, EventsFilterTrait, EventSpyAssertionsTrait +}; use starknet::{ContractAddress, storage::StorageMemberAccessTrait}; use starknet_bridge::mocks::{ messaging::{IMockMessagingDispatcherTrait, IMockMessagingDispatcher}, erc20::ERC20 @@ -14,8 +16,12 @@ use openzeppelin::access::ownable::{ OwnableComponent, OwnableComponent::Event as OwnableEvent, interface::{IOwnableTwoStepDispatcher, IOwnableTwoStepDispatcherTrait} }; +use openzeppelin::token::erc20::interface::{IERC20, IERC20Dispatcher, IERC20DispatcherTrait}; use starknet::contract_address::{contract_address_const}; use super::constants::{OWNER, L3_BRIDGE_ADDRESS, DELAY_TIME}; +use starknet_bridge::constants; +use starknet_bridge::bridge::tests::utils::message_payloads; + pub fn deploy_erc20(name: ByteArray, symbol: ByteArray) -> ContractAddress { let erc20_class_hash = snf::declare("ERC20").unwrap(); @@ -27,6 +33,14 @@ pub fn deploy_erc20(name: ByteArray, symbol: ByteArray) -> ContractAddress { OWNER().serialize(ref constructor_args); let (usdc, _) = erc20_class_hash.deploy(@constructor_args).unwrap(); + + let usdc_token = IERC20Dispatcher { contract_address: usdc }; + + // Transfering usdc to test address for testing + snf::start_cheat_caller_address(usdc, OWNER()); + usdc_token.transfer(snf::test_address(), 100); + snf::stop_cheat_caller_address(usdc); + return usdc; } @@ -76,3 +90,27 @@ pub fn deploy_token_bridge() -> (ITokenBridgeDispatcher, EventSpy) { pub fn mock_state_testing() -> TokenBridge::ContractState { TokenBridge::contract_state_for_testing() } + +pub fn enroll_token_and_settle( + token_bridge: ITokenBridgeDispatcher, + messaging_mock: IMockMessagingDispatcher, + token: ContractAddress +) { + assert(token_bridge.get_status(token) == TokenStatus::Unknown, 'Should be Unknown'); + + token_bridge.enroll_token(token); + assert(token_bridge.get_status(token) == TokenStatus::Pending, 'Should be Pending'); + + // Settles the message sent to appchain + messaging_mock + .process_last_message_to_appchain( + L3_BRIDGE_ADDRESS(), + constants::HANDLE_TOKEN_DEPLOYMENT_SELECTOR, + message_payloads::deployment_message_payload(token) + ); + + token_bridge.check_deployment_status(token); + + let final_status = token_bridge.get_status(token); + assert(final_status == TokenStatus::Active, 'Should be Active'); +} diff --git a/tests/token_bridge_test.cairo b/tests/token_bridge_test.cairo index 14a2765..545c960 100644 --- a/tests/token_bridge_test.cairo +++ b/tests/token_bridge_test.cairo @@ -21,7 +21,7 @@ use openzeppelin::access::ownable::{ interface::{IOwnableTwoStepDispatcher, IOwnableTwoStepDispatcherTrait} }; use starknet::contract_address::{contract_address_const}; -use super::setup::{deploy_erc20, deploy_token_bridge, mock_state_testing}; +use super::setup::{deploy_erc20, deploy_token_bridge}; use super::constants::{OWNER, L3_BRIDGE_ADDRESS, USDC_MOCK_ADDRESS, DELAY_TIME}; @@ -83,25 +83,8 @@ fn set_appchain_bridge_not_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' - ); } -#[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',))] @@ -111,7 +94,7 @@ fn set_max_total_balance_not_owner() { contract_address: token_bridge.contract_address }; - let usdc_address = deploy_erc20("USDC", "USDC"); + let usdc_address = USDC_MOCK_ADDRESS(); let decimals = 1000_000; token_bridge_admin.set_max_total_balance(usdc_address, 50 * decimals); } @@ -124,7 +107,7 @@ fn set_max_total_balance_ok() { contract_address: token_bridge.contract_address }; - let usdc_address = deploy_erc20("usdc", "usdc"); + let usdc_address = USDC_MOCK_ADDRESS(); let owner = OWNER(); // Cheat for the owner diff --git a/tests/withdraw_test.cairo b/tests/withdraw_test.cairo new file mode 100644 index 0000000..5a248a1 --- /dev/null +++ b/tests/withdraw_test.cairo @@ -0,0 +1,142 @@ +use core::num::traits::zero::Zero; +use starknet_bridge::bridge::token_bridge::TokenBridge::__member_module_token_settings::InternalContractMemberStateTrait; +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, EventsFilterTrait, EventSpyAssertionsTrait +}; +use starknet::{ContractAddress, storage::StorageMemberAccessTrait}; +use starknet_bridge::mocks::{ + messaging::{IMockMessagingDispatcherTrait, IMockMessagingDispatcher}, erc20::ERC20 +}; +use piltover::messaging::{IMessaging, IMessagingDispatcher, IMessagingDispatcherTrait}; +use starknet_bridge::bridge::{ + 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, IOwnableTwoStepDispatcherTrait} +}; + +use openzeppelin::token::erc20::interface::{IERC20, IERC20Dispatcher, IERC20DispatcherTrait}; +use starknet::contract_address::{contract_address_const}; +use super::constants::{OWNER, L3_BRIDGE_ADDRESS, DELAY_TIME}; +use super::setup::{ + deploy_erc20, deploy_token_bridge_with_messaging, deploy_token_bridge, enroll_token_and_settle +}; +use starknet_bridge::constants; +use starknet_bridge::bridge::tests::utils::message_payloads; + + +fn setup() -> (ITokenBridgeDispatcher, EventSpy, IERC20Dispatcher, IMockMessagingDispatcher, u256) { + let (token_bridge, mut spy, messaging_mock) = deploy_token_bridge_with_messaging(); + let usdc_address = deploy_erc20("usdc", "usdc"); + let usdc = IERC20Dispatcher { contract_address: usdc_address }; + enroll_token_and_settle(token_bridge, messaging_mock, usdc.contract_address); + + snf::start_cheat_caller_address(usdc.contract_address, OWNER()); + usdc.transfer(snf::test_address(), 100); + snf::stop_cheat_caller_address(usdc_address); + + let amount = 100; + usdc.approve(token_bridge.contract_address, amount); + token_bridge.deposit(usdc_address, amount, snf::test_address()); + messaging_mock + .process_last_message_to_appchain( + L3_BRIDGE_ADDRESS(), + constants::HANDLE_TOKEN_DEPOSIT_SELECTOR, + message_payloads::deposit_message_payload( + usdc_address, + amount, + snf::test_address(), + snf::test_address(), + false, + array![].span() + ) + ); + + (token_bridge, spy, usdc, messaging_mock, amount) +} + +#[test] +fn withdraw_ok() { + let (token_bridge, _, usdc, messaging_mock, amount) = setup(); + + // Register a withdraw message from appchain to piltover + messaging_mock + .process_message_to_starknet( + L3_BRIDGE_ADDRESS(), + token_bridge.contract_address, + message_payloads::withdraw_message_payload_from_appchain( + usdc.contract_address, amount, snf::test_address() + ) + ); + + let initial_bridge_balance = usdc.balance_of(token_bridge.contract_address); + let initial_recipient_balance = usdc.balance_of(snf::test_address()); + token_bridge.withdraw(usdc.contract_address, amount, snf::test_address()); + + assert( + usdc.balance_of(snf::test_address()) == initial_recipient_balance + amount, + 'Incorrect amount recieved' + ); + + assert( + usdc.balance_of(token_bridge.contract_address) == initial_bridge_balance - amount, + 'Incorrect token amount' + ); +} + +#[test] +#[should_panic(expected: ('INVALID_MESSAGE_TO_CONSUME',))] +fn withdraw_incorrect_recipient() { + let (token_bridge, _, usdc, messaging_mock, amount) = setup(); + + // Register a withdraw message from appchain to piltover + messaging_mock + .process_message_to_starknet( + L3_BRIDGE_ADDRESS(), + token_bridge.contract_address, + message_payloads::withdraw_message_payload_from_appchain( + usdc.contract_address, amount, snf::test_address() + ) + ); + + token_bridge.withdraw(usdc.contract_address, amount, contract_address_const::<'user2'>()); +} + + +#[test] +#[should_panic(expected: ('LIMIT_EXCEEDED',))] +fn withdraw_limit_reached() { + let (token_bridge, _, usdc, messaging_mock, _) = setup(); + + let token_bridge_admin = ITokenBridgeAdminDispatcher { + contract_address: token_bridge.contract_address + }; + + snf::start_cheat_caller_address(token_bridge.contract_address, OWNER()); + token_bridge_admin.enable_withdrawal_limit(usdc.contract_address); + snf::stop_cheat_caller_address(token_bridge.contract_address); + + let withdraw_amount = 50; + + // Register a withdraw message from appchain to piltover + messaging_mock + .process_message_to_starknet( + L3_BRIDGE_ADDRESS(), + token_bridge.contract_address, + message_payloads::withdraw_message_payload_from_appchain( + usdc.contract_address, withdraw_amount, snf::test_address() + ) + ); + + token_bridge.withdraw(usdc.contract_address, withdraw_amount, snf::test_address()); +} diff --git a/tests/token_actions_test.cairo b/tests/withdrawal_limit_bridge_test.cairo similarity index 68% rename from tests/token_actions_test.cairo rename to tests/withdrawal_limit_bridge_test.cairo index 4aeca8a..f2a8e24 100644 --- a/tests/token_actions_test.cairo +++ b/tests/withdrawal_limit_bridge_test.cairo @@ -22,7 +22,7 @@ use openzeppelin::access::ownable::{ }; use starknet::contract_address::{contract_address_const}; use super::constants::{OWNER, L3_BRIDGE_ADDRESS, USDC_MOCK_ADDRESS, DELAY_TIME}; -use super::setup::{deploy_erc20, deploy_token_bridge, mock_state_testing}; +use super::setup::{deploy_erc20, deploy_token_bridge}; #[test] @@ -33,13 +33,13 @@ fn enable_withdrawal_limit_not_owner() { contract_address: token_bridge.contract_address }; - let usdc_address = deploy_erc20("USDC", "USDC"); + let usdc_address = USDC_MOCK_ADDRESS(); token_bridge_admin.enable_withdrawal_limit(usdc_address); } #[test] fn enable_withdrawal_limit_ok() { - let (token_bridge, _) = deploy_token_bridge(); + let (token_bridge, mut spy) = deploy_token_bridge(); let token_bridge_admin = ITokenBridgeAdminDispatcher { contract_address: token_bridge.contract_address }; @@ -47,20 +47,32 @@ fn enable_withdrawal_limit_ok() { contract_address: token_bridge.contract_address }; - let owner = OWNER(); - snf::start_cheat_caller_address(token_bridge.contract_address, owner); + snf::start_cheat_caller_address(token_bridge.contract_address, OWNER()); - let usdc_address = deploy_erc20("USDC", "USDC"); + let usdc_address = USDC_MOCK_ADDRESS(); 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'); + + let exepected_limit_enabled = TokenBridge::WithdrawalLimitEnabled { + sender: OWNER(), token: usdc_address + }; + spy + .assert_emitted( + @array![ + ( + token_bridge_admin.contract_address, + Event::WithdrawalLimitEnabled(exepected_limit_enabled) + ) + ] + ); } #[test] fn disable_withdrwal_limit_ok() { - let (token_bridge, _) = deploy_token_bridge(); + let (token_bridge, mut spy) = deploy_token_bridge(); let token_bridge_admin = ITokenBridgeAdminDispatcher { contract_address: token_bridge.contract_address }; @@ -71,7 +83,7 @@ fn disable_withdrwal_limit_ok() { let owner = OWNER(); snf::start_cheat_caller_address(token_bridge.contract_address, owner); - let usdc_address = deploy_erc20("USDC", "USDC"); + let usdc_address = USDC_MOCK_ADDRESS(); token_bridge_admin.enable_withdrawal_limit(usdc_address); // Withdrawal limit is now applied @@ -82,12 +94,25 @@ fn disable_withdrwal_limit_ok() { assert( withdrawal_limit.is_withdrawal_limit_applied(usdc_address) == false, 'Limit not applied' ); - snf::stop_cheat_caller_address(token_bridge.contract_address); + + let expected_limit_disabled = TokenBridge::WithdrawalLimitDisabled { + sender: OWNER(), token: usdc_address + }; + + spy + .assert_emitted( + @array![ + ( + token_bridge_admin.contract_address, + Event::WithdrawalLimitDisabled(expected_limit_disabled) + ) + ] + ); } #[test] #[should_panic(expected: ('Caller is not the owner',))] -fn disable_withdrwal_not_owner() { +fn disable_withdrawal_limit_not_owner() { let (token_bridge, _) = deploy_token_bridge(); let token_bridge_admin = ITokenBridgeAdminDispatcher { contract_address: token_bridge.contract_address @@ -100,7 +125,7 @@ fn disable_withdrwal_not_owner() { let owner = OWNER(); snf::start_cheat_caller_address(token_bridge.contract_address, owner); - let usdc_address = deploy_erc20("USDC", "USDC"); + let usdc_address = USDC_MOCK_ADDRESS(); token_bridge_admin.enable_withdrawal_limit(usdc_address); // Withdrawal limit is now applied @@ -114,3 +139,25 @@ fn disable_withdrwal_not_owner() { withdrawal_limit.is_withdrawal_limit_applied(usdc_address) == false, 'Limit not applied' ); } + +#[test] +fn is_withdrawal_limit_applied_ok() { + let (token_bridge, _) = deploy_token_bridge(); + let usdc_address = USDC_MOCK_ADDRESS(); + let token_bridge_admin = ITokenBridgeAdminDispatcher { + contract_address: token_bridge.contract_address + }; + let withdrawal_limit = IWithdrawalLimitStatusDispatcher { + contract_address: token_bridge.contract_address + }; + + assert( + withdrawal_limit.is_withdrawal_limit_applied(usdc_address) == false, 'Limit already applied' + ); + + snf::start_cheat_caller_address(token_bridge.contract_address, OWNER()); + 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'); +}