From 989f075b9133a2ea5a48ecae1a51f1fa0a5345a1 Mon Sep 17 00:00:00 2001 From: Charles Lanier Date: Wed, 10 Jan 2024 16:51:42 +0100 Subject: [PATCH] Max percentage buy provided at launch (#138) The max percentage buy limit was hardcoded to 2% before this PR, now it has to be provided during launch, and the value cannot fall behind 0.5% --- contracts/src/errors.cairo | 1 + contracts/src/factory/factory.cairo | 14 ++++++- contracts/src/factory/interface.cairo | 3 ++ .../src/tests/fork_tests/test_ekubo.cairo | 12 +++--- .../src/tests/fork_tests/test_jediswap.cairo | 15 ++++--- .../src/tests/unit_tests/test_factory.cairo | 39 ++++++++++++++++++- contracts/src/tests/unit_tests/utils.cairo | 2 + contracts/src/token/interface.cairo | 10 ++++- contracts/src/token/memecoin.cairo | 20 +++++++--- 9 files changed, 95 insertions(+), 21 deletions(-) diff --git a/contracts/src/errors.cairo b/contracts/src/errors.cairo index c855587f..e746d78b 100644 --- a/contracts/src/errors.cairo +++ b/contracts/src/errors.cairo @@ -8,3 +8,4 @@ const NOT_FACTORY: felt252 = 'Caller not factory'; const CALLER_NOT_OWNER: felt252 = 'Caller is not the owner'; const ALREADY_LAUNCHED: felt252 = 'Already launched'; const PRICE_ZERO: felt252 = 'Starting tick cannot be 0'; +const MAX_PERCENTAGE_BUY_LAUNCH_TOO_LOW: felt252 = 'Max percentage buy too low'; diff --git a/contracts/src/factory/factory.cairo b/contracts/src/factory/factory.cairo index f4c5731c..63fb45d4 100644 --- a/contracts/src/factory/factory.cairo +++ b/contracts/src/factory/factory.cairo @@ -126,6 +126,7 @@ mod Factory { ref self: ContractState, memecoin_address: ContractAddress, transfer_restriction_delay: u64, + max_percentage_buy_launch: u16, quote_address: ContractAddress, quote_amount: u256, unlock_time: u64, @@ -150,7 +151,12 @@ mod Factory { } ); - memecoin.set_launched(LiquidityType::ERC20(pair_address), :transfer_restriction_delay); + memecoin + .set_launched( + LiquidityType::ERC20(pair_address), + :transfer_restriction_delay, + :max_percentage_buy_launch + ); self .emit( MemecoinLaunched { @@ -164,6 +170,7 @@ mod Factory { ref self: ContractState, memecoin_address: ContractAddress, transfer_restriction_delay: u64, + max_percentage_buy_launch: u16, quote_address: ContractAddress, ekubo_parameters: EkuboPoolParameters, ) -> (u64, EkuboLP) { @@ -183,7 +190,10 @@ mod Factory { additional_parameters: ekubo_parameters ); - memecoin.set_launched(LiquidityType::NFT(id), :transfer_restriction_delay); + memecoin + .set_launched( + LiquidityType::NFT(id), :transfer_restriction_delay, :max_percentage_buy_launch + ); self .emit( MemecoinLaunched { diff --git a/contracts/src/factory/interface.cairo b/contracts/src/factory/interface.cairo index 7325b8a4..ab957ce1 100644 --- a/contracts/src/factory/interface.cairo +++ b/contracts/src/factory/interface.cairo @@ -46,6 +46,7 @@ trait IFactory { /// /// * `memecoin_address` - The address of the memecoin contract. /// * `transfer_restriction_delay` - The delay in seconds during which transfers will be limited to a % of max supply after launch. + /// * `max_percentage_buy_launch` - The max buyable amount in % of the max supply after launch and during the transfer restriction delay. /// * `quote_address` - The address of the quote token contract. /// * `quote_amount` - The amount of quote tokens to add as liquidity. /// * `unlock_time` - The timestamp when the liquidity can be unlocked. @@ -65,6 +66,7 @@ trait IFactory { ref self: TContractState, memecoin_address: ContractAddress, transfer_restriction_delay: u64, + max_percentage_buy_launch: u16, quote_address: ContractAddress, quote_amount: u256, unlock_time: u64, @@ -102,6 +104,7 @@ trait IFactory { ref self: TContractState, memecoin_address: ContractAddress, transfer_restriction_delay: u64, + max_percentage_buy_launch: u16, quote_address: ContractAddress, ekubo_parameters: EkuboPoolParameters, ) -> (u64, EkuboLP); diff --git a/contracts/src/tests/fork_tests/test_ekubo.cairo b/contracts/src/tests/fork_tests/test_ekubo.cairo index 3a2f762c..cfa554d8 100644 --- a/contracts/src/tests/fork_tests/test_ekubo.cairo +++ b/contracts/src/tests/fork_tests/test_ekubo.cairo @@ -29,7 +29,7 @@ use unruggable::tests::fork_tests::utils::{ }; use unruggable::tests::unit_tests::utils::{ OWNER, DEFAULT_MIN_LOCKTIME, pow_256, LOCK_MANAGER_ADDRESS, MEMEFACTORY_ADDRESS, RECIPIENT, - ALICE, DefaultTxInfoMock, TRANSFER_RESTRICTION_DELAY, + ALICE, DefaultTxInfoMock, TRANSFER_RESTRICTION_DELAY, MAX_PERCENTAGE_BUY_LAUNCH }; use unruggable::token::interface::{ IUnruggableMemecoinDispatcher, IUnruggableMemecoinDispatcherTrait @@ -49,6 +49,7 @@ fn launch_memecoin_on_ekubo( .launch_on_ekubo( memecoin_address, TRANSFER_RESTRICTION_DELAY, + MAX_PERCENTAGE_BUY_LAUNCH, quote_address, EkuboPoolParameters { fee, tick_spacing, starting_tick, bound } ); @@ -301,7 +302,7 @@ fn test_swap_token0_price_below_1() { }; // Check that swaps work correctly - let amount_in = 2 * pow_256(10, 16); + let amount_in = MAX_PERCENTAGE_BUY_LAUNCH.into() * pow_256(10, 14); swap_tokens_on_ekubo( token_in_address: quote_address, :amount_in, @@ -369,7 +370,7 @@ fn test_launch_meme_token1_price_below_1() { ); assert(reserve_memecoin > expected_reserve_lower_bound, 'reserves holds too few token'); - let amount_in = 2 * pow_256(10, 16); + let amount_in = MAX_PERCENTAGE_BUY_LAUNCH.into() * pow_256(10, 14); swap_tokens_on_ekubo( token_in_address: quote_address, :amount_in, @@ -437,7 +438,7 @@ fn test_launch_meme_token0_price_above_1() { assert(reserve_memecoin > expected_reserve_lower_bound, 'reserves holds too few token'); // Test that swaps work correctly - let amount_in = 2 * pow_256(10, 16); + let amount_in = MAX_PERCENTAGE_BUY_LAUNCH.into() * pow_256(10, 14); swap_tokens_on_ekubo( token_in_address: quote_address, :amount_in, @@ -506,7 +507,7 @@ fn test_launch_meme_token1_price_above_1() { assert(reserve_memecoin > expected_reserve_lower_bound, 'reserves holds too few token'); // Check that swaps work correctly - let amount_in = 2 * pow_256(10, 16); + let amount_in = MAX_PERCENTAGE_BUY_LAUNCH.into() * pow_256(10, 14); swap_tokens_on_ekubo( token_in_address: quote_address, :amount_in, @@ -618,6 +619,7 @@ fn test_cant_launch_twice() { .launch_on_ekubo( memecoin_address, TRANSFER_RESTRICTION_DELAY, + MAX_PERCENTAGE_BUY_LAUNCH, quote_address, EkuboPoolParameters { fee: 0xc49ba5e353f7d00000000000000000, diff --git a/contracts/src/tests/fork_tests/test_jediswap.cairo b/contracts/src/tests/fork_tests/test_jediswap.cairo index 9c9c2fb0..763c3bb5 100644 --- a/contracts/src/tests/fork_tests/test_jediswap.cairo +++ b/contracts/src/tests/fork_tests/test_jediswap.cairo @@ -13,7 +13,7 @@ use unruggable::tests::addresses::{JEDI_FACTORY_ADDRESS, JEDI_ROUTER_ADDRESS, ET use unruggable::tests::fork_tests::utils::{deploy_memecoin_through_factory_with_owner, sort_tokens}; use unruggable::tests::unit_tests::utils::{ OWNER, DEFAULT_MIN_LOCKTIME, pow_256, LOCK_MANAGER_ADDRESS, MEMEFACTORY_ADDRESS, - deploy_eth_with_owner, TRANSFER_RESTRICTION_DELAY + deploy_eth_with_owner, TRANSFER_RESTRICTION_DELAY, MAX_PERCENTAGE_BUY_LAUNCH }; use unruggable::token::interface::{IUnruggableMemecoinDispatcherTrait}; use unruggable::token::memecoin::LiquidityType; @@ -38,7 +38,12 @@ fn test_jediswap_integration() { let pair_address = factory .launch_on_jediswap( - memecoin_address, TRANSFER_RESTRICTION_DELAY, quote_address, amount, unlock_time + memecoin_address, + TRANSFER_RESTRICTION_DELAY, + MAX_PERCENTAGE_BUY_LAUNCH, + quote_address, + amount, + unlock_time ); let pair = IJediswapPairDispatcher { contract_address: pair_address }; @@ -50,10 +55,10 @@ fn test_jediswap_integration() { quote.approve(JEDI_ROUTER_ADDRESS(), 1 * pow_256(10, 18)); stop_prank(CheatTarget::One(quote.contract_address)); - // Max buy cap is 2% of total supply + // Max buy cap is `MAX_PERCENTAGE_BUY_LAUNCH` of total supply // Initial rate is roughly 1 ETH for 21M meme, - // so max buy is ~ 2% of 1 ETH = 0.02 ETH - let amount_in = 2 * pow_256(10, 16); + // so if max buy is ~ 2% of 1 ETH = 0.02 ETH + let amount_in = MAX_PERCENTAGE_BUY_LAUNCH.into() * pow_256(10, 14); start_prank(CheatTarget::One(router.contract_address), owner); let first_swap = router .swap_exact_tokens_for_tokens( diff --git a/contracts/src/tests/unit_tests/test_factory.cairo b/contracts/src/tests/unit_tests/test_factory.cairo index 3565f1a2..34aa55ad 100644 --- a/contracts/src/tests/unit_tests/test_factory.cairo +++ b/contracts/src/tests/unit_tests/test_factory.cairo @@ -20,7 +20,7 @@ use unruggable::tests::unit_tests::utils::{ SYMBOL, DEFAULT_INITIAL_SUPPLY, INITIAL_HOLDERS, INITIAL_HOLDERS_AMOUNTS, SALT, deploy_memecoin_through_factory, MEMEFACTORY_ADDRESS, deploy_memecoin_through_factory_with_owner, pow_256, LOCK_MANAGER_ADDRESS, DEFAULT_MIN_LOCKTIME, - deploy_and_launch_memecoin, TRANSFER_RESTRICTION_DELAY + deploy_and_launch_memecoin, TRANSFER_RESTRICTION_DELAY, MAX_PERCENTAGE_BUY_LAUNCH }; use unruggable::token::interface::{ IUnruggableMemecoin, IUnruggableMemecoinDispatcher, IUnruggableMemecoinDispatcherTrait @@ -141,6 +141,7 @@ fn test_launch_memecoin_happy_path() { .launch_on_jediswap( memecoin_address, TRANSFER_RESTRICTION_DELAY, + MAX_PERCENTAGE_BUY_LAUNCH, eth.contract_address, eth_amount, DEFAULT_MIN_LOCKTIME, @@ -196,6 +197,34 @@ fn test_launch_memecoin_already_launched() { .launch_on_jediswap( memecoin_address, TRANSFER_RESTRICTION_DELAY, + MAX_PERCENTAGE_BUY_LAUNCH, + eth.contract_address, + eth_amount, + DEFAULT_MIN_LOCKTIME, + ); +} + +#[test] +#[should_panic(expected: ('Max percentage buy too low',))] +fn test_launch_memecoin_with_percentage_buy_launch_too_low() { + let owner = snforge_std::test_address(); + let (memecoin, memecoin_address) = deploy_memecoin_through_factory_with_owner(owner); + let factory = IFactoryDispatcher { contract_address: MEMEFACTORY_ADDRESS() }; + let eth = ERC20ABIDispatcher { contract_address: ETH_ADDRESS() }; + + // approve spending of eth by factory + let eth_amount: u256 = 1 * pow_256(10, 18); // 1 ETHER + let factory_balance_meme = memecoin.balanceOf(factory.contract_address); + start_prank(CheatTarget::One(eth.contract_address), owner); + eth.approve(factory.contract_address, eth_amount); + stop_prank(CheatTarget::One(eth.contract_address)); + + start_prank(CheatTarget::One(factory.contract_address), owner); + let pair_address = factory + .launch_on_jediswap( + memecoin_address, + TRANSFER_RESTRICTION_DELAY, + 49, // 0.49% eth.contract_address, eth_amount, DEFAULT_MIN_LOCKTIME, @@ -209,7 +238,12 @@ fn test_launch_memecoin_not_owner() { let factory = IFactoryDispatcher { contract_address: MEMEFACTORY_ADDRESS() }; let pair_address = factory .launch_on_jediswap( - memecoin_address, TRANSFER_RESTRICTION_DELAY, ETH_ADDRESS(), 1, DEFAULT_MIN_LOCKTIME, + memecoin_address, + TRANSFER_RESTRICTION_DELAY, + MAX_PERCENTAGE_BUY_LAUNCH, + ETH_ADDRESS(), + 1, + DEFAULT_MIN_LOCKTIME, ); } @@ -228,6 +262,7 @@ fn test_launch_memecoin_amm_not_whitelisted() { .launch_on_ekubo( memecoin_address, TRANSFER_RESTRICTION_DELAY, + MAX_PERCENTAGE_BUY_LAUNCH, eth.contract_address, EkuboPoolParameters { fee: 0, tick_spacing: 0, starting_tick: i129 { sign: false, mag: 0 }, bound: 0 diff --git a/contracts/src/tests/unit_tests/utils.cairo b/contracts/src/tests/unit_tests/utils.cairo index c93c3a3e..e4eed8b0 100644 --- a/contracts/src/tests/unit_tests/utils.cairo +++ b/contracts/src/tests/unit_tests/utils.cairo @@ -84,6 +84,7 @@ fn UNLOCK_TIME() -> u64 { const ETH_DECIMALS: u8 = 18; const TRANSFER_RESTRICTION_DELAY: u64 = 1000; +const MAX_PERCENTAGE_BUY_LAUNCH: u16 = 200; // 2% fn MEMEFACTORY_ADDRESS() -> ContractAddress { @@ -274,6 +275,7 @@ fn deploy_and_launch_memecoin() -> (IUnruggableMemecoinDispatcher, ContractAddre .launch_on_jediswap( memecoin_address, TRANSFER_RESTRICTION_DELAY, + MAX_PERCENTAGE_BUY_LAUNCH, eth.contract_address, eth_amount, DEFAULT_MIN_LOCKTIME, diff --git a/contracts/src/token/interface.cairo b/contracts/src/token/interface.cairo index 6b7afa96..ee1d28a0 100644 --- a/contracts/src/token/interface.cairo +++ b/contracts/src/token/interface.cairo @@ -52,7 +52,10 @@ trait IUnruggableMemecoin { fn get_team_allocation(self: @TState) -> u256; fn memecoin_factory_address(self: @TState) -> ContractAddress; fn set_launched( - ref self: TState, liquidity_type: LiquidityType, transfer_restriction_delay: u64 + ref self: TState, + liquidity_type: LiquidityType, + transfer_restriction_delay: u64, + max_percentage_buy_launch: u16 ); } @@ -113,6 +116,9 @@ trait IUnruggableAdditional { /// * The memecoin has already been launched (error code: `errors::ALREADY_LAUNCHED`). /// fn set_launched( - ref self: TState, liquidity_type: LiquidityType, transfer_restriction_delay: u64 + ref self: TState, + liquidity_type: LiquidityType, + transfer_restriction_delay: u64, + max_percentage_buy_launch: u16 ); } diff --git a/contracts/src/token/memecoin.cairo b/contracts/src/token/memecoin.cairo index 7bf3248d..29966e40 100644 --- a/contracts/src/token/memecoin.cairo +++ b/contracts/src/token/memecoin.cairo @@ -61,9 +61,8 @@ mod UnruggableMemecoin { /// The maximum percentage of the total supply that can be allocated to the team. /// This is to prevent the team from having too much control over the supply. const MAX_SUPPLY_PERCENTAGE_TEAM_ALLOCATION: u16 = 1_000; // 10% - /// The maximum percentage of the supply that can be bought at once. - //TODO: discuss whether this should be a constant or a parameter - const MAX_PERCENTAGE_BUY_LAUNCH: u8 = 200; // 2% + /// The minimum maximum percentage of the supply that can be bought at once. + const MIN_MAX_PERCENTAGE_BUY_LAUNCH: u16 = 50; // 0.5% #[storage] struct Storage { @@ -75,6 +74,7 @@ mod UnruggableMemecoin { launch_time: u64, factory_contract: ContractAddress, liquidity_type: Option, + max_percentage_buy_launch: u16, // Components. #[substorage(v0)] ownable: OwnableComponent::Storage, @@ -145,16 +145,24 @@ mod UnruggableMemecoin { } fn set_launched( - ref self: ContractState, liquidity_type: LiquidityType, transfer_restriction_delay: u64 + ref self: ContractState, + liquidity_type: LiquidityType, + transfer_restriction_delay: u64, + max_percentage_buy_launch: u16 ) { self.assert_only_factory(); assert(!self.is_launched(), errors::ALREADY_LAUNCHED); + assert( + max_percentage_buy_launch >= MIN_MAX_PERCENTAGE_BUY_LAUNCH, + errors::MAX_PERCENTAGE_BUY_LAUNCH_TOO_LOW + ); self.liquidity_type.write(Option::Some(liquidity_type)); self.launch_time.write(get_block_timestamp()); // Enable a transfer limit - until this time has passed, // transfers are limited to a certain amount. + self.max_percentage_buy_launch.write(max_percentage_buy_launch); self.transfer_restriction_delay.write(transfer_restriction_delay); // renounce ownership @@ -314,7 +322,9 @@ mod UnruggableMemecoin { } assert( - amount <= self.total_supply().percent_mul(MAX_PERCENTAGE_BUY_LAUNCH.into()), + amount <= self + .total_supply() + .percent_mul(self.max_percentage_buy_launch.read().into()), 'Max buy cap reached' );