diff --git a/README.md b/README.md index 05f1caa..a9ffab4 100644 --- a/README.md +++ b/README.md @@ -1,77 +1,14 @@ -# Introduction to Starknet Contracts +# Introduction to Testing Starknet Contract + +- Test suite, unit tests are provided under the each contract's implementations directly whereas full flow integration tests lies within this test suite. We use starknet-foundry testing framework in this class and test thoroughly for any edge cases in each of the contract. + +## Running Tests + 1. Install starknet-foundry by running this command: `curl -L https://raw.githubusercontent.com/foundry-rs/starknet-foundry/master/scripts/install.sh | sh` restart your terminal run `snfoundryup` -2. Create an account on any of these RPC provides: - - [Voyager](https://voyager.online/) - - [BlastAPI](https://starknet-testnet.blastapi.io) - - [Infura](https://www.infura.io/) - -Generate an RPC apikey to interact with the network - -3. Create a contract account by running this command on your terminal: -`sncast -u account create -n --add-profile` - -4. Deploy the contract account: -`sncast --url account deploy --name --max-fee 4323000047553` -`NB` -Running the above command should trigger an error: -`error: Account balance is smaller than the transaction's max_fee.` -That why your account must be funded; to fund your account, visit - https://faucet.goerli.starknet.io/ - -5. Compile your contract by running: `scarb build` - -6. Declare your contract: -`sncast --account test_deploy -u declare --contract-name ` - -7. Deploy your contract: -`sncast --account --url deploy --class-hash ` - -`NB` -While deploying, make sure you check the constructor argument of the contract you are trying to deploy. All arguments must be passed in appropriately; for such case, use this command: -```sncast --account --url deploy --class-hash --constructor-calldata ``` - - - - ---- -# Introduction to Dispatchers - - -### Deployed Contracts - -#### Ownable Contract -- [x] class hash - 0x421a3ad93deda96f863e26ab51a79f4cea384d71714a5b37ace35010872a088 -- [x] address - 0x4a742edef4df3d3fb09809535a322971ababb1f337ffcf5c297a941f54a76e1 - -#### Counter Contract -- [x] class hash - 0x71d83bb407cdd1a963bdcba92c82b3ff18e8e56fd3cfa9410b0dce069477511 -- [x] address - 0x14b32ec4783dabf825bb2ff4c82b20a81273455cf90ff263c85216b54b1f36d - -#### Caller Contract -- [x] class hash - 0x6c9d24030d72669af3e857dc1f04981c5cf316e0c2efee443509bbf95530587 -- [x] address - 0x2ee3772f1ec48d45bd6280daf74bc35eacd8f5dd741daceaea04130bade808 - - - ---- -### Interacting with Deployed Contracts -- Invoke: to execute the logic of a state-changing (writes) function within your deployed contracts from the terminal, run -``` -sncast --url --account invoke --contract-address --function "" --calldata -``` - - -- Call: to execute the logic of a non-state-changing (reads) function within your deployed contracts from the terminal, run: -``` -sncast --url --account call --contract-address --function "()` is similar to address(0) in Solidity - from: contract_address_const::<0>(), to: recipient, value: _initial_supply + Transfer { //Here, `contract_address_const::<0>()` is similar to address(0) in Solidity + from: contract_address_const::<0>(), to: recipient, value: 1000000 } ); } @@ -130,8 +131,12 @@ mod BWCERC20Token { amount: u256 ) { let caller = get_caller_address(); - let my_allowance = self.allowances.read((sender, recipient)); - assert(my_allowance <= amount, 'Amount Not Allowed'); + let my_allowance = self.allowances.read((sender, caller)); + + assert(my_allowance > 0, 'You have no token approved'); + assert(amount <= my_allowance, 'Amount Not Allowed'); + // assert(my_allowance <= amount, 'Amount Not Allowed'); + self .spend_allowance( sender, caller, amount @@ -205,7 +210,7 @@ mod BWCERC20Token { // define a variable ONES_MASK of type u128 let ONES_MASK = 0xfffffffffffffffffffffffffffffff_u128; - // to determine whether the authorization is unlimited, + // to determine whether the authorization is unlimited, let is_unlimited_allowance = current_allowance.low == ONES_MASK && current_allowance @@ -218,3 +223,270 @@ mod BWCERC20Token { } } } + + +// Annotation +#[cfg(test)] +mod test { + use core::serde::Serde; + use super::{IERC20, BWCERC20Token, IERC20Dispatcher, IERC20DispatcherTrait}; + use starknet::ContractAddress; + use starknet::contract_address::contract_address_const; + use array::ArrayTrait; + use snforge_std::{declare, ContractClassTrait, fs::{FileTrait, read_txt}}; + use snforge_std::{start_prank, stop_prank, CheatTarget}; + use snforge_std::PrintTrait; + use traits::{Into, TryInto}; + + // We first have to deploy first via a helper function + fn deploy_contract() -> ContractAddress { + // Before deploying a starknet contract, we need a contract_class. + // Get it using the declare function from starknetFoundry + let erc20contract_class = declare('BWCERC20Token'); + + // Supply values the constructor arguements when deploying + // REMEMBER: It has to be in an array + let file = FileTrait::new('data/constructor_args.txt'); + let constructor_args = read_txt(@file); + let contract_address = erc20contract_class.deploy(@constructor_args).unwrap(); + contract_address + } + + // Generate an address + mod Account { + use starknet::ContractAddress; + use traits::TryInto; + + fn user1() -> ContractAddress { + 'joy'.try_into().unwrap() + } + fn user2() -> ContractAddress { + 'caleb'.try_into().unwrap() + } + + fn admin() -> ContractAddress { + 'admin'.try_into().unwrap() + } + } + + + // --------------------- Now we start testing -------------------------------- + + // Test 1 - Test wether we can get the name + #[test] + fn test_constructor() { + let contract_address = deploy_contract(); + let dispatcher = IERC20Dispatcher { contract_address }; + // let name = dispatcher.get_name(); + let name = dispatcher.get_name(); + + assert(name == 'BlockheaderToken', 'name is not correct'); + } + + #[test] + fn test_decimal_is_correct() { + let contract_address = deploy_contract(); + let dispatcher = IERC20Dispatcher { contract_address }; + let decimal = dispatcher.get_decimals(); + + assert(decimal == 18, 'Decimal is not correct'); + } + + #[test] + fn test_total_supply() { + let contract_address = deploy_contract(); + let dispatcher = IERC20Dispatcher { contract_address }; + let total_supply = dispatcher.get_total_supply(); + + assert(total_supply == 1000000, 'Total supply is wrong'); + } + + #[test] + fn test_address_balance() { + let contract_address = deploy_contract(); + let dispatcher = IERC20Dispatcher { contract_address }; + let balance = dispatcher.get_total_supply(); + let admin_balance = dispatcher.balance_of(Account::admin()); + assert(admin_balance == balance, Errors::INVALID_BALANCE); + + start_prank(CheatTarget::One(contract_address), Account::admin()); + + dispatcher.transfer(Account::user1(), 10); + let new_admin_balance = dispatcher.balance_of(Account::admin()); + assert(new_admin_balance == balance - 10, Errors::INVALID_BALANCE); + stop_prank(CheatTarget::One(contract_address)); + + let user1_balance = dispatcher.balance_of(Account::user1()); + assert(user1_balance == 10, Errors::INVALID_BALANCE); + } + + #[test] + fn test_allowance() { + let contract_address = deploy_contract(); + let dispatcher = IERC20Dispatcher { contract_address }; + + start_prank(CheatTarget::One(contract_address), Account::admin()); + dispatcher.approve(contract_address, 20); + + let currentAllowance = dispatcher.allowance(Account::admin(), contract_address); + + assert(currentAllowance == 20, Errors::INVALID_ALLOWANCE_GIVEN); + stop_prank(CheatTarget::One(contract_address)); + } + + #[test] + fn test_transfer() { + let contract_address = deploy_contract(); + let dispatcher = IERC20Dispatcher { contract_address }; + + // Get original balances + let original_sender_balance = dispatcher.balance_of(Account::admin()); + let original_recipient_balance = dispatcher.balance_of(Account::user1()); + + start_prank(CheatTarget::One(contract_address), Account::admin()); + + dispatcher.transfer(Account::user1(), 50); + + // Confirm that the funds have been sent! + assert( + dispatcher.balance_of(Account::admin()) == original_sender_balance - 50, + Errors::FUNDS_NOT_SENT + ); + + // Confirm that the funds have been recieved! + assert( + dispatcher.balance_of(Account::user1()) == original_recipient_balance + 50, + Errors::FUNDS_NOT_RECIEVED + ); + + stop_prank(CheatTarget::One(contract_address)); + } + + + #[test] + fn test_transfer_from() { + let contract_address = deploy_contract(); + let dispatcher = IERC20Dispatcher { contract_address }; + + start_prank(CheatTarget::One(contract_address), Account::admin()); + dispatcher.approve(Account::user1(), 20); + stop_prank(CheatTarget::One(contract_address)); + + assert( + dispatcher.allowance(Account::admin(), Account::user1()) == 20, + Errors::INVALID_ALLOWANCE_GIVEN + ); + + start_prank(CheatTarget::One(contract_address), Account::user1()); + dispatcher.transfer_from(Account::admin(), Account::user2(), 10); + assert( + dispatcher.allowance(Account::admin(), Account::user1()) == 10, Errors::FUNDS_NOT_SENT + ); + stop_prank(CheatTarget::One(contract_address)); + } + + #[test] + fn test_transfer_from() { + let contract_address = deploy_contract(); + let dispatcher = IERC20Dispatcher { contract_address }; + let user1 = Account::user1(); + start_prank(CheatTarget::One(contract_address), Account::admin()); + dispatcher.approve(user1, 10); + assert(dispatcher.allowance(Account::admin(), user1) == 10, Errors::NOT_ALLOWED); + stop_prank(CheatTarget::One(contract_address)); + + start_prank(CheatTarget::One(contract_address), user1); + dispatcher.transfer_from(Account::admin(), Account::user2(), 5); + assert(dispatcher.balance_of(Account::user2()) == 5, Errors::INVALID_BALANCE); + // dispatcher.transfer_from(Account::admin(), user1, 15); + // assert(dispatcher.balance_of(user1) == 5, Errors::INVALID_BALANCE); + stop_prank(CheatTarget::One(contract_address)); + } + + #[test] + #[should_panic(expected: ('Amount Not Allowed', ))] + fn test_transfer_from_should_fail() { + let contract_address = deploy_contract(); + let dispatcher = IERC20Dispatcher {contract_address}; + start_prank(CheatTarget::One(contract_address), Account::admin()); + dispatcher.approve(Account::user1(), 20); + stop_prank(CheatTarget::One(contract_address)); + + start_prank(CheatTarget::One(contract_address), Account::user1()); + dispatcher.transfer_from(Account::admin(), Account::user2(), 40); + } + + #[test] + #[should_panic(expected: ('You have no token approved', ))] + fn test_transfer_from_failed_when_not_approved() { + let contract_address = deploy_contract(); + let dispatcher = IERC20Dispatcher { contract_address }; + start_prank(CheatTarget::One(contract_address), Account::user1()); + dispatcher.transfer_from(Account::admin(), Account::user2(), 5); + } + + #[test] + fn test_approve() { + let contract_address = deploy_contract(); + let dispatcher = IERC20Dispatcher { contract_address }; + + start_prank(CheatTarget::One(contract_address), Account::admin()); + dispatcher.approve(Account::user1(), 50); + assert( + dispatcher.allowance(Account::admin(), Account::user1()) == 50, + Errors::INVALID_ALLOWANCE_GIVEN + ); + } + + #[test] + fn test_increase_allowance() { + let contract_address = deploy_contract(); + let dispatcher = IERC20Dispatcher { contract_address }; + + start_prank(CheatTarget::One(contract_address), Account::admin()); + dispatcher.approve(Account::user1(), 30); + assert( + dispatcher.allowance(Account::admin(), Account::user1()) == 30, + Errors::INVALID_ALLOWANCE_GIVEN + ); + + dispatcher.increase_allowance(Account::user1(), 20); + + assert( + dispatcher.allowance(Account::admin(), Account::user1()) == 50, + Errors::ERROR_INCREASING_ALLOWANCE + ); + } + + #[test] + fn test_decrease_allowance() { + let contract_address = deploy_contract(); + let dispatcher = IERC20Dispatcher { contract_address }; + + start_prank(CheatTarget::One(contract_address), Account::admin()); + dispatcher.approve(Account::user1(), 30); + assert( + dispatcher.allowance(Account::admin(), Account::user1()) == 30, + Errors::INVALID_ALLOWANCE_GIVEN + ); + + dispatcher.decrease_allowance(Account::user1(), 5); + + assert( + dispatcher.allowance(Account::admin(), Account::user1()) == 25, + Errors::ERROR_DECREASING_ALLOWANCE + ); + } + + // Custom errors for error handling + mod Errors { + const INVALID_DECIMALS: felt252 = 'Invalid decimals!'; + const UNMATCHED_SUPPLY: felt252 = 'Unmatched supply!'; + const INVALID_BALANCE: felt252 = 'Invalid balance!'; + const INVALID_ALLOWANCE_GIVEN: felt252 = 'Invalid allowance given'; + const FUNDS_NOT_SENT: felt252 = 'Funds not sent!'; + const FUNDS_NOT_RECIEVED: felt252 = 'Funds not recieved!'; + const ERROR_INCREASING_ALLOWANCE: felt252 = 'Allowance not increased'; + const ERROR_DECREASING_ALLOWANCE: felt252 = 'Allowance not decreased'; + } +}