diff --git a/js/packages/cli/src/fair-launch-cli.ts b/js/packages/cli/src/fair-launch-cli.ts index 2f4f144..8c3bcb7 100755 --- a/js/packages/cli/src/fair-launch-cli.ts +++ b/js/packages/cli/src/fair-launch-cli.ts @@ -726,6 +726,7 @@ program const exists = await anchorProgram.provider.connection.getAccountInfo( fairLaunchLotteryBitmap, ); + if (!exists) { await anchorProgram.rpc.createFairLaunchLotteryBitmap(bump, { accounts: { diff --git a/rust/fair-launch/src/lib.rs b/rust/fair-launch/src/lib.rs index 4b58fe5..35de826 100644 --- a/rust/fair-launch/src/lib.rs +++ b/rust/fair-launch/src/lib.rs @@ -3,8 +3,9 @@ pub mod utils; use { crate::utils::{ adjust_counts, assert_data_valid, assert_derivation, assert_initialized, assert_owned_by, - assert_valid_amount, create_or_allocate_account_raw, get_mask_and_index_for_seq, - spl_token_mint_to, spl_token_transfer, TokenTransferParams, + assert_valid_amount, calculate_refund_amount, calculate_withdraw_amount, + create_or_allocate_account_raw, get_mask_and_index_for_seq, spl_token_burn, + spl_token_mint_to, spl_token_transfer, TokenBurnParams, TokenTransferParams, }, anchor_lang::{ prelude::*, @@ -660,6 +661,7 @@ pub mod fair_launch { let fair_launch = &mut ctx.accounts.fair_launch; let treasury = &mut ctx.accounts.treasury; let authority = &mut ctx.accounts.authority; + let token_mint = &ctx.accounts.token_mint; if fair_launch.number_tickets_sold > fair_launch.number_tickets_dropped + fair_launch.number_tickets_punched @@ -671,9 +673,12 @@ pub mod fair_launch { return Err(ErrorCode::CannotCashOutUntilPhaseThree.into()); } + let mint: spl_token::state::Mint = assert_initialized(token_mint)?; + let tokens = mint.supply; + let signer_seeds = [ PREFIX.as_bytes(), - fair_launch.token_mint.as_ref(), + &token_mint.key.as_ref(), &[fair_launch.bump], ]; @@ -719,17 +724,39 @@ pub mod fair_launch { return Err(ErrorCode::AccountShouldHaveNoDelegates.into()); } + if fair_launch.treasury_snapshot.is_none() { + fair_launch.treasury_snapshot = Some(treasury_account.amount) + } + + let amount = calculate_withdraw_amount( + &fair_launch.data, + tokens, + fair_launch.treasury_snapshot.unwrap(), + treasury_account.amount, + )?; + spl_token_transfer(TokenTransferParams { source: treasury.to_account_info(), destination: authority_token_account_info.clone(), authority: fair_launch.to_account_info(), authority_signer_seeds: &signer_seeds, token_program: token_program.clone(), - amount: treasury_account.amount, + amount, })?; } else { + if fair_launch.treasury_snapshot.is_none() { + fair_launch.treasury_snapshot = Some(treasury.lamports()) + } + + let amount = calculate_withdraw_amount( + &fair_launch.data, + tokens, + fair_launch.treasury_snapshot.unwrap(), + treasury.lamports(), + )?; + invoke( - &system_instruction::transfer(treasury.key, authority.key, treasury.lamports()), + &system_instruction::transfer(treasury.key, authority.key, amount), &[ treasury.to_account_info(), authority.clone(), @@ -742,6 +769,117 @@ pub mod fair_launch { } } +pub fn receive_refund<'info>( + ctx: Context<'_, '_, '_, 'info, ReceiveRefund<'info>>, +) -> ProgramResult { + let fair_launch = &mut ctx.accounts.fair_launch; + let treasury = &mut ctx.accounts.treasury; + let buyer = &mut ctx.accounts.buyer; + let token_mint = &ctx.accounts.token_mint; + let token_program = &ctx.accounts.token_program; + let clock = &ctx.accounts.clock; + let buyer_token_account = &mut ctx.accounts.buyer_token_account; + let transfer_authority = &mut ctx.accounts.transfer_authority; + + let signer_seeds = [ + PREFIX.as_bytes(), + &token_mint.key.as_ref(), + &[fair_launch.bump], + ]; + + if fair_launch.number_tickets_sold + > fair_launch.number_tickets_dropped + fair_launch.number_tickets_punched + { + return Err(ErrorCode::CannotRefundUntilAllTicketsHaveBeenPunchedOrDropped.into()); + } + + if !fair_launch.phase_three_started { + return Err(ErrorCode::CannotRefundUntilPhaseThree.into()); + } + + fair_launch.number_tokens_burned_for_refunds = fair_launch + .number_tokens_burned_for_refunds + .checked_add(1) + .ok_or(ErrorCode::NumericalOverflowError)?; + + spl_token_burn(TokenBurnParams { + mint: token_mint.clone(), + source: buyer_token_account.clone(), + amount: 1, + authority: transfer_authority.clone(), + authority_signer_seeds: None, + token_program: token_program.clone(), + })?; + + if let Some(treasury_mint) = fair_launch.treasury_mint { + let treasury_mint_info = &ctx.remaining_accounts[0]; + let _treasury_mint: spl_token::state::Mint = assert_initialized(&treasury_mint_info)?; + + let buyer_payment_account_info = &ctx.remaining_accounts[1]; + let buyer_payment_account: Account = assert_initialized(&buyer_payment_account_info)?; + let treasury_account: Account = assert_initialized(treasury)?; + + if *treasury_mint_info.key != treasury_mint { + return Err(ErrorCode::TreasuryMintMismatch.into()); + } + + assert_owned_by(treasury_mint_info, &token_program.key)?; + assert_owned_by(buyer_payment_account_info, &token_program.key)?; + assert_owned_by(treasury, &token_program.key)?; + + if buyer_payment_account.mint != *treasury_mint_info.key { + return Err(ErrorCode::TreasuryMintMismatch.into()); + } + + // assert is an ATA + assert_derivation( + &Pubkey::from_str(ASSOCIATED_TOKEN_PROGRAM_ID).unwrap(), + buyer_payment_account_info, + &[ + buyer.key.as_ref(), + token_program.key.as_ref(), + &treasury_mint_info.key.as_ref(), + ], + )?; + + if buyer_payment_account.delegate.is_some() { + return Err(ErrorCode::AccountShouldHaveNoDelegates.into()); + } + + if fair_launch.treasury_snapshot.is_none() { + fair_launch.treasury_snapshot = Some(treasury_account.amount) + } + + let amount = calculate_refund_amount(fair_launch, clock.unix_timestamp)?; + + spl_token_transfer(TokenTransferParams { + source: treasury.to_account_info(), + destination: buyer_payment_account_info.clone(), + authority: fair_launch.to_account_info(), + authority_signer_seeds: &signer_seeds, + token_program: token_program.clone(), + amount, + })?; + } else { + if fair_launch.treasury_snapshot.is_none() { + fair_launch.treasury_snapshot = Some(treasury.lamports()) + } + + let amount = calculate_refund_amount(fair_launch, clock.unix_timestamp)?; + + invoke( + &system_instruction::transfer(treasury.key, buyer.key, amount), + &[ + treasury.to_account_info(), + buyer.clone(), + ctx.accounts.system_program.clone(), + ], + )?; + } + + Ok(()) +} + #[derive(Accounts)] #[instruction(bump: u8, treasury_bump: u8, token_mint_bump: u8, data: FairLaunchData)] pub struct InitializeFairLaunch<'info> { @@ -901,12 +1039,14 @@ pub struct PunchTicket<'info> { #[derive(Accounts)] pub struct WithdrawFunds<'info> { - #[account(seeds=[PREFIX.as_bytes(), fair_launch.token_mint.as_ref()], bump=fair_launch.bump, has_one=authority, has_one=treasury)] + #[account(mut, seeds=[PREFIX.as_bytes(), fair_launch.token_mint.as_ref()], bump=fair_launch.bump, has_one=authority, has_one=treasury)] fair_launch: ProgramAccount<'info, FairLaunch>, #[account(mut)] treasury: AccountInfo<'info>, #[account(mut)] authority: AccountInfo<'info>, + #[account(mut, seeds=[PREFIX.as_bytes(), fair_launch.authority.as_ref(), MINT.as_bytes(), fair_launch.data.uuid.as_bytes()], bump=fair_launch.token_mint_bump)] + token_mint: AccountInfo<'info>, #[account(address = system_program::ID)] system_program: AccountInfo<'info>, // Remaining accounts in this order if using spl tokens for payment: @@ -915,6 +1055,30 @@ pub struct WithdrawFunds<'info> { // [optional] token program } +#[derive(Accounts)] +pub struct ReceiveRefund<'info> { + #[account(mut, seeds=[PREFIX.as_bytes(), fair_launch.token_mint.as_ref()], bump=fair_launch.bump, has_one=treasury)] + fair_launch: ProgramAccount<'info, FairLaunch>, + #[account(mut)] + treasury: AccountInfo<'info>, + #[account(mut)] + buyer: AccountInfo<'info>, + #[account(mut)] + buyer_token_account: AccountInfo<'info>, + #[account(signer)] + transfer_authority: AccountInfo<'info>, + #[account(mut, seeds=[PREFIX.as_bytes(), fair_launch.authority.as_ref(), MINT.as_bytes(), fair_launch.data.uuid.as_bytes()], bump=fair_launch.token_mint_bump)] + token_mint: AccountInfo<'info>, + #[account(address = spl_token::id())] + token_program: AccountInfo<'info>, + #[account(address = system_program::ID)] + system_program: AccountInfo<'info>, + clock: Sysvar<'info, Clock>, + // Remaining accounts in this order if using spl tokens for payment: + // [Writable/optional] treasury mint + // [Writable/optional] buyer payment token account (must be ata) +} + pub const FAIR_LAUNCH_LOTTERY_SIZE: usize = 8 + // discriminator 32 + // fair launch 1 + // bump @@ -944,7 +1108,10 @@ pub const FAIR_LAUNCH_SPACE_VEC_START: usize = 8 + // discriminator 8 + // number of tickets sold 8 + // number of tickets dropped 8 + // number of tickets punched +8 + // number of tokens burned for refunds +8 + // number of tokens preminted 1 + // phase three started +9 + // treasury snapshot 8 + // current median, 4 + // u32 representing number of amounts in vec so far 100; // padding @@ -1000,11 +1167,22 @@ pub struct FairLaunch { pub treasury_bump: u8, pub token_mint_bump: u8, pub data: FairLaunchData, + /// Tickets that are missing a corresponding seq pda. Crank it. pub number_tickets_un_seqed: u64, + /// If I have to explain this, you're an idiot. pub number_tickets_sold: u64, + /// People that withdrew in phase 2 because they dislike you. pub number_tickets_dropped: u64, + /// People who won the lottery and punched ticket in exchange for token. Good job! pub number_tickets_punched: u64, + /// if you go past refund date, here is how many people lost faith in you. + pub number_tokens_burned_for_refunds: u64, + /// here is how many tokens you preminted before people had access. SHAME. *bell* + pub number_tokens_preminted: u64, + /// Yes. pub phase_three_started: bool, + /// Snapshot of treasury taken on first withdrawal. + pub treasury_snapshot: Option, pub current_median: u64, pub counts_at_each_tick: Vec, } @@ -1121,4 +1299,22 @@ pub enum ErrorCode { PhaseTwoEnded, #[msg("Cannot punch ticket when having paid less than median.")] CannotPunchTicketWhenHavingPaidLessThanMedian, + #[msg("You have already withdrawn your seed capital alotment from the treasury.")] + AlreadyWithdrawnCapitalAlotment, + #[msg("No anti rug settings on this fair launch. Should've checked twice.")] + NoAntiRugSetting, + #[msg("Self destruct date has not passed yet, so you are not eligible for a refund.")] + SelfDestructNotPassed, + #[msg("Token burn failed")] + TokenBurnFailed, + #[msg("No treasury snapshot present")] + NoTreasurySnapshot, + #[msg("Cannot refund until all existing tickets have been dropped or punched")] + CannotRefundUntilAllTicketsHaveBeenPunchedOrDropped, + #[msg("Cannot refund until phase three")] + CannotRefundUntilPhaseThree, + #[msg("Invalid reserve bp")] + InvalidReserveBp, + #[msg("Anti Rug Token Requirement must be less than or equal to number of tokens being sold")] + InvalidAntiRugTokenRequirement, } diff --git a/rust/fair-launch/src/utils.rs b/rust/fair-launch/src/utils.rs index f62e5ab..f0abad8 100644 --- a/rust/fair-launch/src/utils.rs +++ b/rust/fair-launch/src/utils.rs @@ -207,6 +207,16 @@ pub fn assert_data_valid(data: &FairLaunchData) -> ProgramResult { return Err(ErrorCode::InvalidPriceRanges.into()); } + if let Some(anti_rug) = &data.anti_rug_setting { + if anti_rug.reserve_bp > 10000 { + return Err(ErrorCode::InvalidReserveBp.into()); + } + + if anti_rug.token_requirement > data.number_of_tokens { + return Err(ErrorCode::InvalidAntiRugTokenRequirement.into()); + } + } + let difference = data .price_range_end .checked_sub(data.price_range_start) @@ -229,6 +239,86 @@ pub fn assert_data_valid(data: &FairLaunchData) -> ProgramResult { Ok(()) } +pub fn calculate_refund_amount( + fair_launch: &ProgramAccount, + unix_timestamp: i64, +) -> Result { + if let Some(anti_rug) = &fair_launch.data.anti_rug_setting { + if unix_timestamp < anti_rug.self_destruct_date { + return Err(ErrorCode::SelfDestructNotPassed.into()); + } + if let Some(snapshot) = fair_launch.treasury_snapshot { + let reserve_size = snapshot + .checked_sub(get_expected_capital_alotment_size( + anti_rug.reserve_bp, + snapshot, + )?) + .ok_or(ErrorCode::NumericalOverflowError)?; + + msg!( + "calculated reserve size total is {} dividing by number tickets punched {}", + reserve_size, + fair_launch.number_tickets_punched + ); + + let my_slice = (reserve_size) + .checked_div(fair_launch.number_tickets_punched) + .ok_or(ErrorCode::NumericalOverflowError)?; + + msg!("My slice is {}", my_slice); + + Ok(my_slice) + } else { + return Err(ErrorCode::NoTreasurySnapshot.into()); + } + } else { + return Err(ErrorCode::NoAntiRugSetting.into()); + } +} + +pub fn calculate_withdraw_amount( + data: &FairLaunchData, + supply: u64, + snapshot: u64, + real_amount: u64, +) -> Result { + let amount_to_withdraw = if let Some(anti_rug) = &data.anti_rug_setting { + if supply <= anti_rug.token_requirement { + msg!("Deal satisfied. You can withdraw it all!"); + real_amount + } else { + if snapshot != real_amount { + return Err(ErrorCode::AlreadyWithdrawnCapitalAlotment.into()); + } + get_expected_capital_alotment_size(anti_rug.reserve_bp, snapshot)? + } + } else { + real_amount + }; + + Ok(amount_to_withdraw) +} + +pub fn get_expected_capital_alotment_size( + reserve_bp: u16, + snapshot: u64, +) -> Result { + let non_reserve_frac: u128 = 10000u128 - reserve_bp as u128; + msg!("Non reserve frac {}", non_reserve_frac); + let numerator: u128 = (snapshot as u128) + .checked_mul(non_reserve_frac) + .ok_or(ErrorCode::NumericalOverflowError)?; + msg!("Numerator {}", numerator); + let divided = numerator + .checked_div(10000) + .ok_or(ErrorCode::NumericalOverflowError)?; + msg!( + "Numerator divided by 10000 {} is amount to withdrawal", + divided + ); + Ok(divided as u64) +} + pub fn assert_valid_amount(data: &FairLaunchData, amount: u64) -> ProgramResult { if amount < data.price_range_start || amount > data.price_range_end { return Err(ErrorCode::InvalidPurchaseAmount.into()); @@ -327,3 +417,47 @@ pub fn spl_token_mint_to<'a: 'b, 'b>( ); result.map_err(|_| ErrorCode::TokenMintToFailed.into()) } + +/// TokenBurnParams +pub struct TokenBurnParams<'a: 'b, 'b> { + /// mint + pub mint: AccountInfo<'a>, + /// source + pub source: AccountInfo<'a>, + /// amount + pub amount: u64, + /// authority + pub authority: AccountInfo<'a>, + /// authority_signer_seeds + pub authority_signer_seeds: Option<&'b [&'b [u8]]>, + /// token_program + pub token_program: AccountInfo<'a>, +} + +pub fn spl_token_burn(params: TokenBurnParams<'_, '_>) -> ProgramResult { + let TokenBurnParams { + mint, + source, + authority, + token_program, + amount, + authority_signer_seeds, + } = params; + let mut seeds: Vec<&[&[u8]]> = vec![]; + if let Some(seed) = authority_signer_seeds { + seeds.push(seed); + } + let result = invoke_signed( + &spl_token::instruction::burn( + token_program.key, + source.key, + mint.key, + authority.key, + &[], + amount, + )?, + &[source, mint, authority, token_program], + seeds.as_slice(), + ); + result.map_err(|_| ErrorCode::TokenBurnFailed.into()) +}