diff --git a/Anchor.toml b/Anchor.toml index 87abaee6e..5cda24af4 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -64,6 +64,8 @@ v07-claim-launch-additional-tokens = "yarn run tsx scripts/v0.7/claimLaunchAddit v07-remove-proposal = "yarn run tsx scripts/v0.7/removeProposal.ts" v07-audit-liquidity-position-authorities = "yarn run tsx scripts/v0.7/auditLiquidityPositionAuthorities.ts" v07-fix-position-authorities = "yarn run tsx scripts/v0.7/fixPositionAuthorities.ts" +v07-dump-launches-funding-records = "yarn run tsx scripts/v0.7/dumpLaunchesAndFundingRecords.ts" +v07-resize-launches-funding-records = "yarn run tsx scripts/v0.7/resizeLaunchesAndFundingRecords.ts" [test] startup_wait = 5000 diff --git a/programs/v07_launchpad/src/error.rs b/programs/v07_launchpad/src/error.rs index 10e6c5248..73fd0b95b 100644 --- a/programs/v07_launchpad/src/error.rs +++ b/programs/v07_launchpad/src/error.rs @@ -62,4 +62,6 @@ pub enum LaunchpadError { PerformancePackageAlreadyInitialized, #[msg("Invalid DAO")] InvalidDao, + #[msg("Accumulator activation delay must be less than the launch duration")] + InvalidAccumulatorActivationDelaySeconds, } diff --git a/programs/v07_launchpad/src/events.rs b/programs/v07_launchpad/src/events.rs index 7b5422197..3a04011c5 100644 --- a/programs/v07_launchpad/src/events.rs +++ b/programs/v07_launchpad/src/events.rs @@ -39,6 +39,7 @@ pub struct LaunchInitializedEvent { pub seconds_for_launch: u32, pub additional_tokens_amount: u64, pub additional_tokens_recipient: Option, + pub accumulator_activation_delay_seconds: u32, } #[event] @@ -58,6 +59,7 @@ pub struct LaunchFundedEvent { pub amount: u64, pub total_committed_by_funder: u64, pub total_committed: u64, + pub committed_amount_accumulator: u128, } #[event] diff --git a/programs/v07_launchpad/src/instructions/fund.rs b/programs/v07_launchpad/src/instructions/fund.rs index 6aa55ccdb..9c45caa69 100644 --- a/programs/v07_launchpad/src/instructions/fund.rs +++ b/programs/v07_launchpad/src/instructions/fund.rs @@ -86,8 +86,23 @@ impl Fund<'_> { )?; let funding_record = &mut ctx.accounts.funding_record; + let clock = Clock::get()?; if funding_record.funder == ctx.accounts.funder.key() { + // Existing funding record — flush accumulator before changing committed_amount + let activation_timestamp = ctx.accounts.launch.unix_timestamp_started.unwrap() + + ctx.accounts.launch.accumulator_activation_delay_seconds as i64; + let now = clock.unix_timestamp; + + if funding_record.last_accumulator_update > 0 && now > activation_timestamp { + let period_start = + std::cmp::max(funding_record.last_accumulator_update, activation_timestamp); + let elapsed = now - period_start; + funding_record.committed_amount_accumulator += + (funding_record.committed_amount as u128) * (elapsed as u128); + } + + funding_record.last_accumulator_update = now; funding_record.committed_amount += amount; } else { funding_record.set_inner(FundingRecord { @@ -98,6 +113,8 @@ impl Fund<'_> { is_tokens_claimed: false, is_usdc_refunded: false, approved_amount: 0, + committed_amount_accumulator: 0, + last_accumulator_update: clock.unix_timestamp, }); } @@ -106,7 +123,6 @@ impl Fund<'_> { ctx.accounts.launch.seq_num += 1; - let clock = Clock::get()?; emit_cpi!(LaunchFundedEvent { common: CommonFields::new(&clock, ctx.accounts.launch.seq_num), launch: ctx.accounts.launch.key(), @@ -115,6 +131,7 @@ impl Fund<'_> { total_committed: ctx.accounts.launch.total_committed_amount, funding_record: funding_record.key(), total_committed_by_funder: funding_record.committed_amount, + committed_amount_accumulator: funding_record.committed_amount_accumulator, }); Ok(()) diff --git a/programs/v07_launchpad/src/instructions/initialize_launch.rs b/programs/v07_launchpad/src/instructions/initialize_launch.rs index 262c2aec6..7fb0d317b 100644 --- a/programs/v07_launchpad/src/instructions/initialize_launch.rs +++ b/programs/v07_launchpad/src/instructions/initialize_launch.rs @@ -30,6 +30,7 @@ pub struct InitializeLaunchArgs { pub months_until_insiders_can_unlock: u8, pub team_address: Pubkey, pub additional_tokens_amount: u64, + pub accumulator_activation_delay_seconds: u32, } #[event_cpi] @@ -125,6 +126,12 @@ impl InitializeLaunch<'_> { LaunchpadError::InvalidSecondsForLaunch ); + require_gt!( + args.seconds_for_launch, + args.accumulator_activation_delay_seconds, + LaunchpadError::InvalidAccumulatorActivationDelaySeconds + ); + require!( self.base_mint.freeze_authority.is_none(), LaunchpadError::FreezeAuthoritySet @@ -239,6 +246,7 @@ impl InitializeLaunch<'_> { additional_tokens_claimed: false, unix_timestamp_completed: None, is_performance_package_initialized: false, + accumulator_activation_delay_seconds: args.accumulator_activation_delay_seconds, }); let clock = Clock::get()?; @@ -266,6 +274,7 @@ impl InitializeLaunch<'_> { .additional_tokens_recipient .as_ref() .map(|a| a.key()), + accumulator_activation_delay_seconds: args.accumulator_activation_delay_seconds, }); let launch_key = ctx.accounts.launch.key(); diff --git a/programs/v07_launchpad/src/instructions/mod.rs b/programs/v07_launchpad/src/instructions/mod.rs index 5d39ee562..e3441167c 100644 --- a/programs/v07_launchpad/src/instructions/mod.rs +++ b/programs/v07_launchpad/src/instructions/mod.rs @@ -6,6 +6,8 @@ pub mod fund; pub mod initialize_launch; pub mod initialize_performance_package; pub mod refund; +pub mod resize_funding_record; +pub mod resize_launch; pub mod set_funding_record_approval; pub mod start_launch; @@ -17,5 +19,7 @@ pub use fund::*; pub use initialize_launch::*; pub use initialize_performance_package::*; pub use refund::*; +pub use resize_funding_record::*; +pub use resize_launch::*; pub use set_funding_record_approval::*; pub use start_launch::*; diff --git a/programs/v07_launchpad/src/instructions/resize_funding_record.rs b/programs/v07_launchpad/src/instructions/resize_funding_record.rs new file mode 100644 index 000000000..2eb79a715 --- /dev/null +++ b/programs/v07_launchpad/src/instructions/resize_funding_record.rs @@ -0,0 +1,73 @@ +use anchor_lang::{prelude::*, system_program, Discriminator}; + +use crate::state::{FundingRecord, OldFundingRecord}; +use crate::ID; + +#[derive(Accounts)] +pub struct ResizeFundingRecord<'info> { + /// CHECK: we check the discriminator, owner, and size + #[account(mut)] + pub funding_record: UncheckedAccount<'info>, + #[account(mut)] + pub payer: Signer<'info>, + pub system_program: Program<'info, System>, +} + +impl ResizeFundingRecord<'_> { + pub fn handle(ctx: Context) -> Result<()> { + let funding_record = &mut ctx.accounts.funding_record; + + // Owner check + require_eq!(funding_record.owner, &ID); + + // Discriminator check + let is_discriminator_correct = + funding_record.data.try_borrow_mut().unwrap()[..8] == FundingRecord::discriminator(); + require_eq!(is_discriminator_correct, true); + + const AFTER_REALLOC_SIZE: usize = 8 + FundingRecord::INIT_SPACE; + const BEFORE_REALLOC_SIZE: usize = 8 + OldFundingRecord::INIT_SPACE; + + // Size check + if funding_record.data_len() != BEFORE_REALLOC_SIZE { + require_eq!(funding_record.data_len(), AFTER_REALLOC_SIZE); + return Ok(()); + } + + let old_data = + OldFundingRecord::deserialize(&mut &funding_record.try_borrow_data().unwrap()[8..])?; + + let new_data = FundingRecord { + pda_bump: old_data.pda_bump, + funder: old_data.funder, + launch: old_data.launch, + committed_amount: old_data.committed_amount, + is_tokens_claimed: old_data.is_tokens_claimed, + is_usdc_refunded: old_data.is_usdc_refunded, + approved_amount: old_data.approved_amount, + committed_amount_accumulator: 0, + last_accumulator_update: 0, + }; + + funding_record.realloc(AFTER_REALLOC_SIZE, true)?; + + let lamports_needed = Rent::get()?.minimum_balance(AFTER_REALLOC_SIZE); + + if lamports_needed > funding_record.lamports() { + system_program::transfer( + CpiContext::new( + ctx.accounts.system_program.to_account_info(), + system_program::Transfer { + from: ctx.accounts.payer.to_account_info(), + to: funding_record.to_account_info(), + }, + ), + lamports_needed - funding_record.lamports(), + )?; + } + + new_data.serialize(&mut &mut funding_record.try_borrow_mut_data().unwrap()[8..])?; + + Ok(()) + } +} diff --git a/programs/v07_launchpad/src/instructions/resize_launch.rs b/programs/v07_launchpad/src/instructions/resize_launch.rs new file mode 100644 index 000000000..52bfdc59f --- /dev/null +++ b/programs/v07_launchpad/src/instructions/resize_launch.rs @@ -0,0 +1,93 @@ +use anchor_lang::{prelude::*, system_program, Discriminator}; + +use crate::state::{Launch, OldLaunch}; +use crate::ID; + +#[derive(Accounts)] +pub struct ResizeLaunch<'info> { + /// CHECK: we check the discriminator, owner, and size + #[account(mut)] + pub launch: UncheckedAccount<'info>, + #[account(mut)] + pub payer: Signer<'info>, + pub system_program: Program<'info, System>, +} + +impl ResizeLaunch<'_> { + pub fn handle(ctx: Context) -> Result<()> { + let launch = &mut ctx.accounts.launch; + + // Owner check + require_eq!(launch.owner, &ID); + + // Discriminator check + let is_discriminator_correct = + launch.data.try_borrow_mut().unwrap()[..8] == Launch::discriminator(); + require_eq!(is_discriminator_correct, true); + + const AFTER_REALLOC_SIZE: usize = 8 + Launch::INIT_SPACE; + const BEFORE_REALLOC_SIZE: usize = 8 + OldLaunch::INIT_SPACE; + + // Size check + if launch.data_len() != BEFORE_REALLOC_SIZE { + require_eq!(launch.data_len(), AFTER_REALLOC_SIZE); + return Ok(()); + } + + let old_data = OldLaunch::deserialize(&mut &launch.try_borrow_data().unwrap()[8..])?; + + let new_data = Launch { + pda_bump: old_data.pda_bump, + minimum_raise_amount: old_data.minimum_raise_amount, + monthly_spending_limit_amount: old_data.monthly_spending_limit_amount, + monthly_spending_limit_members: old_data.monthly_spending_limit_members, + launch_authority: old_data.launch_authority, + launch_signer: old_data.launch_signer, + launch_signer_pda_bump: old_data.launch_signer_pda_bump, + launch_quote_vault: old_data.launch_quote_vault, + launch_base_vault: old_data.launch_base_vault, + base_mint: old_data.base_mint, + quote_mint: old_data.quote_mint, + unix_timestamp_started: old_data.unix_timestamp_started, + unix_timestamp_closed: old_data.unix_timestamp_closed, + total_committed_amount: old_data.total_committed_amount, + state: old_data.state, + seq_num: old_data.seq_num, + seconds_for_launch: old_data.seconds_for_launch, + dao: old_data.dao, + dao_vault: old_data.dao_vault, + performance_package_grantee: old_data.performance_package_grantee, + performance_package_token_amount: old_data.performance_package_token_amount, + months_until_insiders_can_unlock: old_data.months_until_insiders_can_unlock, + team_address: old_data.team_address, + total_approved_amount: old_data.total_approved_amount, + additional_tokens_amount: old_data.additional_tokens_amount, + additional_tokens_recipient: old_data.additional_tokens_recipient, + additional_tokens_claimed: old_data.additional_tokens_claimed, + unix_timestamp_completed: old_data.unix_timestamp_completed, + is_performance_package_initialized: old_data.is_performance_package_initialized, + accumulator_activation_delay_seconds: 0, + }; + + launch.realloc(AFTER_REALLOC_SIZE, true)?; + + let lamports_needed = Rent::get()?.minimum_balance(AFTER_REALLOC_SIZE); + + if lamports_needed > launch.lamports() { + system_program::transfer( + CpiContext::new( + ctx.accounts.system_program.to_account_info(), + system_program::Transfer { + from: ctx.accounts.payer.to_account_info(), + to: launch.to_account_info(), + }, + ), + lamports_needed - launch.lamports(), + )?; + } + + new_data.serialize(&mut &mut launch.try_borrow_mut_data().unwrap()[8..])?; + + Ok(()) + } +} diff --git a/programs/v07_launchpad/src/lib.rs b/programs/v07_launchpad/src/lib.rs index b7ee84fe4..63134f9d3 100644 --- a/programs/v07_launchpad/src/lib.rs +++ b/programs/v07_launchpad/src/lib.rs @@ -121,4 +121,12 @@ pub mod launchpad_v7 { ) -> Result<()> { InitializePerformancePackage::handle(ctx) } + + pub fn resize_funding_record(ctx: Context) -> Result<()> { + ResizeFundingRecord::handle(ctx) + } + + pub fn resize_launch(ctx: Context) -> Result<()> { + ResizeLaunch::handle(ctx) + } } diff --git a/programs/v07_launchpad/src/state/funding_record.rs b/programs/v07_launchpad/src/state/funding_record.rs index 2c51297c8..9179c9eeb 100644 --- a/programs/v07_launchpad/src/state/funding_record.rs +++ b/programs/v07_launchpad/src/state/funding_record.rs @@ -18,4 +18,28 @@ pub struct FundingRecord { /// The amount of USDC that the launch authority has approved for the funder. /// If zero, the funder has not been approved for any amount. pub approved_amount: u64, + /// Running integral of committed_amount over time (committed_amount * seconds). + pub committed_amount_accumulator: u128, + /// Unix timestamp of the last accumulator update. + pub last_accumulator_update: i64, +} + +#[account] +#[derive(InitSpace)] +pub struct OldFundingRecord { + /// The PDA bump. + pub pda_bump: u8, + /// The funder. + pub funder: Pubkey, + /// The launch. + pub launch: Pubkey, + /// The amount of USDC that has been committed by the funder. + pub committed_amount: u64, + /// Whether the tokens have been claimed. + pub is_tokens_claimed: bool, + /// Whether the USDC has been refunded. + pub is_usdc_refunded: bool, + /// The amount of USDC that the launch authority has approved for the funder. + /// If zero, the funder has not been approved for any amount. + pub approved_amount: u64, } diff --git a/programs/v07_launchpad/src/state/launch.rs b/programs/v07_launchpad/src/state/launch.rs index 9c46624cd..58fba6dcd 100644 --- a/programs/v07_launchpad/src/state/launch.rs +++ b/programs/v07_launchpad/src/state/launch.rs @@ -25,6 +25,74 @@ impl ToString for LaunchState { #[account] #[derive(InitSpace)] pub struct Launch { + /// The PDA bump. + pub pda_bump: u8, + /// The minimum amount of USDC that must be raised, otherwise + /// everyone can get their USDC back. + pub minimum_raise_amount: u64, + /// The monthly spending limit the DAO allocates to the team. Must be + /// less than 1/6th of the minimum raise amount (so 6 months of burn). + pub monthly_spending_limit_amount: u64, + /// The wallets that have access to the monthly spending limit. + #[max_len(10)] + pub monthly_spending_limit_members: Vec, + /// The account that can start the launch. + pub launch_authority: Pubkey, + /// The launch signer address. Needed because Raydium pools need a SOL payer and this PDA can't hold SOL. + pub launch_signer: Pubkey, + /// The PDA bump for the launch signer. + pub launch_signer_pda_bump: u8, + /// The USDC vault that will hold the USDC raised until the launch is over. + pub launch_quote_vault: Pubkey, + /// The token vault, used to send tokens to Raydium. + pub launch_base_vault: Pubkey, + /// The token that will be minted to funders and that will control the DAO. + pub base_mint: Pubkey, + /// The USDC mint. + pub quote_mint: Pubkey, + /// The unix timestamp when the launch was started. + pub unix_timestamp_started: Option, + /// The unix timestamp when the launch stopped taking new contributions. + pub unix_timestamp_closed: Option, + /// The amount of USDC that has been committed by the users. + pub total_committed_amount: u64, + /// The state of the launch. + pub state: LaunchState, + /// The sequence number of this launch. Useful for sorting events. + pub seq_num: u64, + /// The number of seconds that the launch will be live for. + pub seconds_for_launch: u32, + /// The DAO, if the launch is complete. + pub dao: Option, + /// The DAO treasury that USDC / LP is sent to, if the launch is complete. + pub dao_vault: Option, + /// The address that will receive the performance package tokens. + pub performance_package_grantee: Pubkey, + /// The amount of tokens to be granted to the performance package grantee. + pub performance_package_token_amount: u64, + /// The number of months that insiders must wait before unlocking their tokens. + pub months_until_insiders_can_unlock: u8, + /// The initial address used to sponsor team proposals. + pub team_address: Pubkey, + /// The amount of USDC that the launch authority has approved across all funders. + pub total_approved_amount: u64, + /// The amount of additional tokens to be minted on a successful launch. + pub additional_tokens_amount: u64, + /// The token account that will receive the additional tokens. + pub additional_tokens_recipient: Option, + /// Are the additional tokens claimed + pub additional_tokens_claimed: bool, + /// The unix timestamp when the launch was completed. + pub unix_timestamp_completed: Option, + pub is_performance_package_initialized: bool, + /// Number of seconds after launch start before the funding accumulator + /// begins tracking. + pub accumulator_activation_delay_seconds: u32, +} + +#[account] +#[derive(InitSpace)] +pub struct OldLaunch { /// The PDA bump. pub pda_bump: u8, /// The minimum amount of USDC that must be raised, otherwise diff --git a/scripts/v0.7/dumpLaunchesAndFundingRecords.ts b/scripts/v0.7/dumpLaunchesAndFundingRecords.ts new file mode 100644 index 000000000..81db03e50 --- /dev/null +++ b/scripts/v0.7/dumpLaunchesAndFundingRecords.ts @@ -0,0 +1,124 @@ +import { PublicKey } from "@solana/web3.js"; +import * as anchor from "@coral-xyz/anchor"; +import { LaunchpadClient } from "@metadaoproject/futarchy/v0.7"; +import dotenv from "dotenv"; +import * as fs from "fs"; +import * as path from "path"; +import bs58 from "bs58"; + +dotenv.config(); + +const provider = anchor.AnchorProvider.env(); + +function getDiscriminator(accountName: string): Buffer { + return Buffer.from( + anchor.BorshAccountsCoder.accountDiscriminator(accountName), + ); +} + +async function dumpAccount( + publicKey: PublicKey, + outputDir: string, + accountType: string, +) { + const accountInfo = await provider.connection.getAccountInfo(publicKey); + + if (!accountInfo) { + console.error(`Account ${publicKey.toBase58()} not found`); + return; + } + + const accountData = { + pubkey: publicKey.toBase58(), + account: { + lamports: accountInfo.lamports, + data: [accountInfo.data.toString("base64"), "base64"], + owner: accountInfo.owner.toBase58(), + executable: accountInfo.executable, + rentEpoch: "U64_MAX_PLACEHOLDER", + }, + }; + + const filename = path.join(outputDir, `${publicKey.toBase58()}.json`); + fs.writeFileSync( + filename, + JSON.stringify(accountData, null, 2).replace( + '"U64_MAX_PLACEHOLDER"', + "18446744073709551615", + ), + ); + + console.log(`Dumped ${accountType}: ${publicKey.toBase58()}`); +} + +async function main() { + const launchpad = LaunchpadClient.createClient({ provider }); + + const launchesDir = "launches"; + const fundingRecordsDir = "funding_records"; + if (!fs.existsSync(launchesDir)) { + fs.mkdirSync(launchesDir); + } + if (!fs.existsSync(fundingRecordsDir)) { + fs.mkdirSync(fundingRecordsDir); + } + + const launchDiscriminator = getDiscriminator("Launch"); + const fundingRecordDiscriminator = getDiscriminator("FundingRecord"); + console.log( + `Launch discriminator (hex): ${launchDiscriminator.toString("hex")}`, + ); + console.log( + `Launch discriminator (base58): ${bs58.encode(launchDiscriminator)}`, + ); + console.log( + `FundingRecord discriminator (hex): ${fundingRecordDiscriminator.toString("hex")}`, + ); + console.log( + `FundingRecord discriminator (base58): ${bs58.encode(fundingRecordDiscriminator)}`, + ); + console.log(`Program ID: ${launchpad.launchpad.programId.toBase58()}\n`); + + const launchAccounts = await provider.connection.getProgramAccounts( + launchpad.launchpad.programId, + { + filters: [ + { + memcmp: { + offset: 0, + bytes: bs58.encode(launchDiscriminator), + }, + }, + ], + }, + ); + + console.log(`Found ${launchAccounts.length} Launches`); + for (const { pubkey } of launchAccounts) { + await dumpAccount(pubkey, launchesDir, "Launch"); + } + + const fundingRecordAccounts = await provider.connection.getProgramAccounts( + launchpad.launchpad.programId, + { + filters: [ + { + memcmp: { + offset: 0, + bytes: bs58.encode(fundingRecordDiscriminator), + }, + }, + ], + }, + ); + + console.log(`Found ${fundingRecordAccounts.length} FundingRecords`); + for (const { pubkey } of fundingRecordAccounts) { + await dumpAccount(pubkey, fundingRecordsDir, "FundingRecord"); + } +} + +main().catch((error) => { + console.error("Fatal error:", error); + process.exit(1); +}); diff --git a/scripts/v0.7/resizeLaunchesAndFundingRecords.ts b/scripts/v0.7/resizeLaunchesAndFundingRecords.ts new file mode 100644 index 000000000..927926703 --- /dev/null +++ b/scripts/v0.7/resizeLaunchesAndFundingRecords.ts @@ -0,0 +1,206 @@ +import { + ComputeBudgetProgram, + Keypair, + TransactionInstruction, + VersionedTransaction, + TransactionMessage, +} from "@solana/web3.js"; +import * as anchor from "@coral-xyz/anchor"; +import { LaunchpadClient } from "@metadaoproject/futarchy/v0.7"; +import dotenv from "dotenv"; +import bs58 from "bs58"; + +dotenv.config(); + +const provider = anchor.AnchorProvider.env(); +const payer = provider.wallet["payer"]; + +function getDiscriminator(accountName: string): Buffer { + return Buffer.from( + anchor.BorshAccountsCoder.accountDiscriminator(accountName), + ); +} + +async function main() { + const launchpad = LaunchpadClient.createClient({ provider }); + + const launchDiscriminator = getDiscriminator("Launch"); + const fundingRecordDiscriminator = getDiscriminator("FundingRecord"); + + const batchSize = 20; + + console.log( + `Launch discriminator (hex): ${launchDiscriminator.toString("hex")}`, + ); + console.log( + `FundingRecord discriminator (hex): ${fundingRecordDiscriminator.toString("hex")}`, + ); + console.log(`Program ID: ${launchpad.launchpad.programId.toBase58()}\n`); + + // Resize Launches + const launchAccounts = await provider.connection.getProgramAccounts( + launchpad.launchpad.programId, + { + filters: [ + { + memcmp: { + offset: 0, + bytes: bs58.encode(launchDiscriminator), + }, + }, + ], + }, + ); + + console.log(`Found ${launchAccounts.length} Launches`); + for (let i = 0; i < launchAccounts.length; i += batchSize) { + const batch = launchAccounts.slice( + i, + Math.min(i + batchSize, launchAccounts.length), + ); + console.log( + `Processing batch ${i / batchSize + 1} with ${batch.length} Launches`, + ); + + const ixs = await Promise.all( + batch.map(async ({ pubkey }) => { + return await launchpad.launchpad.methods + .resizeLaunch() + .accounts({ + launch: pubkey, + payer: payer.publicKey, + }) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ + units: 9_000, + }), + ComputeBudgetProgram.setComputeUnitPrice({ + microLamports: 1, + }), + ]) + .instruction(); + }), + ); + + await sendAndConfirmTransaction( + ixs, + `Resize Launches batch ${i / batchSize + 1}`, + ); + } + + // Resize FundingRecords + const fundingRecordAccounts = await provider.connection.getProgramAccounts( + launchpad.launchpad.programId, + { + filters: [ + { + memcmp: { + offset: 0, + bytes: bs58.encode(fundingRecordDiscriminator), + }, + }, + ], + }, + ); + + console.log(`Found ${fundingRecordAccounts.length} FundingRecords`); + for (let i = 0; i < fundingRecordAccounts.length; i += batchSize) { + const batch = fundingRecordAccounts.slice( + i, + Math.min(i + batchSize, fundingRecordAccounts.length), + ); + console.log( + `Processing batch ${i / batchSize + 1} with ${batch.length} FundingRecords`, + ); + + const ixs = await Promise.all( + batch.map(async ({ pubkey }) => { + return await launchpad.launchpad.methods + .resizeFundingRecord() + .accounts({ + fundingRecord: pubkey, + payer: payer.publicKey, + }) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ + units: 6_000, + }), + ComputeBudgetProgram.setComputeUnitPrice({ + microLamports: 1, + }), + ]) + .instruction(); + }), + ); + + await sendAndConfirmTransaction( + ixs, + `Resize FundingRecords batch ${i / batchSize + 1}`, + ); + } + + // Verify all accounts load through SDK + console.log( + "Confirming launches and funding records can be loaded through SDK...", + ); + const launches = await launchpad.launchpad.account.launch.all(); + console.log(`Confirmed ${launches.length} Launches`); + const fundingRecords = await launchpad.launchpad.account.fundingRecord.all(); + console.log(`Confirmed ${fundingRecords.length} FundingRecords`); +} + +main().catch((error) => { + console.error("Fatal error:", error); + process.exit(1); +}); + +async function sendAndConfirmTransaction( + ixs: TransactionInstruction[], + label: string, + signers: Keypair[] = [], +) { + const { blockhash } = await provider.connection.getLatestBlockhash(); + + // Simulate without compute budget to get units consumed + const messageV0 = new TransactionMessage({ + instructions: ixs, + payerKey: payer.publicKey, + recentBlockhash: blockhash, + }).compileToV0Message(); + const simulationTx = new VersionedTransaction(messageV0); + simulationTx.sign([payer, ...signers]); + + const simulationResult = + await provider.connection.simulateTransaction(simulationTx); + + const computeBudgetIx = ComputeBudgetProgram.setComputeUnitLimit({ + units: Math.ceil(simulationResult.value.unitsConsumed! * 1.15), + }); + + // Rebuild transaction with compute budget instruction prepended + const finalMessageV0 = new TransactionMessage({ + instructions: [computeBudgetIx, ...ixs], + payerKey: payer.publicKey, + recentBlockhash: blockhash, + }).compileToV0Message(); + const tx = new VersionedTransaction(finalMessageV0); + tx.sign([payer, ...signers]); + + const txHash = await provider.connection.sendRawTransaction(tx.serialize()); + console.log(`${label} transaction sent:`, txHash); + + await provider.connection.confirmTransaction(txHash, "confirmed"); + const txStatus = await provider.connection.getTransaction(txHash, { + maxSupportedTransactionVersion: 0, + commitment: "confirmed", + }); + if (txStatus?.meta?.err) { + throw new Error( + `Transaction failed: ${txHash}\nError: ${JSON.stringify( + txStatus?.meta?.err, + )}\n\n${txStatus?.meta?.logMessages?.join("\n")}`, + ); + } + console.log(`${label} transaction confirmed`); + return txHash; +} diff --git a/sdk/package.json b/sdk/package.json index f0eabbc54..60d42aa6c 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@metadaoproject/futarchy", - "version": "0.7.0-alpha.13", + "version": "0.7.0-alpha.14", "type": "module", "main": "dist/index.js", "module": "dist/index.js", diff --git a/sdk/src/v0.7/LaunchpadClient.ts b/sdk/src/v0.7/LaunchpadClient.ts index c0c6cef6b..57b7cfb32 100644 --- a/sdk/src/v0.7/LaunchpadClient.ts +++ b/sdk/src/v0.7/LaunchpadClient.ts @@ -152,6 +152,7 @@ export class LaunchpadClient { payer = this.provider.publicKey, additionalTokensRecipient, additionalTokensAmount, + accumulatorActivationDelaySeconds = 0, }: { tokenName: string; tokenSymbol: string; @@ -170,6 +171,7 @@ export class LaunchpadClient { payer?: PublicKey; additionalTokensRecipient?: PublicKey; additionalTokensAmount?: BN; + accumulatorActivationDelaySeconds?: number; }) { const [launch] = getLaunchAddr(this.launchpad.programId, baseMint); const [launchSigner] = getLaunchSignerAddr( @@ -203,6 +205,7 @@ export class LaunchpadClient { monthsUntilInsidersCanUnlock, teamAddress, additionalTokensAmount: additionalTokensAmount ?? new BN(0), + accumulatorActivationDelaySeconds, }) .accounts({ launch, diff --git a/sdk/src/v0.7/types/launchpad_v7.ts b/sdk/src/v0.7/types/launchpad_v7.ts index 98cf391a3..41d0415b6 100644 --- a/sdk/src/v0.7/types/launchpad_v7.ts +++ b/sdk/src/v0.7/types/launchpad_v7.ts @@ -781,10 +781,110 @@ export type LaunchpadV7 = { ]; args: []; }, + { + name: "resizeFundingRecord"; + accounts: [ + { + name: "fundingRecord"; + isMut: true; + isSigner: false; + }, + { + name: "payer"; + isMut: true; + isSigner: true; + }, + { + name: "systemProgram"; + isMut: false; + isSigner: false; + }, + ]; + args: []; + }, + { + name: "resizeLaunch"; + accounts: [ + { + name: "launch"; + isMut: true; + isSigner: false; + }, + { + name: "payer"; + isMut: true; + isSigner: true; + }, + { + name: "systemProgram"; + isMut: false; + isSigner: false; + }, + ]; + args: []; + }, ]; accounts: [ { name: "fundingRecord"; + type: { + kind: "struct"; + fields: [ + { + name: "pdaBump"; + docs: ["The PDA bump."]; + type: "u8"; + }, + { + name: "funder"; + docs: ["The funder."]; + type: "publicKey"; + }, + { + name: "launch"; + docs: ["The launch."]; + type: "publicKey"; + }, + { + name: "committedAmount"; + docs: ["The amount of USDC that has been committed by the funder."]; + type: "u64"; + }, + { + name: "isTokensClaimed"; + docs: ["Whether the tokens have been claimed."]; + type: "bool"; + }, + { + name: "isUsdcRefunded"; + docs: ["Whether the USDC has been refunded."]; + type: "bool"; + }, + { + name: "approvedAmount"; + docs: [ + "The amount of USDC that the launch authority has approved for the funder.", + "If zero, the funder has not been approved for any amount.", + ]; + type: "u64"; + }, + { + name: "committedAmountAccumulator"; + docs: [ + "Running integral of committed_amount over time (committed_amount * seconds).", + ]; + type: "u128"; + }, + { + name: "lastAccumulatorUpdate"; + docs: ["Unix timestamp of the last accumulator update."]; + type: "i64"; + }, + ]; + }; + }, + { + name: "oldFundingRecord"; type: { kind: "struct"; fields: [ @@ -1022,6 +1122,214 @@ export type LaunchpadV7 = { option: "i64"; }; }, + { + name: "isPerformancePackageInitialized"; + type: "bool"; + }, + { + name: "accumulatorActivationDelaySeconds"; + docs: [ + "Number of seconds after launch start before the funding accumulator", + "begins tracking.", + ]; + type: "u32"; + }, + ]; + }; + }, + { + name: "oldLaunch"; + type: { + kind: "struct"; + fields: [ + { + name: "pdaBump"; + docs: ["The PDA bump."]; + type: "u8"; + }, + { + name: "minimumRaiseAmount"; + docs: [ + "The minimum amount of USDC that must be raised, otherwise", + "everyone can get their USDC back.", + ]; + type: "u64"; + }, + { + name: "monthlySpendingLimitAmount"; + docs: [ + "The monthly spending limit the DAO allocates to the team. Must be", + "less than 1/6th of the minimum raise amount (so 6 months of burn).", + ]; + type: "u64"; + }, + { + name: "monthlySpendingLimitMembers"; + docs: [ + "The wallets that have access to the monthly spending limit.", + ]; + type: { + vec: "publicKey"; + }; + }, + { + name: "launchAuthority"; + docs: ["The account that can start the launch."]; + type: "publicKey"; + }, + { + name: "launchSigner"; + docs: [ + "The launch signer address. Needed because Raydium pools need a SOL payer and this PDA can't hold SOL.", + ]; + type: "publicKey"; + }, + { + name: "launchSignerPdaBump"; + docs: ["The PDA bump for the launch signer."]; + type: "u8"; + }, + { + name: "launchQuoteVault"; + docs: [ + "The USDC vault that will hold the USDC raised until the launch is over.", + ]; + type: "publicKey"; + }, + { + name: "launchBaseVault"; + docs: ["The token vault, used to send tokens to Raydium."]; + type: "publicKey"; + }, + { + name: "baseMint"; + docs: [ + "The token that will be minted to funders and that will control the DAO.", + ]; + type: "publicKey"; + }, + { + name: "quoteMint"; + docs: ["The USDC mint."]; + type: "publicKey"; + }, + { + name: "unixTimestampStarted"; + docs: ["The unix timestamp when the launch was started."]; + type: { + option: "i64"; + }; + }, + { + name: "unixTimestampClosed"; + docs: [ + "The unix timestamp when the launch stopped taking new contributions.", + ]; + type: { + option: "i64"; + }; + }, + { + name: "totalCommittedAmount"; + docs: ["The amount of USDC that has been committed by the users."]; + type: "u64"; + }, + { + name: "state"; + docs: ["The state of the launch."]; + type: { + defined: "LaunchState"; + }; + }, + { + name: "seqNum"; + docs: [ + "The sequence number of this launch. Useful for sorting events.", + ]; + type: "u64"; + }, + { + name: "secondsForLaunch"; + docs: ["The number of seconds that the launch will be live for."]; + type: "u32"; + }, + { + name: "dao"; + docs: ["The DAO, if the launch is complete."]; + type: { + option: "publicKey"; + }; + }, + { + name: "daoVault"; + docs: [ + "The DAO treasury that USDC / LP is sent to, if the launch is complete.", + ]; + type: { + option: "publicKey"; + }; + }, + { + name: "performancePackageGrantee"; + docs: [ + "The address that will receive the performance package tokens.", + ]; + type: "publicKey"; + }, + { + name: "performancePackageTokenAmount"; + docs: [ + "The amount of tokens to be granted to the performance package grantee.", + ]; + type: "u64"; + }, + { + name: "monthsUntilInsidersCanUnlock"; + docs: [ + "The number of months that insiders must wait before unlocking their tokens.", + ]; + type: "u8"; + }, + { + name: "teamAddress"; + docs: ["The initial address used to sponsor team proposals."]; + type: "publicKey"; + }, + { + name: "totalApprovedAmount"; + docs: [ + "The amount of USDC that the launch authority has approved across all funders.", + ]; + type: "u64"; + }, + { + name: "additionalTokensAmount"; + docs: [ + "The amount of additional tokens to be minted on a successful launch.", + ]; + type: "u64"; + }, + { + name: "additionalTokensRecipient"; + docs: [ + "The token account that will receive the additional tokens.", + ]; + type: { + option: "publicKey"; + }; + }, + { + name: "additionalTokensClaimed"; + docs: ["Are the additional tokens claimed"]; + type: "bool"; + }, + { + name: "unixTimestampCompleted"; + docs: ["The unix timestamp when the launch was completed."]; + type: { + option: "i64"; + }; + }, { name: "isPerformancePackageInitialized"; docs: ["Whether the performance package has been initialized."]; @@ -1107,6 +1415,10 @@ export type LaunchpadV7 = { name: "additionalTokensAmount"; type: "u64"; }, + { + name: "accumulatorActivationDelaySeconds"; + type: "u32"; + }, ]; }; }, @@ -1239,6 +1551,11 @@ export type LaunchpadV7 = { }; index: false; }, + { + name: "accumulatorActivationDelaySeconds"; + type: "u32"; + index: false; + }, ]; }, { @@ -1308,6 +1625,11 @@ export type LaunchpadV7 = { type: "u64"; index: false; }, + { + name: "committedAmountAccumulator"; + type: "u128"; + index: false; + }, ]; }, { @@ -1696,6 +2018,11 @@ export type LaunchpadV7 = { name: "InvalidDao"; msg: "Invalid DAO"; }, + { + code: 6030; + name: "InvalidAccumulatorActivationDelaySeconds"; + msg: "Accumulator activation delay must be less than the launch duration"; + }, ]; }; @@ -2482,10 +2809,110 @@ export const IDL: LaunchpadV7 = { ], args: [], }, - ], - accounts: [ { - name: "fundingRecord", + name: "resizeFundingRecord", + accounts: [ + { + name: "fundingRecord", + isMut: true, + isSigner: false, + }, + { + name: "payer", + isMut: true, + isSigner: true, + }, + { + name: "systemProgram", + isMut: false, + isSigner: false, + }, + ], + args: [], + }, + { + name: "resizeLaunch", + accounts: [ + { + name: "launch", + isMut: true, + isSigner: false, + }, + { + name: "payer", + isMut: true, + isSigner: true, + }, + { + name: "systemProgram", + isMut: false, + isSigner: false, + }, + ], + args: [], + }, + ], + accounts: [ + { + name: "fundingRecord", + type: { + kind: "struct", + fields: [ + { + name: "pdaBump", + docs: ["The PDA bump."], + type: "u8", + }, + { + name: "funder", + docs: ["The funder."], + type: "publicKey", + }, + { + name: "launch", + docs: ["The launch."], + type: "publicKey", + }, + { + name: "committedAmount", + docs: ["The amount of USDC that has been committed by the funder."], + type: "u64", + }, + { + name: "isTokensClaimed", + docs: ["Whether the tokens have been claimed."], + type: "bool", + }, + { + name: "isUsdcRefunded", + docs: ["Whether the USDC has been refunded."], + type: "bool", + }, + { + name: "approvedAmount", + docs: [ + "The amount of USDC that the launch authority has approved for the funder.", + "If zero, the funder has not been approved for any amount.", + ], + type: "u64", + }, + { + name: "committedAmountAccumulator", + docs: [ + "Running integral of committed_amount over time (committed_amount * seconds).", + ], + type: "u128", + }, + { + name: "lastAccumulatorUpdate", + docs: ["Unix timestamp of the last accumulator update."], + type: "i64", + }, + ], + }, + }, + { + name: "oldFundingRecord", type: { kind: "struct", fields: [ @@ -2723,6 +3150,214 @@ export const IDL: LaunchpadV7 = { option: "i64", }, }, + { + name: "isPerformancePackageInitialized", + type: "bool", + }, + { + name: "accumulatorActivationDelaySeconds", + docs: [ + "Number of seconds after launch start before the funding accumulator", + "begins tracking.", + ], + type: "u32", + }, + ], + }, + }, + { + name: "oldLaunch", + type: { + kind: "struct", + fields: [ + { + name: "pdaBump", + docs: ["The PDA bump."], + type: "u8", + }, + { + name: "minimumRaiseAmount", + docs: [ + "The minimum amount of USDC that must be raised, otherwise", + "everyone can get their USDC back.", + ], + type: "u64", + }, + { + name: "monthlySpendingLimitAmount", + docs: [ + "The monthly spending limit the DAO allocates to the team. Must be", + "less than 1/6th of the minimum raise amount (so 6 months of burn).", + ], + type: "u64", + }, + { + name: "monthlySpendingLimitMembers", + docs: [ + "The wallets that have access to the monthly spending limit.", + ], + type: { + vec: "publicKey", + }, + }, + { + name: "launchAuthority", + docs: ["The account that can start the launch."], + type: "publicKey", + }, + { + name: "launchSigner", + docs: [ + "The launch signer address. Needed because Raydium pools need a SOL payer and this PDA can't hold SOL.", + ], + type: "publicKey", + }, + { + name: "launchSignerPdaBump", + docs: ["The PDA bump for the launch signer."], + type: "u8", + }, + { + name: "launchQuoteVault", + docs: [ + "The USDC vault that will hold the USDC raised until the launch is over.", + ], + type: "publicKey", + }, + { + name: "launchBaseVault", + docs: ["The token vault, used to send tokens to Raydium."], + type: "publicKey", + }, + { + name: "baseMint", + docs: [ + "The token that will be minted to funders and that will control the DAO.", + ], + type: "publicKey", + }, + { + name: "quoteMint", + docs: ["The USDC mint."], + type: "publicKey", + }, + { + name: "unixTimestampStarted", + docs: ["The unix timestamp when the launch was started."], + type: { + option: "i64", + }, + }, + { + name: "unixTimestampClosed", + docs: [ + "The unix timestamp when the launch stopped taking new contributions.", + ], + type: { + option: "i64", + }, + }, + { + name: "totalCommittedAmount", + docs: ["The amount of USDC that has been committed by the users."], + type: "u64", + }, + { + name: "state", + docs: ["The state of the launch."], + type: { + defined: "LaunchState", + }, + }, + { + name: "seqNum", + docs: [ + "The sequence number of this launch. Useful for sorting events.", + ], + type: "u64", + }, + { + name: "secondsForLaunch", + docs: ["The number of seconds that the launch will be live for."], + type: "u32", + }, + { + name: "dao", + docs: ["The DAO, if the launch is complete."], + type: { + option: "publicKey", + }, + }, + { + name: "daoVault", + docs: [ + "The DAO treasury that USDC / LP is sent to, if the launch is complete.", + ], + type: { + option: "publicKey", + }, + }, + { + name: "performancePackageGrantee", + docs: [ + "The address that will receive the performance package tokens.", + ], + type: "publicKey", + }, + { + name: "performancePackageTokenAmount", + docs: [ + "The amount of tokens to be granted to the performance package grantee.", + ], + type: "u64", + }, + { + name: "monthsUntilInsidersCanUnlock", + docs: [ + "The number of months that insiders must wait before unlocking their tokens.", + ], + type: "u8", + }, + { + name: "teamAddress", + docs: ["The initial address used to sponsor team proposals."], + type: "publicKey", + }, + { + name: "totalApprovedAmount", + docs: [ + "The amount of USDC that the launch authority has approved across all funders.", + ], + type: "u64", + }, + { + name: "additionalTokensAmount", + docs: [ + "The amount of additional tokens to be minted on a successful launch.", + ], + type: "u64", + }, + { + name: "additionalTokensRecipient", + docs: [ + "The token account that will receive the additional tokens.", + ], + type: { + option: "publicKey", + }, + }, + { + name: "additionalTokensClaimed", + docs: ["Are the additional tokens claimed"], + type: "bool", + }, + { + name: "unixTimestampCompleted", + docs: ["The unix timestamp when the launch was completed."], + type: { + option: "i64", + }, + }, { name: "isPerformancePackageInitialized", docs: ["Whether the performance package has been initialized."], @@ -2808,6 +3443,10 @@ export const IDL: LaunchpadV7 = { name: "additionalTokensAmount", type: "u64", }, + { + name: "accumulatorActivationDelaySeconds", + type: "u32", + }, ], }, }, @@ -2940,6 +3579,11 @@ export const IDL: LaunchpadV7 = { }, index: false, }, + { + name: "accumulatorActivationDelaySeconds", + type: "u32", + index: false, + }, ], }, { @@ -3009,6 +3653,11 @@ export const IDL: LaunchpadV7 = { type: "u64", index: false, }, + { + name: "committedAmountAccumulator", + type: "u128", + index: false, + }, ], }, { @@ -3397,5 +4046,10 @@ export const IDL: LaunchpadV7 = { name: "InvalidDao", msg: "Invalid DAO", }, + { + code: 6030, + name: "InvalidAccumulatorActivationDelaySeconds", + msg: "Accumulator activation delay must be less than the launch duration", + }, ], }; diff --git a/tests/launchpad_v7/unit/fund.test.ts b/tests/launchpad_v7/unit/fund.test.ts index 2d1e8c7c1..fc3fcc65c 100644 --- a/tests/launchpad_v7/unit/fund.test.ts +++ b/tests/launchpad_v7/unit/fund.test.ts @@ -1,4 +1,9 @@ -import { Keypair, PublicKey, Signer } from "@solana/web3.js"; +import { + Keypair, + PublicKey, + Signer, + ComputeBudgetProgram, +} from "@solana/web3.js"; import { assert } from "chai"; import { FutarchyClient, @@ -211,4 +216,200 @@ export default function suite() { assert.include(e.message, "LaunchExpired"); } }); + + it("accumulator starts at 0 and last_accumulator_update is set on first fund", async function () { + await launchpadClient + .startLaunchIx({ launch, launchAuthority: launchAuthority.publicKey }) + .signers([launchAuthority]) + .rpc(); + await this.createTokenAccount(META, this.payer.publicKey); + + const fundAmount = new BN(100_000_000); // 100 USDC + await launchpadClient.fundIx({ launch, amount: fundAmount }).rpc(); + + const [fundingRecord] = getFundingRecordAddr( + launchpadClient.getProgramId(), + launch, + this.payer.publicKey, + ); + + const record = await launchpadClient.fetchFundingRecord(fundingRecord); + assert.equal(record.committedAmountAccumulator.toString(), "0"); + assert.isAbove(record.lastAccumulatorUpdate.toNumber(), 0); + }); + + it("accumulator correctly sums across multiple time intervals", async function () { + await launchpadClient + .startLaunchIx({ launch, launchAuthority: launchAuthority.publicKey }) + .signers([launchAuthority]) + .rpc(); + await this.createTokenAccount(META, this.payer.publicKey); + + const [fundingRecord] = getFundingRecordAddr( + launchpadClient.getProgramId(), + launch, + this.payer.publicKey, + ); + + // Fund 100 USDC at t=0 + await launchpadClient.fundIx({ launch, amount: new BN(100_000_000) }).rpc(); + + // Advance 30s, fund 200 USDC at t=30 + // Flush: 100_000_000 * 30 = 3_000_000_000 + await this.advanceBySeconds(30); + await launchpadClient + .fundIx({ launch, amount: new BN(200_000_000) }) + .postInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ units: 200_000 }), + ]) + .rpc(); + + let record = await launchpadClient.fetchFundingRecord(fundingRecord); + assert.equal(record.committedAmountAccumulator.toString(), "3000000000"); + + // Advance 60s, fund 50 USDC at t=90 + // Flush: 300_000_000 * 60 = 18_000_000_000 + await this.advanceBySeconds(60); + await launchpadClient + .fundIx({ launch, amount: new BN(50_000_000) }) + .postInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ units: 200_001 }), + ]) + .rpc(); + + record = await launchpadClient.fetchFundingRecord(fundingRecord); + // 3_000_000_000 + 18_000_000_000 = 21_000_000_000 + assert.equal(record.committedAmountAccumulator.toString(), "21000000000"); + }); + + it("accumulator stays 0 during activation delay period", async function () { + // Need a fresh launch with custom accumulatorActivationDelaySeconds + const result = await initializeMintWithSeeds( + this.banksClient, + launchpadClient, + this.payer, + ); + + const delayLaunch = result.launch; + + await launchpadClient + .initializeLaunchIx({ + tokenName: "META", + tokenSymbol: "META", + tokenUri: "https://example.com", + minimumRaiseAmount: minRaise, + secondsForLaunch: secondsForLaunch, + baseMint: result.tokenMint, + quoteMint: MAINNET_USDC, + monthlySpendingLimitAmount: monthlySpend, + monthlySpendingLimitMembers: [this.payer.publicKey], + performancePackageGrantee: recipientAddress, + performancePackageTokenAmount: premineAmount, + monthsUntilInsidersCanUnlock: 18, + teamAddress: PublicKey.default, + launchAuthority: launchAuthority.publicKey, + accumulatorActivationDelaySeconds: 120, // 2 minutes + }) + .rpc(); + + await launchpadClient + .startLaunchIx({ + launch: delayLaunch, + launchAuthority: launchAuthority.publicKey, + }) + .signers([launchAuthority]) + .rpc(); + await this.createTokenAccount(result.tokenMint, this.payer.publicKey); + + // Fund 100 USDC + await launchpadClient + .fundIx({ launch: delayLaunch, amount: new BN(100_000_000) }) + .rpc(); + + // Advance 60s (still within 120s delay) + await this.advanceBySeconds(60); + + // Second fund to trigger flush attempt + await launchpadClient + .fundIx({ launch: delayLaunch, amount: new BN(1_000_000) }) + .postInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ units: 200_000 }), + ]) + .rpc(); + + const [fundingRecord] = getFundingRecordAddr( + launchpadClient.getProgramId(), + delayLaunch, + this.payer.publicKey, + ); + + const record = await launchpadClient.fetchFundingRecord(fundingRecord); + assert.equal(record.committedAmountAccumulator.toString(), "0"); + }); + + it("accumulator only counts time after activation delay", async function () { + // Need a fresh launch with custom accumulatorActivationDelaySeconds + const result = await initializeMintWithSeeds( + this.banksClient, + launchpadClient, + this.payer, + ); + + const delayLaunch = result.launch; + + await launchpadClient + .initializeLaunchIx({ + tokenName: "META", + tokenSymbol: "META", + tokenUri: "https://example.com", + minimumRaiseAmount: minRaise, + secondsForLaunch: secondsForLaunch, + baseMint: result.tokenMint, + quoteMint: MAINNET_USDC, + monthlySpendingLimitAmount: monthlySpend, + monthlySpendingLimitMembers: [this.payer.publicKey], + performancePackageGrantee: recipientAddress, + performancePackageTokenAmount: premineAmount, + monthsUntilInsidersCanUnlock: 18, + teamAddress: PublicKey.default, + launchAuthority: launchAuthority.publicKey, + accumulatorActivationDelaySeconds: 60, // 1 minute + }) + .rpc(); + + await launchpadClient + .startLaunchIx({ + launch: delayLaunch, + launchAuthority: launchAuthority.publicKey, + }) + .signers([launchAuthority]) + .rpc(); + await this.createTokenAccount(result.tokenMint, this.payer.publicKey); + + // Fund 100 USDC at t=0 + await launchpadClient + .fundIx({ launch: delayLaunch, amount: new BN(100_000_000) }) + .rpc(); + + // Advance 90s (activation at t=60, so 30s of active time) + await this.advanceBySeconds(90); + + // Second fund to trigger flush + await launchpadClient + .fundIx({ launch: delayLaunch, amount: new BN(1_000_000) }) + .postInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ units: 200_000 }), + ]) + .rpc(); + + const [fundingRecord] = getFundingRecordAddr( + launchpadClient.getProgramId(), + delayLaunch, + this.payer.publicKey, + ); + + const record = await launchpadClient.fetchFundingRecord(fundingRecord); + // 100_000_000 * 30 = 3_000_000_000 + assert.equal(record.committedAmountAccumulator.toString(), "3000000000"); + }); } diff --git a/tests/launchpad_v7/unit/initializeLaunch.test.ts b/tests/launchpad_v7/unit/initializeLaunch.test.ts index 6a4615fa2..60351ebd6 100644 --- a/tests/launchpad_v7/unit/initializeLaunch.test.ts +++ b/tests/launchpad_v7/unit/initializeLaunch.test.ts @@ -106,6 +106,7 @@ export default function suite() { assert.exists(storedLaunch.state.initialized); assert.isNull(storedLaunch.unixTimestampStarted); assert.isNull(storedLaunch.dao); + assert.equal(storedLaunch.accumulatorActivationDelaySeconds, 0); }); it("fails when monthly spending limit members contains duplicates", async function () { @@ -175,6 +176,39 @@ export default function suite() { } }); + it("rejects accumulator activation delay >= seconds_for_launch", async function () { + const minRaise = new BN(1000_000000); + const secondsForLaunch = 60 * 60 * 24 * 7; + const monthlySpend = new BN(100_000000); + const recipientAddress = Keypair.generate().publicKey; + const premineAmount = new BN(500_000_000); + + try { + await launchpadClient + .initializeLaunchIx({ + tokenName: "META", + tokenSymbol: "META", + tokenUri: "https://example.com", + minimumRaiseAmount: minRaise, + secondsForLaunch: secondsForLaunch, + baseMint: META, + quoteMint: MAINNET_USDC, + monthlySpendingLimitAmount: monthlySpend, + monthlySpendingLimitMembers: [this.payer.publicKey], + performancePackageGrantee: recipientAddress, + performancePackageTokenAmount: premineAmount, + monthsUntilInsidersCanUnlock: 18, + teamAddress: PublicKey.default, + launchAuthority: launchAuthority.publicKey, + accumulatorActivationDelaySeconds: secondsForLaunch, + }) + .rpc(); + assert.fail("Expected initialize_launch to fail"); + } catch (e) { + assert.include(e.message, "InvalidAccumulatorActivationDelaySeconds"); + } + }); + it("fails when launch signer is faked", async function () { const minRaise = new BN(1000_000000); // 1000 USDC const secondsForLaunch = 60 * 60 * 24 * 7; // 1 week diff --git a/yarn.lock b/yarn.lock index c2b9c7269..a06b93872 100644 --- a/yarn.lock +++ b/yarn.lock @@ -975,7 +975,7 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@metadaoproject/futarchy@./sdk": - version "0.7.0-alpha.12" + version "0.7.0-alpha.14" dependencies: "@coral-xyz/anchor" "^0.29.0" "@metaplex-foundation/umi" "^0.9.2"