Skip to content

Commit

Permalink
feat: implement maximum holders before launch safeguard (#15)
Browse files Browse the repository at this point in the history
implement maximum holders before launch safeguard #4
  • Loading branch information
credence0x authored Dec 18, 2023
1 parent 2355a9f commit efd01f6
Show file tree
Hide file tree
Showing 3 changed files with 247 additions and 2 deletions.
11 changes: 11 additions & 0 deletions contracts/src/tokens/interface.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ trait IUnruggableMemecoin<TState> {
// ************************************
// * Additional functions
// ************************************

/// Checks whether token has launched
///
/// # Returns
/// bool: whether token has launched
fn launched(self: @TState) -> bool;
fn launch_memecoin(ref self: TState);
}

Expand All @@ -61,5 +67,10 @@ trait IUnruggableMemecoinSnake<TState> {

#[starknet::interface]
trait IUnruggableAdditional<TState> {
/// Checks whether token has launched
///
/// # Returns
/// bool: whether token has launched
fn launched(self: @TState) -> bool;
fn launch_memecoin(ref self: TState);
}
65 changes: 63 additions & 2 deletions contracts/src/tokens/memecoin.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -56,6 +58,10 @@ 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
Expand All @@ -80,7 +86,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);
}

//
Expand All @@ -91,11 +97,18 @@ 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.
// Effects.

// Launch the coin
self.launched.write(true);
// Interactions.
}
}
Expand Down Expand Up @@ -171,12 +184,60 @@ 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 _enforce_holders_limit(ref self: ContractState, recipient: ContractAddress) {
// enforce max number of holders before launch

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
);

self.pre_launch_holders_count.write(current_holders_count + 1);
}
}


/// 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._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._enforce_holders_limit(recipient);
self.erc20._transfer(sender, recipient, amount);
}
}
Expand Down
173 changes: 173 additions & 0 deletions contracts/tests/test_unruggable_memecoin.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,8 @@ mod memecoin_entrypoints {

start_prank(CheatTarget::One(memecoin.contract_address), owner);
memecoin.launch_memecoin();

assert(memecoin.launched(), 'Coin not launched');
//TODO
}

Expand All @@ -330,3 +332,174 @@ mod memecoin_entrypoints {
memecoin.launch_memecoin();
}
}


mod memecoin_internals {
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]
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]
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() {
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;
};
}
}

0 comments on commit efd01f6

Please sign in to comment.