From 5596dfd8db269112e3dd3a849eda0aed09d06cf6 Mon Sep 17 00:00:00 2001 From: Credence Date: Tue, 12 Dec 2023 23:38:39 +0100 Subject: [PATCH 1/4] feat: implement maximum holders before launch safeguard #4 --- contracts/src/unruggable_memecoin.cairo | 35 ++ .../tests/test_unruggable_memecoin.cairo | 327 +++++++++++++++++- 2 files changed, 359 insertions(+), 3 deletions(-) diff --git a/contracts/src/unruggable_memecoin.cairo b/contracts/src/unruggable_memecoin.cairo index 9f6364a4..4914f7c9 100644 --- a/contracts/src/unruggable_memecoin.cairo +++ b/contracts/src/unruggable_memecoin.cairo @@ -56,11 +56,17 @@ mod UnruggableMemecoin { #[storage] struct Storage { marker_v_0: (), + // ERC20 name: felt252, symbol: felt252, total_supply: u256, balances: LegacyMap, allowances: LegacyMap<(ContractAddress, ContractAddress), u256>, + + // UnruggableMemecoin + launched: bool, + pre_launch_holders_count: u8, + // Components. #[substorage(v0)] ownable: OwnableComponent::Storage @@ -128,6 +134,8 @@ mod UnruggableMemecoin { self.ownable.assert_only_owner(); // Effects. + self.launched.write(true); + // Interactions. } @@ -161,7 +169,21 @@ mod UnruggableMemecoin { } fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool { + let sender = get_caller_address(); + + if self.launched.read() == false { + // enforce max number of holders before launch + let holders_count = self.pre_launch_holders_count.read(); + assert( + holders_count < MAX_HOLDERS_BEFORE_LAUNCH, + 'Unruggable: max holders reached' + ); + + // update holder count + self.pre_launch_holders_count.write(holders_count + 1); + } + self._transfer(sender, recipient, amount); true } @@ -173,6 +195,19 @@ mod UnruggableMemecoin { amount: u256 ) -> bool { let caller = get_caller_address(); + + if self.launched.read() == false { + // enforce max number of holders before launch + let holders_count = self.pre_launch_holders_count.read(); + assert( + holders_count < MAX_HOLDERS_BEFORE_LAUNCH, + 'Unruggable: max holders reached' + ); + + // update holder count + self.pre_launch_holders_count.write(holders_count + 1); + } + self._spend_allowance(sender, caller, amount); self._transfer(sender, recipient, amount); true diff --git a/contracts/tests/test_unruggable_memecoin.cairo b/contracts/tests/test_unruggable_memecoin.cairo index 709953de..00be3950 100644 --- a/contracts/tests/test_unruggable_memecoin.cairo +++ b/contracts/tests/test_unruggable_memecoin.cairo @@ -2,10 +2,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 snforge_std::{declare, ContractClassTrait, start_prank, stop_prank, CheatTarget}; use unruggablememecoin::unruggable_memecoin::{ - IUnruggableMemecoinDispatcher, IUnruggableMemecoinDispatcherTrait + IUnruggableMemecoinDispatcher, IUnruggableMemecoinDispatcherTrait, + UnruggableMemecoin::MAX_HOLDERS_BEFORE_LAUNCH }; fn deploy_contract( @@ -45,3 +45,324 @@ fn test_mint() { let balance = safe_dispatcher.balance_of(owner); assert(balance == initial_supply, 'Invalid balance'); } + + +#[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 + ); + + + // set owner as caller + start_prank(CheatTarget::One(contract_address), owner); + + // make transfer + let safe_dispatcher = IUnruggableMemecoinDispatcher { contract_address }; + safe_dispatcher.transfer(recipient, initial_supply - 1); + + // check owner's balance. Should be equal to initial supply. + let owner_balance = safe_dispatcher.balance_of(owner); + assert(owner_balance == 1, 'invalid owner balance'); + + // check initial balance. Should be equal to initial supply. + let recipient_balance = safe_dispatcher.balance_of(recipient); + assert(recipient_balance == initial_supply - 1 , 'invalid recipient balance'); +} + + +#[test] +fn test_transfer_2() { + /// Ensure that transfers can be made out to + /// up to `MAX_HOLDERS_BEFORE_LAUNCH` addresses + + let owner = contract_address_const::<42>(); + let initial_supply = 1000.into(); + let contract_address = deploy_contract( + owner, owner, 'UnruggableMemecoin', 'MT', initial_supply + ); + + + // set owner as caller + start_prank(CheatTarget::One(contract_address), owner); + + let safe_dispatcher = IUnruggableMemecoinDispatcher { contract_address }; + + + // index starts from 1 because the owner + // is considered to be the first hodler + let mut index = 1; + loop { + if index == MAX_HOLDERS_BEFORE_LAUNCH { + break; + } + + // make transfer + let recipient: ContractAddress = (index.into() + 9999999).try_into().unwrap(); + safe_dispatcher.transfer(recipient, 1); + + // check initial balance. Should be equal to initial supply. + let recipient_balance = safe_dispatcher.balance_of(recipient); + assert(recipient_balance == 1, 'invalid recipient balance'); + + index += 1; + }; +} + + +#[test] +#[should_panic(expected: ('Unruggable: max holders reached', ))] +fn test_transfer_3() { + /// Ensure that transfers can only be made out to + /// up to `MAX_HOLDERS_BEFORE_LAUNCH` addresses + + let owner = contract_address_const::<42>(); + let initial_supply = 1000.into(); + let contract_address = deploy_contract( + owner, owner, 'UnruggableMemecoin', 'MT', initial_supply + ); + + // set owner as caller + start_prank(CheatTarget::One(contract_address), owner); + + let safe_dispatcher = IUnruggableMemecoinDispatcher { contract_address }; + + let mut index = 0; + loop { + if index == MAX_HOLDERS_BEFORE_LAUNCH + 1 { + break; + } + + // make transfer + let recipient: ContractAddress = (index.into() + 9999999).try_into().unwrap(); + safe_dispatcher.transfer(recipient, 1); + + // check initial balance. Should be equal to initial supply. + let recipient_balance = safe_dispatcher.balance_of(recipient); + assert(recipient_balance == 1, 'invalid recipient balance'); + + index += 1; + }; +} + + + +#[test] +fn test_transfer_4() { + /// Ensure that transfer to more than `MAX_HOLDERS_BEFORE_LAUNCH` + /// works after the token is launched + + let owner = contract_address_const::<42>(); + let initial_supply = 1000.into(); + let contract_address = deploy_contract( + owner, owner, 'UnruggableMemecoin', 'MT', initial_supply + ); + + + // set owner as caller + start_prank(CheatTarget::One(contract_address), owner); + + let safe_dispatcher = IUnruggableMemecoinDispatcher { contract_address }; + // launch memecoin + safe_dispatcher.launch_memecoin(); + + let mut index = 0; + loop { + if index == MAX_HOLDERS_BEFORE_LAUNCH + 1 { + break; + } + + // make transfer + let recipient: ContractAddress = (index.into() + 9999999).try_into().unwrap(); + safe_dispatcher.transfer(recipient, 1); + + // check initial balance. Should be equal to initial supply. + let recipient_balance = safe_dispatcher.balance_of(recipient); + assert(recipient_balance == 1, 'invalid recipient balance'); + + index += 1; + }; +} + + + +#[test] +fn test_transfer_from() { + let owner = contract_address_const::<42>(); + let recipient = contract_address_const::<43>(); + let approved = contract_address_const::<44>(); + let initial_supply = 1000.into(); + let contract_address = deploy_contract( + owner, owner, 'UnruggableMemecoin', 'MT', initial_supply + ); + + let safe_dispatcher = IUnruggableMemecoinDispatcher { contract_address }; + + // set owner as caller + start_prank(CheatTarget::One(contract_address), owner); + + // approve to spend initial_supply + safe_dispatcher.approve(approved, initial_supply); + + // set approved as caller + start_prank(CheatTarget::One(contract_address), approved); + + // make transfer + safe_dispatcher.transfer_from(owner, recipient, initial_supply - 1); + + // check owner's balance. Should be equal to initial supply. + let owner_balance = safe_dispatcher.balance_of(owner); + assert(owner_balance == 1, 'invalid owner balance'); + + // check that approval was spent + let approved_allowance = safe_dispatcher.allowance(owner, approved); + assert(approved_allowance == 1, 'invalid approved balance'); + + // check initial balance. Should be equal to initial supply. + let recipient_balance = safe_dispatcher.balance_of(recipient); + assert(recipient_balance == initial_supply - 1 , 'invalid recipient balance'); +} + + +#[test] +fn test_transfer_from_2() { + /// Ensure that transfers can be made out to + /// up to `MAX_HOLDERS_BEFORE_LAUNCH` addresses + + let owner = contract_address_const::<42>(); + let approved = contract_address_const::<44>(); + let initial_supply = 1000.into(); + let contract_address = deploy_contract( + owner, owner, 'UnruggableMemecoin', 'MT', initial_supply + ); + + let safe_dispatcher = IUnruggableMemecoinDispatcher { contract_address }; + + + // set owner as caller + start_prank(CheatTarget::One(contract_address), owner); + + // approve to spend initial_supply + safe_dispatcher.approve(approved, initial_supply); + + // set approved as caller + start_prank(CheatTarget::One(contract_address), approved); + + let mut index = 0; + loop { + if index == MAX_HOLDERS_BEFORE_LAUNCH { + break; + } + + // make transfer + let recipient: ContractAddress = (index.into() + 9999999).try_into().unwrap(); + safe_dispatcher.transfer_from(owner, recipient, 1); + + // check initial balance. Should be equal to initial supply. + let recipient_balance = safe_dispatcher.balance_of(recipient); + assert(recipient_balance == 1, 'invalid recipient balance'); + + index += 1; + }; +} + + +#[test] +#[should_panic(expected: ('Unruggable: max holders reached', ))] +fn test_transfer_from_3() { + /// Ensure that transfers can only be made out to + /// up to `MAX_HOLDERS_BEFORE_LAUNCH` addresses + + let owner = contract_address_const::<42>(); + let approved = contract_address_const::<44>(); + let initial_supply = 1000.into(); + let contract_address = deploy_contract( + owner, owner, 'UnruggableMemecoin', 'MT', initial_supply + ); + + + let safe_dispatcher = IUnruggableMemecoinDispatcher { contract_address }; + + // set owner as caller + start_prank(CheatTarget::One(contract_address), owner); + + // approve to spend initial_supply + safe_dispatcher.approve(approved, initial_supply); + + // set approved as caller + start_prank(CheatTarget::One(contract_address), approved); + + let mut index = 0; + loop { + if index == MAX_HOLDERS_BEFORE_LAUNCH + 1 { + break; + } + + // make transfer + let recipient: ContractAddress = (index.into() + 9999999).try_into().unwrap(); + safe_dispatcher.transfer_from(owner, recipient, 1); + + // check initial balance. Should be equal to initial supply. + let recipient_balance = safe_dispatcher.balance_of(recipient); + assert(recipient_balance == 1, 'invalid recipient balance'); + + index += 1; + }; +} + + + +#[test] +fn test_transfer_from_4() { + /// Ensure that transfer to more than `MAX_HOLDERS_BEFORE_LAUNCH` + /// works after the token is launched + + let owner = contract_address_const::<42>(); + let approved = contract_address_const::<44>(); + let initial_supply = 1000.into(); + let contract_address = deploy_contract( + owner, owner, 'UnruggableMemecoin', 'MT', initial_supply + ); + + let safe_dispatcher = IUnruggableMemecoinDispatcher { contract_address }; + + + // set owner as caller + start_prank(CheatTarget::One(contract_address), owner); + + // approve to spend initial_supply + safe_dispatcher.approve(approved, initial_supply); + + // launch memecoin + safe_dispatcher.launch_memecoin(); + + // set approved as caller + start_prank(CheatTarget::One(contract_address), approved); + + + let mut index = 0; + loop { + if index == MAX_HOLDERS_BEFORE_LAUNCH + 1 { + break; + } + + // make transfer + let recipient: ContractAddress = (index.into() + 9999999).try_into().unwrap(); + safe_dispatcher.transfer_from(owner, recipient, 1); + + + // check initial balance. Should be equal to initial supply. + let recipient_balance = safe_dispatcher.balance_of(recipient); + assert(recipient_balance == 1, 'invalid recipient balance'); + + index += 1; + }; +} + + + + + From 2ed4b382761840e96ce73e8a05221bbb1a61f265 Mon Sep 17 00:00:00 2001 From: Credence Date: Thu, 14 Dec 2023 01:55:44 +0100 Subject: [PATCH 2/4] feat: implement max holders before launch safeguard --- contracts/src/tokens/interface.cairo | 2 + contracts/src/tokens/memecoin.cairo | 44 ++++- .../tests/test_unruggable_memecoin.cairo | 150 +++++++++++++++++- 3 files changed, 194 insertions(+), 2 deletions(-) diff --git a/contracts/src/tokens/interface.cairo b/contracts/src/tokens/interface.cairo index 0b9ab808..ff78a221 100644 --- a/contracts/src/tokens/interface.cairo +++ b/contracts/src/tokens/interface.cairo @@ -35,6 +35,7 @@ trait IUnruggableMemecoin { // ************************************ // * Additional functions // ************************************ + fn launched(self: @TState) -> bool; fn launch_memecoin(ref self: TState); } @@ -61,5 +62,6 @@ trait IUnruggableMemecoinSnake { #[starknet::interface] trait IUnruggableAdditional { + fn launched(self: @TState) -> bool; fn launch_memecoin(ref self: TState); } diff --git a/contracts/src/tokens/memecoin.cairo b/contracts/src/tokens/memecoin.cairo index eb6a881e..30d8a9dc 100644 --- a/contracts/src/tokens/memecoin.cairo +++ b/contracts/src/tokens/memecoin.cairo @@ -40,6 +40,8 @@ mod UnruggableMemecoin { #[storage] struct Storage { marker_v_0: (), + launched: bool, + pre_launch_holders_count: u8, // Components. #[substorage(v0)] ownable: OwnableComponent::Storage, @@ -56,6 +58,11 @@ mod UnruggableMemecoin { ERC20Event: ERC20Component::Event } + mod Errors { + const MAX_HOLDERS_REACHED: felt252 = 'memecoin: max holders reached'; + } + + /// Constructor called once when the contract is deployed. /// # Arguments @@ -80,7 +87,7 @@ mod UnruggableMemecoin { self.ownable.initializer(owner); // Mint initial supply to the initial recipient. - self.erc20._mint(initial_recipient, initial_supply); + self._mint(initial_recipient, initial_supply); } // @@ -91,11 +98,19 @@ mod UnruggableMemecoin { // ************************************ // * UnruggableMemecoin functions // ************************************ + + fn launched(self: @ContractState) -> bool { + self.launched.read() + } + fn launch_memecoin(ref self: ContractState) { // Checks: Only the owner can launch the memecoin. self.ownable.assert_only_owner(); // Effects. + // Launch the coin + self.launched.write(true); + // Interactions. } } @@ -171,12 +186,39 @@ mod UnruggableMemecoin { // #[generate_trait] impl UnruggableMemecoinInternalImpl of UnruggableMemecoinInternalTrait { + + #[inline(always)] + fn _check_holders_limit(ref self: ContractState) { + + // enforce max number of holders before launch + + if !self.launched.read() { + let current_holders_count = self.pre_launch_holders_count.read(); + assert( + current_holders_count < MAX_HOLDERS_BEFORE_LAUNCH, + Errors::MAX_HOLDERS_REACHED + ); + + self.pre_launch_holders_count.write(current_holders_count + 1); + } + } + + fn _mint( + ref self: ContractState, + recipient: ContractAddress, + amount: u256 + ) { + self._check_holders_limit(); + self.erc20._mint(recipient, amount); + } + fn _transfer( ref self: ContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256 ) { + self._check_holders_limit(); self.erc20._transfer(sender, recipient, amount); } } diff --git a/contracts/tests/test_unruggable_memecoin.cairo b/contracts/tests/test_unruggable_memecoin.cairo index ff26ce94..d80e896e 100644 --- a/contracts/tests/test_unruggable_memecoin.cairo +++ b/contracts/tests/test_unruggable_memecoin.cairo @@ -312,6 +312,9 @@ mod memecoin_entrypoints { start_prank(CheatTarget::One(memecoin.contract_address), owner); memecoin.launch_memecoin(); + + assert(memecoin.launched(), 'Coin not launched'); + //TODO } @@ -329,4 +332,149 @@ mod memecoin_entrypoints { memecoin.launch_memecoin(); } -} \ No newline at end of file +} + + +mod memecoin_internals { + use core::option::OptionTrait; + use snforge_std::{start_prank, CheatTarget}; + use starknet::{ContractAddress, contract_address_const}; + use unruggable::tokens::memecoin::UnruggableMemecoin; + use UnruggableMemecoin::{ + UnruggableMemecoinInternalImpl, SnakeEntrypoints, UnruggableEntrypoints, + MAX_HOLDERS_BEFORE_LAUNCH + }; + + + #[test] + fn test__transfer() { + let owner = contract_address_const::<42>(); + let recipient = contract_address_const::<43>(); + let initial_supply = 1000.into(); + + // call contract constructor + let mut contract_state = UnruggableMemecoin::contract_state_for_testing(); + UnruggableMemecoin::constructor( + ref contract_state, owner, owner, 'UnruggableMemecoin', 'UM', initial_supply + ); + + // Transfer 100 tokens to the recipient + UnruggableMemecoinInternalImpl::_transfer( + ref contract_state, owner, recipient, 100 + ); + + // Check balance. Should be equal to initial supply - 100. + let owner_balance + = SnakeEntrypoints::balance_of(@contract_state, owner); + assert(owner_balance == (initial_supply - 100), 'Invalid balance owner'); + + // Check recipient balance. Should be equal to 100. + let recipient_balance + = SnakeEntrypoints::balance_of(@contract_state, recipient); + assert(recipient_balance == 100.into(), 'Invalid balance recipient'); + } + + + #[test] + fn test__transfer_recipients_equal_holder_cap() { + let owner = contract_address_const::<42>(); + let initial_supply = 1000.into(); + + // call contract constructor + let mut contract_state = UnruggableMemecoin::contract_state_for_testing(); + UnruggableMemecoin::constructor( + ref contract_state, owner, owner, 'UnruggableMemecoin', 'UM', initial_supply + ); + + // index starts from 1 because owner has initial supply + let mut index = 1; + loop { + if index == MAX_HOLDERS_BEFORE_LAUNCH { + break; + } + + // Transfer 1 token to the unique recipient + let unique_recipient: ContractAddress = (index.into() + 9999).try_into().unwrap(); + UnruggableMemecoinInternalImpl::_transfer( + ref contract_state, owner, unique_recipient, 1 + ); + + // Check recipient balance. Should be equal to 1. + let recipient_balance + = SnakeEntrypoints::balance_of( + @contract_state, unique_recipient + ); + assert(recipient_balance == 1.into(), 'Invalid balance recipient'); + + index += 1; + }; + } + + + #[test] + #[should_panic(expected: ('memecoin: max holders reached', ))] + fn test__transfer_above_holder_cap() { + let owner = contract_address_const::<42>(); + let initial_supply = 1000.into(); + + // call contract constructor + let mut contract_state = UnruggableMemecoin::contract_state_for_testing(); + UnruggableMemecoin::constructor( + ref contract_state, owner, owner, 'UnruggableMemecoin', 'UM', initial_supply + ); + + // index starts from 1 because owner has initial supply + let mut index = 1; + loop { + if index == MAX_HOLDERS_BEFORE_LAUNCH + 1 { + break; + } + + // Transfer 1 token to the unique recipient + let unique_recipient: ContractAddress = (index.into() + 9999).try_into().unwrap(); + UnruggableMemecoinInternalImpl::_transfer( + ref contract_state, owner, unique_recipient, 1 + ); + + index += 1; + }; + } + + + #[test] + fn test__transfer_no_holder_cap_after_launch() { + let owner = contract_address_const::<42>(); + let initial_supply = 1000.into(); + + // call contract constructor + let mut contract_state = UnruggableMemecoin::contract_state_for_testing(); + UnruggableMemecoin::constructor( + ref contract_state, owner, owner, 'UnruggableMemecoin', 'UM', initial_supply + ); + + // set owner as caller to bypass owner restrictions + start_prank(CheatTarget::All, owner); + + // launch memecoin + UnruggableEntrypoints::launch_memecoin(ref contract_state); + + // index starts from 1 because owner has initial supply + let mut index = 1; + loop { + if index == MAX_HOLDERS_BEFORE_LAUNCH + 1 { + break; + } + + // Transfer 1 token to the unique recipient + let unique_recipient: ContractAddress = (index.into() + 9999).try_into().unwrap(); + UnruggableMemecoinInternalImpl::_transfer( + ref contract_state, owner, unique_recipient, 1 + ); + + index += 1; + }; + } +} + + + From bef873a10258777f1baf920f743b5b483d535a48 Mon Sep 17 00:00:00 2001 From: Credence Date: Thu, 14 Dec 2023 02:23:46 +0100 Subject: [PATCH 3/4] run scarb fmt --- contracts/src/tokens/memecoin.cairo | 17 ++------ .../tests/test_unruggable_memecoin.cairo | 42 +++++++------------ 2 files changed, 20 insertions(+), 39 deletions(-) diff --git a/contracts/src/tokens/memecoin.cairo b/contracts/src/tokens/memecoin.cairo index 30d8a9dc..cee03275 100644 --- a/contracts/src/tokens/memecoin.cairo +++ b/contracts/src/tokens/memecoin.cairo @@ -63,7 +63,6 @@ mod UnruggableMemecoin { } - /// Constructor called once when the contract is deployed. /// # Arguments /// * `owner` - The owner of the contract. @@ -106,11 +105,10 @@ mod UnruggableMemecoin { fn launch_memecoin(ref self: ContractState) { // Checks: Only the owner can launch the memecoin. self.ownable.assert_only_owner(); - // Effects. + // Effects. // Launch the coin self.launched.write(true); - // Interactions. } } @@ -186,32 +184,25 @@ mod UnruggableMemecoin { // #[generate_trait] impl UnruggableMemecoinInternalImpl of UnruggableMemecoinInternalTrait { - #[inline(always)] fn _check_holders_limit(ref self: ContractState) { - // enforce max number of holders before launch if !self.launched.read() { let current_holders_count = self.pre_launch_holders_count.read(); assert( - current_holders_count < MAX_HOLDERS_BEFORE_LAUNCH, - Errors::MAX_HOLDERS_REACHED + current_holders_count < MAX_HOLDERS_BEFORE_LAUNCH, Errors::MAX_HOLDERS_REACHED ); self.pre_launch_holders_count.write(current_holders_count + 1); } } - fn _mint( - ref self: ContractState, - recipient: ContractAddress, - amount: u256 - ) { + fn _mint(ref self: ContractState, recipient: ContractAddress, amount: u256) { self._check_holders_limit(); self.erc20._mint(recipient, amount); } - + fn _transfer( ref self: ContractState, sender: ContractAddress, diff --git a/contracts/tests/test_unruggable_memecoin.cairo b/contracts/tests/test_unruggable_memecoin.cairo index d80e896e..72a4fd93 100644 --- a/contracts/tests/test_unruggable_memecoin.cairo +++ b/contracts/tests/test_unruggable_memecoin.cairo @@ -314,7 +314,6 @@ mod memecoin_entrypoints { memecoin.launch_memecoin(); assert(memecoin.launched(), 'Coin not launched'); - //TODO } @@ -336,14 +335,14 @@ mod memecoin_entrypoints { mod memecoin_internals { - use core::option::OptionTrait; - use snforge_std::{start_prank, CheatTarget}; - use starknet::{ContractAddress, contract_address_const}; - use unruggable::tokens::memecoin::UnruggableMemecoin; use UnruggableMemecoin::{ UnruggableMemecoinInternalImpl, SnakeEntrypoints, UnruggableEntrypoints, MAX_HOLDERS_BEFORE_LAUNCH }; + use core::option::OptionTrait; + use snforge_std::{start_prank, CheatTarget}; + use starknet::{ContractAddress, contract_address_const}; + use unruggable::tokens::memecoin::UnruggableMemecoin; #[test] @@ -359,19 +358,15 @@ mod memecoin_internals { ); // Transfer 100 tokens to the recipient - UnruggableMemecoinInternalImpl::_transfer( - ref contract_state, owner, recipient, 100 - ); + UnruggableMemecoinInternalImpl::_transfer(ref contract_state, owner, recipient, 100); // Check balance. Should be equal to initial supply - 100. - let owner_balance - = SnakeEntrypoints::balance_of(@contract_state, owner); + let owner_balance = SnakeEntrypoints::balance_of(@contract_state, owner); assert(owner_balance == (initial_supply - 100), 'Invalid balance owner'); // Check recipient balance. Should be equal to 100. - let recipient_balance - = SnakeEntrypoints::balance_of(@contract_state, recipient); - assert(recipient_balance == 100.into(), 'Invalid balance recipient'); + let recipient_balance = SnakeEntrypoints::balance_of(@contract_state, recipient); + assert(recipient_balance == 100.into(), 'Invalid balance recipient'); } @@ -387,7 +382,7 @@ mod memecoin_internals { ); // index starts from 1 because owner has initial supply - let mut index = 1; + let mut index = 1; loop { if index == MAX_HOLDERS_BEFORE_LAUNCH { break; @@ -400,19 +395,16 @@ mod memecoin_internals { ); // Check recipient balance. Should be equal to 1. - let recipient_balance - = SnakeEntrypoints::balance_of( - @contract_state, unique_recipient - ); + let recipient_balance = SnakeEntrypoints::balance_of(@contract_state, unique_recipient); assert(recipient_balance == 1.into(), 'Invalid balance recipient'); index += 1; - }; + }; } #[test] - #[should_panic(expected: ('memecoin: max holders reached', ))] + #[should_panic(expected: ('memecoin: max holders reached',))] fn test__transfer_above_holder_cap() { let owner = contract_address_const::<42>(); let initial_supply = 1000.into(); @@ -424,7 +416,7 @@ mod memecoin_internals { ); // index starts from 1 because owner has initial supply - let mut index = 1; + let mut index = 1; loop { if index == MAX_HOLDERS_BEFORE_LAUNCH + 1 { break; @@ -437,7 +429,7 @@ mod memecoin_internals { ); index += 1; - }; + }; } @@ -459,7 +451,7 @@ mod memecoin_internals { UnruggableEntrypoints::launch_memecoin(ref contract_state); // index starts from 1 because owner has initial supply - let mut index = 1; + let mut index = 1; loop { if index == MAX_HOLDERS_BEFORE_LAUNCH + 1 { break; @@ -472,9 +464,7 @@ mod memecoin_internals { ); index += 1; - }; + }; } } - - From 5edc524bac3faecc3bb74eb2b2bdf8c988275e7a Mon Sep 17 00:00:00 2001 From: Credence Date: Thu, 14 Dec 2023 15:10:35 +0100 Subject: [PATCH 4/4] add docs && update holder limit enforcement logic --- contracts/src/tokens/interface.cairo | 9 +++++ contracts/src/tokens/memecoin.cairo | 36 ++++++++++++++++--- .../tests/test_unruggable_memecoin.cairo | 35 ++++++++++++++++++ 3 files changed, 76 insertions(+), 4 deletions(-) diff --git a/contracts/src/tokens/interface.cairo b/contracts/src/tokens/interface.cairo index ff78a221..0c693924 100644 --- a/contracts/src/tokens/interface.cairo +++ b/contracts/src/tokens/interface.cairo @@ -35,6 +35,11 @@ trait IUnruggableMemecoin { // ************************************ // * Additional functions // ************************************ + + /// Checks whether token has launched + /// + /// # Returns + /// bool: whether token has launched fn launched(self: @TState) -> bool; fn launch_memecoin(ref self: TState); } @@ -62,6 +67,10 @@ trait IUnruggableMemecoinSnake { #[starknet::interface] trait IUnruggableAdditional { + /// Checks whether token has launched + /// + /// # Returns + /// bool: whether token has launched fn launched(self: @TState) -> bool; fn launch_memecoin(ref self: TState); } diff --git a/contracts/src/tokens/memecoin.cairo b/contracts/src/tokens/memecoin.cairo index cee03275..87bb27f0 100644 --- a/contracts/src/tokens/memecoin.cairo +++ b/contracts/src/tokens/memecoin.cairo @@ -184,11 +184,19 @@ mod UnruggableMemecoin { // #[generate_trait] impl UnruggableMemecoinInternalImpl of UnruggableMemecoinInternalTrait { + /// Internal function to enforce pre launch holder limit + /// + /// Note that when transfers are done, between addresses that already + /// hold tokens, we do not increment the number of holders. it only + /// gets incremented when the recipient that hold no tokens + /// + /// # Arguments + /// * `recipient` - The recipient of the tokens being transferred. #[inline(always)] - fn _check_holders_limit(ref self: ContractState) { + fn _enforce_holders_limit(ref self: ContractState, recipient: ContractAddress) { // enforce max number of holders before launch - if !self.launched.read() { + if !self.launched.read() && self.balance_of(recipient) == 0 { let current_holders_count = self.pre_launch_holders_count.read(); assert( current_holders_count < MAX_HOLDERS_BEFORE_LAUNCH, Errors::MAX_HOLDERS_REACHED @@ -198,18 +206,38 @@ mod UnruggableMemecoin { } } + + /// Internal function to mint tokens + /// + /// Before minting, a check is done to ensure that + /// only `MAX_HOLDERS_BEFORE_LAUNCH` addresses can hold + /// tokens if token hasn't launched + /// + /// # Arguments + /// * `recipient` - The recipient of the tokens. + /// * `amount` - The amount of tokens to be minted. fn _mint(ref self: ContractState, recipient: ContractAddress, amount: u256) { - self._check_holders_limit(); + self._enforce_holders_limit(recipient); self.erc20._mint(recipient, amount); } + /// Internal function to transfer tokens + /// + /// Before transferring, a check is done to ensure that + /// only `MAX_HOLDERS_BEFORE_LAUNCH` addresses can hold + /// tokens if token hasn't launched + /// + /// # Arguments + /// * `sender` - The sender or owner of the tokens. + /// * `recipient` - The recipient of the tokens. + /// * `amount` - The amount of tokens to be transferred. fn _transfer( ref self: ContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256 ) { - self._check_holders_limit(); + self._enforce_holders_limit(recipient); self.erc20._transfer(sender, recipient, amount); } } diff --git a/contracts/tests/test_unruggable_memecoin.cairo b/contracts/tests/test_unruggable_memecoin.cairo index 72a4fd93..96bf1651 100644 --- a/contracts/tests/test_unruggable_memecoin.cairo +++ b/contracts/tests/test_unruggable_memecoin.cairo @@ -403,6 +403,41 @@ mod memecoin_internals { } + #[test] + fn test__transfer_existing_holders() { + /// pre launch holder number should not change when + /// transfer is done to recipient(s) who already have tokens + + /// to test this, we are going to continously self transfer tokens + /// and ensure that we can transfer more than `MAX_HOLDERS_BEFORE_LAUNCH` times + + let owner = contract_address_const::<42>(); + let initial_supply = 1000.into(); + + // call contract constructor + let mut contract_state = UnruggableMemecoin::contract_state_for_testing(); + UnruggableMemecoin::constructor( + ref contract_state, owner, owner, 'UnruggableMemecoin', 'UM', initial_supply + ); + + // index starts from 1 because owner has initial supply + let mut index = 1; + loop { + if index == MAX_HOLDERS_BEFORE_LAUNCH + 1 { + break; + } + + // Self transfer tokens + + UnruggableMemecoinInternalImpl::_transfer( + ref contract_state, owner, owner, initial_supply + ); + + index += 1; + }; + } + + #[test] #[should_panic(expected: ('memecoin: max holders reached',))] fn test__transfer_above_holder_cap() {