From 58d31ac61b4ca53882cf1880c33c34ae961fd383 Mon Sep 17 00:00:00 2001 From: enitrat Date: Wed, 13 Dec 2023 11:00:07 +0100 Subject: [PATCH] refactor: components & add tests --- contracts/Scarb.toml | 3 + contracts/src/lib.cairo | 1 + contracts/src/tokens/interface.cairo | 65 +++++ contracts/src/tokens/memecoin.cairo | 214 ++++----------- .../tests/test_unruggable_memecoin.cairo | 258 ++++++++++++++++-- 5 files changed, 364 insertions(+), 177 deletions(-) create mode 100644 contracts/src/tokens/interface.cairo diff --git a/contracts/Scarb.toml b/contracts/Scarb.toml index b81026f6..f3cdbdfb 100644 --- a/contracts/Scarb.toml +++ b/contracts/Scarb.toml @@ -18,3 +18,6 @@ allowed-libfuncs-list.name = "experimental" [cairo] sierra-replace-ids = true + +[tool.fmt] +sort-module-level-items = true diff --git a/contracts/src/lib.cairo b/contracts/src/lib.cairo index ac2ae0ab..2e2d51a8 100644 --- a/contracts/src/lib.cairo +++ b/contracts/src/lib.cairo @@ -1,3 +1,4 @@ mod tokens { + mod interface; mod memecoin; } diff --git a/contracts/src/tokens/interface.cairo b/contracts/src/tokens/interface.cairo new file mode 100644 index 00000000..0b9ab808 --- /dev/null +++ b/contracts/src/tokens/interface.cairo @@ -0,0 +1,65 @@ +use openzeppelin::token::erc20::interface::{IERC20Metadata, IERC20, IERC20Camel}; +use starknet::ContractAddress; + + +#[starknet::interface] +trait IUnruggableMemecoin { + // ************************************ + // * Metadata + // ************************************ + fn name(self: @TState) -> felt252; + fn symbol(self: @TState) -> felt252; + fn decimals(self: @TState) -> u8; + + // ************************************ + // * snake_case + // ************************************ + fn total_supply(self: @TState) -> u256; + fn balance_of(self: @TState, account: ContractAddress) -> u256; + fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress) -> u256; + fn transfer(ref self: TState, recipient: ContractAddress, amount: u256) -> bool; + fn transfer_from( + ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; + fn approve(ref self: TState, spender: ContractAddress, amount: u256) -> bool; + + // ************************************ + // * camelCase + // ************************************ + fn totalSupply(self: @TState) -> u256; + fn balanceOf(self: @TState, account: ContractAddress) -> u256; + fn transferFrom( + ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; + + // ************************************ + // * Additional functions + // ************************************ + fn launch_memecoin(ref self: TState); +} + +#[starknet::interface] +trait IUnruggableMemecoinCamel { + fn totalSupply(self: @TState) -> u256; + fn balanceOf(self: @TState, account: ContractAddress) -> u256; + fn transferFrom( + ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; +} + +#[starknet::interface] +trait IUnruggableMemecoinSnake { + fn total_supply(self: @TState) -> u256; + fn balance_of(self: @TState, account: ContractAddress) -> u256; + fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress) -> u256; + fn transfer(ref self: TState, recipient: ContractAddress, amount: u256) -> bool; + fn transfer_from( + ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; + fn approve(ref self: TState, spender: ContractAddress, amount: u256) -> bool; +} + +#[starknet::interface] +trait IUnruggableAdditional { + fn launch_memecoin(ref self: TState); +} diff --git a/contracts/src/tokens/memecoin.cairo b/contracts/src/tokens/memecoin.cairo index 9f6364a4..eb6a881e 100644 --- a/contracts/src/tokens/memecoin.cairo +++ b/contracts/src/tokens/memecoin.cairo @@ -1,48 +1,32 @@ //! `UnruggableMemecoin` is an ERC20 token has additional features to prevent rug pulls. use starknet::ContractAddress; -#[starknet::interface] -trait IUnruggableMemecoin { - // ************************************ - // * Standard ERC20 functions - // ************************************ - fn name(self: @TState) -> felt252; - fn symbol(self: @TState) -> felt252; - fn decimals(self: @TState) -> u8; - fn total_supply(self: @TState) -> u256; - fn balance_of(self: @TState, account: ContractAddress) -> u256; - fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress) -> u256; - fn transfer(ref self: TState, recipient: ContractAddress, amount: u256) -> bool; - fn transfer_from( - ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 - ) -> bool; - fn approve(ref self: TState, spender: ContractAddress, amount: u256) -> bool; - // ************************************ - // * Additional functions - // ************************************ - fn launch_memecoin(ref self: TState); -} #[starknet::contract] mod UnruggableMemecoin { - // Core dependencies. - use openzeppelin::access::ownable::ownable::OwnableComponent::InternalTrait; use integer::BoundedInt; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::access::ownable::ownable::OwnableComponent::InternalTrait; + use openzeppelin::token::erc20::ERC20Component; use starknet::{ContractAddress, get_caller_address}; + use unruggable::tokens::interface::{ + IUnruggableMemecoinSnake, IUnruggableMemecoinCamel, IUnruggableAdditional + }; use zeroable::Zeroable; - // External dependencies. - use openzeppelin::access::ownable::OwnableComponent; - - // Internal dependencies. - use super::IUnruggableMemecoin; - // Components. component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); impl OwnableInternalImpl = OwnableComponent::InternalImpl; + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + // Internals + impl ERC20InternalImpl = ERC20Component::InternalImpl; + + // ERC20 entrypoints. + #[abi(embed_v0)] + impl ERC20MetadataImpl = ERC20Component::ERC20MetadataImpl; + // Constants. - const DECIMALS: u8 = 18; /// The maximum number of holders allowed before launch. /// This is to prevent the contract from being launched with a large number of holders. /// Once reached, transfers are disabled until the memecoin is launched. @@ -56,38 +40,22 @@ mod UnruggableMemecoin { #[storage] struct Storage { marker_v_0: (), - name: felt252, - symbol: felt252, - total_supply: u256, - balances: LegacyMap, - allowances: LegacyMap<(ContractAddress, ContractAddress), u256>, // Components. #[substorage(v0)] - ownable: OwnableComponent::Storage + ownable: OwnableComponent::Storage, + #[substorage(v0)] + erc20: ERC20Component::Storage } #[event] #[derive(Drop, starknet::Event)] enum Event { - Transfer: Transfer, - Approval: Approval, #[flat] - OwnableEvent: OwnableComponent::Event + OwnableEvent: OwnableComponent::Event, + #[flat] + ERC20Event: ERC20Component::Event } - #[derive(Drop, starknet::Event)] - struct Transfer { - from: ContractAddress, - to: ContractAddress, - value: u256 - } - - #[derive(Drop, starknet::Event)] - struct Approval { - owner: ContractAddress, - spender: ContractAddress, - value: u256 - } /// Constructor called once when the contract is deployed. /// # Arguments @@ -106,20 +74,20 @@ mod UnruggableMemecoin { initial_supply: u256 ) { // Initialize the ERC20 token. - self.initializer(name, symbol); + self.erc20.initializer(name, symbol); // Initialize the owner. self.ownable.initializer(owner); // Mint initial supply to the initial recipient. - self._mint(initial_recipient, initial_supply); + self.erc20._mint(initial_recipient, initial_supply); } // // External // #[abi(embed_v0)] - impl UnruggableMemecoinImpl of IUnruggableMemecoin { + impl UnruggableEntrypoints of IUnruggableAdditional { // ************************************ // * UnruggableMemecoin functions // ************************************ @@ -130,34 +98,25 @@ mod UnruggableMemecoin { // Interactions. } + } + #[abi(embed_v0)] + impl SnakeEntrypoints of IUnruggableMemecoinSnake { // ************************************ - // * Standard ERC20 functions + // * snake_case functions // ************************************ - fn name(self: @ContractState) -> felt252 { - self.name.read() - } - - fn symbol(self: @ContractState) -> felt252 { - self.symbol.read() - } - - fn decimals(self: @ContractState) -> u8 { - DECIMALS - } - fn total_supply(self: @ContractState) -> u256 { - self.total_supply.read() + self.erc20.ERC20_total_supply.read() } fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { - self.balances.read(account) + self.erc20.ERC20_balances.read(account) } fn allowance( self: @ContractState, owner: ContractAddress, spender: ContractAddress ) -> u256 { - self.allowances.read((owner, spender)) + self.erc20.ERC20_allowances.read((owner, spender)) } fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool { @@ -173,119 +132,52 @@ mod UnruggableMemecoin { amount: u256 ) -> bool { let caller = get_caller_address(); - self._spend_allowance(sender, caller, amount); - self._transfer(sender, recipient, amount); + self.erc20._spend_allowance(sender, caller, amount); + self.erc20._transfer(sender, recipient, amount); true } fn approve(ref self: ContractState, spender: ContractAddress, amount: u256) -> bool { let caller = get_caller_address(); - self._approve(caller, spender, amount); + self.erc20._approve(caller, spender, amount); true } } - #[external(v0)] - fn increase_allowance( - ref self: ContractState, spender: ContractAddress, added_value: u256 - ) -> bool { - self._increase_allowance(spender, added_value) - } - - #[external(v0)] - fn increaseAllowance( - ref self: ContractState, spender: ContractAddress, addedValue: u256 - ) -> bool { - increase_allowance(ref self, spender, addedValue) - } - - #[external(v0)] - fn decrease_allowance( - ref self: ContractState, spender: ContractAddress, subtracted_value: u256 - ) -> bool { - self._decrease_allowance(spender, subtracted_value) - } - - #[external(v0)] - fn decreaseAllowance( - ref self: ContractState, spender: ContractAddress, subtractedValue: u256 - ) -> bool { - decrease_allowance(ref self, spender, subtractedValue) - } - - // - // Internal - // - - #[generate_trait] - impl UnruggableMemecoinInternalImpl of UnruggableMemecoinInternalTrait { - fn initializer(ref self: ContractState, name_: felt252, symbol_: felt252) { - self.name.write(name_); - self.symbol.write(symbol_); + #[abi(embed_v0)] + impl CamelEntrypoints of IUnruggableMemecoinCamel { + fn totalSupply(self: @ContractState) -> u256 { + self.erc20.ERC20_total_supply.read() } - - fn _increase_allowance( - ref self: ContractState, spender: ContractAddress, added_value: u256 - ) -> bool { - let caller = get_caller_address(); - self._approve(caller, spender, self.allowances.read((caller, spender)) + added_value); - true + fn balanceOf(self: @ContractState, account: ContractAddress) -> u256 { + self.erc20.ERC20_balances.read(account) } - - fn _decrease_allowance( - ref self: ContractState, spender: ContractAddress, subtracted_value: u256 + fn transferFrom( + ref self: ContractState, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256 ) -> bool { let caller = get_caller_address(); - self - ._approve( - caller, spender, self.allowances.read((caller, spender)) - subtracted_value - ); + self.erc20._spend_allowance(sender, caller, amount); + self._transfer(sender, recipient, amount); true } + } - fn _mint(ref self: ContractState, recipient: ContractAddress, amount: u256) { - assert(!recipient.is_zero(), 'ERC20: mint to 0'); - self.total_supply.write(self.total_supply.read() + amount); - self.balances.write(recipient, self.balances.read(recipient) + amount); - self.emit(Transfer { from: Zeroable::zero(), to: recipient, value: amount }); - } - - fn _burn(ref self: ContractState, account: ContractAddress, amount: u256) { - assert(!account.is_zero(), 'ERC20: burn from 0'); - self.total_supply.write(self.total_supply.read() - amount); - self.balances.write(account, self.balances.read(account) - amount); - self.emit(Transfer { from: account, to: Zeroable::zero(), value: amount }); - } - - fn _approve( - ref self: ContractState, owner: ContractAddress, spender: ContractAddress, amount: u256 - ) { - assert(!owner.is_zero(), 'ERC20: approve from 0'); - assert(!spender.is_zero(), 'ERC20: approve to 0'); - self.allowances.write((owner, spender), amount); - self.emit(Approval { owner, spender, value: amount }); - } + // + // Internal + // + #[generate_trait] + impl UnruggableMemecoinInternalImpl of UnruggableMemecoinInternalTrait { fn _transfer( ref self: ContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256 ) { - assert(!sender.is_zero(), 'ERC20: transfer from 0'); - assert(!recipient.is_zero(), 'ERC20: transfer to 0'); - self.balances.write(sender, self.balances.read(sender) - amount); - self.balances.write(recipient, self.balances.read(recipient) + amount); - self.emit(Transfer { from: sender, to: recipient, value: amount }); - } - - fn _spend_allowance( - ref self: ContractState, owner: ContractAddress, spender: ContractAddress, amount: u256 - ) { - let current_allowance = self.allowances.read((owner, spender)); - if current_allowance != BoundedInt::max() { - self._approve(owner, spender, current_allowance - amount); - } + self.erc20._transfer(sender, recipient, amount); } } } diff --git a/contracts/tests/test_unruggable_memecoin.cairo b/contracts/tests/test_unruggable_memecoin.cairo index b0e670b8..8b5ffb8a 100644 --- a/contracts/tests/test_unruggable_memecoin.cairo +++ b/contracts/tests/test_unruggable_memecoin.cairo @@ -1,10 +1,10 @@ use core::debug::PrintTrait; use core::traits::Into; -use starknet::{ContractAddress, contract_address_const}; use openzeppelin::token::erc20::interface::IERC20; use snforge_std::{declare, ContractClassTrait, start_prank, stop_prank}; +use starknet::{ContractAddress, contract_address_const}; -use unruggable::tokens::memecoin::{ +use unruggable::tokens::interface::{ IUnruggableMemecoinDispatcher, IUnruggableMemecoinDispatcherTrait }; @@ -27,21 +27,247 @@ fn deploy_contract( contract.deploy(@constructor_calldata).unwrap() } -#[test] -fn test_mint() { - let owner = contract_address_const::<42>(); - let initial_supply = 1000.into(); - let contract_address = deploy_contract( - owner, owner, 'UnruggableMemecoin', 'MT', initial_supply - ); +mod erc20_entrypoints { + use core::debug::PrintTrait; + use core::traits::Into; + use openzeppelin::token::erc20::interface::IERC20; + use snforge_std::{declare, ContractClassTrait, start_prank, stop_prank, CheatTarget}; + use starknet::{ContractAddress, contract_address_const}; + use super::deploy_contract; + use unruggable::tokens::interface::{ + IUnruggableMemecoinDispatcher, IUnruggableMemecoinDispatcherTrait + }; + + // Test ERC20 snake entrypoints + + #[test] + fn test_total_supply() { + let owner = contract_address_const::<42>(); + let initial_supply = 1000.into(); + let contract_address = deploy_contract( + owner, owner, 'UnruggableMemecoin', 'MT', initial_supply + ); + + let memecoin = IUnruggableMemecoinDispatcher { contract_address }; + + // Check total supply. Should be equal to initial supply. + let total_supply = memecoin.total_supply(); + assert(total_supply == initial_supply, 'Invalid total supply'); + + // Check initial balance. Should be equal to initial supply. + let balance = memecoin.balance_of(owner); + assert(balance == initial_supply, 'Invalid balance'); + } + + + #[test] + fn test_balance_of() { + let owner = contract_address_const::<42>(); + let initial_supply = 1000.into(); + let contract_address = deploy_contract( + owner, owner, 'UnruggableMemecoin', 'MT', initial_supply + ); + + let memecoin = IUnruggableMemecoinDispatcher { contract_address }; + + // Check initial balance. Should be equal to initial supply. + let balance = memecoin.balance_of(owner); + assert(balance == initial_supply, 'Invalid balance'); + } + + #[test] + fn test_approve_allowance() { + let owner = contract_address_const::<42>(); + let spender = contract_address_const::<43>(); + let initial_supply = 1000.into(); + let contract_address = deploy_contract( + owner, owner, 'UnruggableMemecoin', 'MT', initial_supply + ); + + let memecoin = IUnruggableMemecoinDispatcher { contract_address }; + + // Check initial allowance. Should be equal to 0. + let allowance = memecoin.allowance(owner, spender); + assert(allowance == 0.into(), 'Invalid allowance before'); + + // Approve initial supply tokens. + start_prank(CheatTarget::One(memecoin.contract_address), owner); + memecoin.approve(spender, initial_supply); + + // Check allowance. Should be equal to initial supply. + let allowance = memecoin.allowance(owner, spender); + assert(allowance == initial_supply, 'Invalid allowance after'); + } + + #[test] + fn test_transfer() { + let owner = contract_address_const::<42>(); + let recipient = contract_address_const::<43>(); + let initial_supply = 1000.into(); + let contract_address = deploy_contract( + owner, owner, 'UnruggableMemecoin', 'MT', initial_supply + ); + + let memecoin = IUnruggableMemecoinDispatcher { contract_address }; + + // Transfer 100 tokens to recipient. + start_prank(CheatTarget::One(memecoin.contract_address), owner); + memecoin.transfer(recipient, 100.into()); + + // Check balance. Should be equal to initial supply - 100. + let owner_balance = memecoin.balance_of(owner); + assert(owner_balance == (initial_supply - 100.into()), 'Invalid balance owner'); + + // Check recipient balance. Should be equal to 100. + let recipient_balance = memecoin.balance_of(recipient); + assert(recipient_balance == 100.into(), 'Invalid balance recipient'); + } + + #[test] + fn test_transfer_from() { + let owner = contract_address_const::<42>(); + let spender = contract_address_const::<43>(); + let recipient = contract_address_const::<44>(); + let initial_supply = 1000.into(); + let contract_address = deploy_contract( + owner, owner, 'UnruggableMemecoin', 'MT', initial_supply + ); + + let memecoin = IUnruggableMemecoinDispatcher { contract_address }; + + // Check initial balance. Should be equal to initial supply. + let balance = memecoin.balance_of(owner); + assert(balance == initial_supply, 'Invalid balance'); + + // Approve initial supply tokens. + start_prank(CheatTarget::One(memecoin.contract_address), owner); + memecoin.approve(spender, initial_supply); + + // Transfer 100 tokens to recipient. + start_prank(CheatTarget::One(memecoin.contract_address), spender); + memecoin.transfer_from(owner, recipient, 100.into()); + + // Check balance. Should be equal to initial supply - 100. + let owner_balance = memecoin.balance_of(owner); + assert(owner_balance == (initial_supply - 100.into()), 'Invalid balance owner'); + + // Check recipient balance. Should be equal to 100. + let recipient_balance = memecoin.balance_of(recipient); + assert(recipient_balance == 100.into(), 'Invalid balance recipient'); + + // Check allowance. Should be equal to initial supply - 100. + let allowance = memecoin.allowance(owner, spender); + assert(allowance == (initial_supply - 100.into()), 'Invalid allowance'); + } + + // Test ERC20 Camel entrypoints + + #[test] + fn test_totalSupply() { + let owner = contract_address_const::<42>(); + let initial_supply = 1000.into(); + let contract_address = deploy_contract( + owner, owner, 'UnruggableMemecoin', 'MT', initial_supply + ); + + let memecoin = IUnruggableMemecoinDispatcher { contract_address }; + + // Check total supply. Should be equal to initial supply. + let total_supply = memecoin.totalSupply(); + assert(total_supply == initial_supply, 'Invalid total supply'); + } + + #[test] + fn test_balanceOf() { + let owner = contract_address_const::<42>(); + let initial_supply = 1000.into(); + let contract_address = deploy_contract( + owner, owner, 'UnruggableMemecoin', 'MT', initial_supply + ); + + let memecoin = IUnruggableMemecoinDispatcher { contract_address }; + + // Check initial balance. Should be equal to initial supply. + let balance = memecoin.balanceOf(owner); + assert(balance == initial_supply, 'Invalid balance'); + } + + #[test] + fn test_transferFrom() { + let owner = contract_address_const::<42>(); + let spender = contract_address_const::<43>(); + let recipient = contract_address_const::<44>(); + let initial_supply = 1000.into(); + let contract_address = deploy_contract( + owner, owner, 'UnruggableMemecoin', 'MT', initial_supply + ); + + let memecoin = IUnruggableMemecoinDispatcher { contract_address }; + + // Check initial balance. Should be equal to initial supply. + let balance = memecoin.balanceOf(owner); + assert(balance == initial_supply, 'Invalid balance'); + + // Approve initial supply tokens. + start_prank(CheatTarget::One(memecoin.contract_address), owner); + memecoin.approve(spender, initial_supply); + + // Transfer 100 tokens to recipient. + start_prank(CheatTarget::One(memecoin.contract_address), spender); + memecoin.transferFrom(owner, recipient, 100.into()); + + // Check balance. Should be equal to initial supply - 100. + let balance = memecoin.balanceOf(owner); + assert(balance == (initial_supply - 100.into()), 'Invalid balance'); + + // Check recipient balance. Should be equal to 100. + let balance = memecoin.balanceOf(recipient); + assert(balance == 100.into(), 'Invalid balance'); + + // Check allowance. Should be equal to initial supply - 100. + let allowance = memecoin.allowance(owner, spender); + assert(allowance == (initial_supply - 100.into()), 'Invalid allowance'); + } +} + +mod memecoin_entrypoints { + use core::debug::PrintTrait; + use openzeppelin::token::erc20::interface::IERC20; + use snforge_std::{declare, ContractClassTrait, start_prank, stop_prank, CheatTarget}; + use starknet::{ContractAddress, contract_address_const}; + use super::deploy_contract; + use unruggable::tokens::interface::{ + IUnruggableMemecoinDispatcher, IUnruggableMemecoinDispatcherTrait + }; + + #[test] + fn test_launch_memecoin() { + let owner = contract_address_const::<42>(); + let recipient = contract_address_const::<43>(); + let initial_supply = 1000.into(); + let contract_address = deploy_contract( + owner, recipient, 'UnruggableMemecoin', 'MT', initial_supply + ); + + let memecoin = IUnruggableMemecoinDispatcher { contract_address }; + + start_prank(CheatTarget::One(memecoin.contract_address), owner); + memecoin.launch_memecoin(); + //TODO + } - let safe_dispatcher = IUnruggableMemecoinDispatcher { contract_address }; + #[test] + #[should_panic(expected: ('Caller is not the owner',))] + fn test_launch_memecoin_not_owner() { + let owner = contract_address_const::<42>(); + let recipient = contract_address_const::<43>(); + let initial_supply = 1000.into(); + let contract_address = deploy_contract( + owner, recipient, 'UnruggableMemecoin', 'MT', initial_supply + ); - // Check total supply. Should be equal to initial supply. - let total_supply = safe_dispatcher.total_supply(); - assert(total_supply == initial_supply, 'Invalid total supply'); + let memecoin = IUnruggableMemecoinDispatcher { contract_address }; - // Check initial balance. Should be equal to initial supply. - let balance = safe_dispatcher.balance_of(owner); - assert(balance == initial_supply, 'Invalid balance'); + memecoin.launch_memecoin(); + } }