From 173bd31d6ffd02bf9e94cab3c0fdc9b2979ff402 Mon Sep 17 00:00:00 2001 From: Yusuf Habib <109147010+manlikeHB@users.noreply.github.com> Date: Sat, 2 Nov 2024 04:43:03 +0100 Subject: [PATCH] Test execute (#116) * add mock erc20 * refac * add tests * clean up + fmt * rename craft_call to craft_erc20_transfer_call --- snphone-contracts/Scarb.lock | 6 + snphone-contracts/Scarb.toml | 1 + snphone-contracts/src/lib.cairo | 1 + snphone-contracts/src/mocks.cairo | 1 + snphone-contracts/src/mocks/erc20_mock.cairo | 36 ++++ snphone-contracts/tests/test_account.cairo | 186 ++++++++++++++++--- 6 files changed, 210 insertions(+), 21 deletions(-) create mode 100644 snphone-contracts/src/mocks.cairo create mode 100644 snphone-contracts/src/mocks/erc20_mock.cairo diff --git a/snphone-contracts/Scarb.lock b/snphone-contracts/Scarb.lock index c88208c2..35769ca9 100644 --- a/snphone-contracts/Scarb.lock +++ b/snphone-contracts/Scarb.lock @@ -5,9 +5,15 @@ version = 1 name = "contracts" version = "0.1.0" dependencies = [ + "openzeppelin", "snforge_std", ] +[[package]] +name = "openzeppelin" +version = "0.10.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.10.0#d77082732daab2690ba50742ea41080eb23299d3" + [[package]] name = "snforge_std" version = "0.20.0" diff --git a/snphone-contracts/Scarb.toml b/snphone-contracts/Scarb.toml index 4d6996fd..43d8eea4 100644 --- a/snphone-contracts/Scarb.toml +++ b/snphone-contracts/Scarb.toml @@ -6,6 +6,7 @@ version = "0.1.0" [dependencies] starknet = "2.6.3" +openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.10.0" } [dev-dependencies] snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v0.20.0" } diff --git a/snphone-contracts/src/lib.cairo b/snphone-contracts/src/lib.cairo index b0edc6c1..9f860cd9 100644 --- a/snphone-contracts/src/lib.cairo +++ b/snphone-contracts/src/lib.cairo @@ -1 +1,2 @@ pub mod account; +pub mod mocks; diff --git a/snphone-contracts/src/mocks.cairo b/snphone-contracts/src/mocks.cairo new file mode 100644 index 00000000..1cdba4b0 --- /dev/null +++ b/snphone-contracts/src/mocks.cairo @@ -0,0 +1 @@ +pub mod erc20_mock; diff --git a/snphone-contracts/src/mocks/erc20_mock.cairo b/snphone-contracts/src/mocks/erc20_mock.cairo new file mode 100644 index 00000000..25e26c6d --- /dev/null +++ b/snphone-contracts/src/mocks/erc20_mock.cairo @@ -0,0 +1,36 @@ +#[starknet::contract] +pub mod ERC20Mock { + use openzeppelin::token::erc20::{ERC20Component}; + use starknet::ContractAddress; + + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + + #[abi(embed_v0)] + impl ERC20Impl = ERC20Component::ERC20Impl; + impl InternalImpl = ERC20Component::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc20: ERC20Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC20Event: ERC20Component::Event + } + + #[constructor] + fn constructor( + ref self: ContractState, + name: ByteArray, + symbol: ByteArray, + initial_supply: u256, + recipient: ContractAddress + ) { + self.erc20.initializer(name, symbol); + self.erc20._mint(recipient, initial_supply); + } +} diff --git a/snphone-contracts/tests/test_account.cairo b/snphone-contracts/tests/test_account.cairo index c0052b99..ca89e371 100644 --- a/snphone-contracts/tests/test_account.cairo +++ b/snphone-contracts/tests/test_account.cairo @@ -1,31 +1,83 @@ -use snforge_std::{declare, ContractClassTrait}; -use snforge_std::{start_prank, stop_prank, CheatTarget}; +use snforge_std::{declare, ContractClassTrait, ContractClass}; +use snforge_std::{start_prank, stop_prank, CheatTarget, prank, CheatSpan}; +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; use starknet::contract_address_const; +use starknet::{ContractAddress, {account::Call}}; use contracts::account::{IStarknetPhoneAccountDispatcher, IStarknetPhoneAccountDispatcherTrait}; -#[test] -fn test_deploy() { +const PUB_KEY: felt252 = 'pub_key'; +const INITIAL_SUPPLY: u256 = 1000; + +fn OWNER() -> ContractAddress { + 'OWNER'.try_into().unwrap() +} + +fn RECIPIENT() -> ContractAddress { + 'RECIPIENT'.try_into().unwrap() +} + +fn BOB() -> ContractAddress { + 'BOB'.try_into().unwrap() +} + +fn ALICE() -> ContractAddress { + 'ALICE'.try_into().unwrap() +} + +fn deploy_erc20_mock(name: ByteArray, symbol: ByteArray,) -> IERC20Dispatcher { + let class = declare("ERC20Mock"); + let mut calldata = array![]; + name.serialize(ref calldata); + symbol.serialize(ref calldata); + INITIAL_SUPPLY.serialize(ref calldata); + OWNER().serialize(ref calldata); + + let contract_address = class.deploy(@calldata).unwrap(); + + IERC20Dispatcher { contract_address } +} + +fn deploy_wallet() -> IStarknetPhoneAccountDispatcher { let class_hash = declare("StarknetPhoneAccount"); - let _pub_key = 'pub_key'; - let contract_address = class_hash.deploy(@array![_pub_key]).unwrap(); + let contract_address = class_hash.deploy(@array![PUB_KEY]).unwrap(); let wallet = IStarknetPhoneAccountDispatcher { contract_address }; + wallet +} + +fn _setup() -> (IStarknetPhoneAccountDispatcher, IERC20Dispatcher) { + let mock_erc20_dispatcher = deploy_erc20_mock("mock one", "MCK1"); + let wallet_dispatcher = deploy_wallet(); + + (wallet_dispatcher, mock_erc20_dispatcher) +} + +fn craft_erc20_transfer_call( + to: ContractAddress, recipient: ContractAddress, amount: u256 +) -> Call { + let mut calldata = array![]; + recipient.serialize(ref calldata); + amount.serialize(ref calldata); + + Call { to, selector: selector!("transfer"), calldata: calldata.span() } +} + +#[test] +fn test_deploy() { + let (wallet, _) = _setup(); let pub_key = wallet.get_public_key(); - assert(pub_key == _pub_key, 'Pub key not set'); + assert(pub_key == PUB_KEY, 'Pub key not set'); } // Test that only the contract owner can change the public key #[test] fn test_only_account_can_change_public_key() { - let class_hash = declare("StarknetPhoneAccount"); - let _pub_key = 'pub_key'; - let contract_address = class_hash.deploy(@array![_pub_key]).unwrap(); - let wallet = IStarknetPhoneAccountDispatcher { contract_address }; + let (wallet, _) = _setup(); // Other contract calls function let new_pub_key = 'new_pub_key'; - start_prank(CheatTarget::One(wallet.contract_address), contract_address); + start_prank(CheatTarget::One(wallet.contract_address), wallet.contract_address); wallet.set_public_key(new_pub_key); stop_prank(CheatTarget::One(wallet.contract_address)); @@ -36,10 +88,7 @@ fn test_only_account_can_change_public_key() { #[test] #[should_panic] fn test_other_account_cannot_change_public_key() { - let class_hash = declare("StarknetPhoneAccount"); - let _pub_key = 'pub_key'; - let contract_address = class_hash.deploy(@array![_pub_key]).unwrap(); - let wallet = IStarknetPhoneAccountDispatcher { contract_address }; + let (wallet, _) = _setup(); // Other contract calls function let not_wallet = contract_address_const::<'not_wallet'>(); @@ -55,10 +104,105 @@ fn test_other_account_cannot_change_public_key() { //fn test_is_valid_signature() { // TODO: Test is_valid_signature() works as expected (valid returns true, anything else returns false (check 0 hash and empty sigs as well)) //} -//#[test] -//fn test_execute() { // TODO: Test __execute__() works as expected (solo and multi-calls should work as expected) -// - Might need to create a mock erc20 contract to test calls (see if the wallet is able to do a multi call (try sending eth to 2 accounts from the -// deployed wallet, both accounts' balance should update) -//} +#[test] +#[should_panic(expected: ('invalid caller',))] +fn test_execute_with_invalid_caller() { + let (wallet, mock_erc20) = _setup(); + // Other contract calls function + let not_wallet = contract_address_const::<'not_wallet'>(); + + // Craft call and add to calls array + let amount = 200; + let mut calldata = array![]; + mock_erc20.contract_address.serialize(ref calldata); + RECIPIENT().serialize(ref calldata); + amount.serialize(ref calldata); + + let call = Call { + to: mock_erc20.contract_address, selector: selector!("transfer"), calldata: calldata.span() + }; + + let calls = array![call]; + + start_prank(CheatTarget::One(wallet.contract_address), not_wallet); + wallet.__execute__(calls); + stop_prank(CheatTarget::One(wallet.contract_address)); +} + +#[test] +fn test_execute() { + let (wallet, mock_erc20) = _setup(); + + // fund wallet + start_prank(CheatTarget::One(mock_erc20.contract_address), OWNER()); + mock_erc20.transfer(wallet.contract_address, INITIAL_SUPPLY); + stop_prank(CheatTarget::One(mock_erc20.contract_address)); + + // Craft call and add to calls array + let amount = 200_u256; + let call = craft_erc20_transfer_call(mock_erc20.contract_address, RECIPIENT(), amount); + + let calls = array![call]; + + let zero = contract_address_const::<0>(); + + let wallet_ballance_before = mock_erc20.balance_of(wallet.contract_address); + // execute + start_prank(CheatTarget::One(wallet.contract_address), zero); + wallet.__execute__(calls); + stop_prank(CheatTarget::One(wallet.contract_address)); + + let wallet_ballance_after = mock_erc20.balance_of(wallet.contract_address); + + assert((wallet_ballance_before - amount) == wallet_ballance_after, 'wrong wallet balance'); + assert(mock_erc20.balance_of(RECIPIENT()) == amount, 'wrong recipient balance'); +} + +#[test] +fn test_multicall() { + let (wallet, mock_erc20) = _setup(); + + // fund wallet + start_prank(CheatTarget::One(mock_erc20.contract_address), OWNER()); + mock_erc20.transfer(wallet.contract_address, INITIAL_SUPPLY); + stop_prank(CheatTarget::One(mock_erc20.contract_address)); + + let first_amount = 300_u256; + let second_amount = 100_u256; + let third_amount = 150_u256; + let forth_amount = 50_u256; + + // Craft call and add to calls array + let first_call = craft_erc20_transfer_call( + mock_erc20.contract_address, RECIPIENT(), first_amount + ); + let second_call = craft_erc20_transfer_call( + mock_erc20.contract_address, OWNER(), second_amount + ); + let third_call = craft_erc20_transfer_call(mock_erc20.contract_address, BOB(), third_amount); + let forth_call = craft_erc20_transfer_call(mock_erc20.contract_address, ALICE(), forth_amount); + + let calls = array![first_call, second_call, third_call, forth_call]; + + let zero = contract_address_const::<0>(); + + // execute + start_prank(CheatTarget::One(wallet.contract_address), zero); + wallet.__execute__(calls); + stop_prank(CheatTarget::One(wallet.contract_address)); + + let wallet_ballance_after = mock_erc20.balance_of(wallet.contract_address); + let expected_wallet_balance = INITIAL_SUPPLY + - first_amount + - second_amount + - third_amount + - forth_amount; + + assert(wallet_ballance_after == expected_wallet_balance, 'wrong wallet balance'); + assert(mock_erc20.balance_of(RECIPIENT()) == first_amount, 'wrong recipient balance'); + assert(mock_erc20.balance_of(OWNER()) == second_amount, 'wrong owner balance'); + assert(mock_erc20.balance_of(BOB()) == third_amount, 'wrong bob balance'); + assert(mock_erc20.balance_of(ALICE()) == forth_amount, 'wrong alice balance'); +}