diff --git a/.cursor/rules/futarchy-project-overview.mdc b/.cursor/rules/futarchy-project-overview.mdc new file mode 100644 index 000000000..405a367a8 --- /dev/null +++ b/.cursor/rules/futarchy-project-overview.mdc @@ -0,0 +1,96 @@ +--- +description: +globs: +alwaysApply: true +--- +# Futarchy Project Overview + +## Project Structure + +This is a Solana-based futarchy platform that implements decision markets and DAO governance. The project consists of multiple Solana programs and a TypeScript SDK. + +### Core Programs +- **Autocrat**: DAO governance and proposal management ([programs/autocrat/](mdc:programs/autocrat)) +- **AMM**: Automated market maker for trading conditional tokens ([programs/amm/](mdc:programs/amm)) +- **Conditional Vault**: Manages conditional tokens for prediction markets ([programs/conditional_vault/](mdc:programs/conditional_vault)) +- **Shared Liquidity Manager**: Manages shared liquidity pools for proposals ([programs/shared_liquidity_manager/](mdc:programs/shared_liquidity_manager)) +- **Launchpad**: Token launch functionality ([programs/launchpad/](mdc:programs/launchpad)) + +### SDK Structure +- **v0.3**: Legacy SDK version ([sdk/src/v0.3/](mdc:sdk/src/v0.3)) +- **v0.4**: Current SDK version with all latest features ([sdk/src/v0.4/](mdc:sdk/src/v0.4)) + +### Testing +- **Unit Tests**: Individual instruction tests ([tests/](mdc:tests)) +- **Integration Tests**: End-to-end workflow tests ([tests/integration/](mdc:tests/integration)) +- **Test Utils**: Common testing utilities ([tests/utils.ts](mdc:tests/utils.ts)) + +## Development Patterns + +### Solana Program Development +- Programs are written in Rust using Anchor framework +- Each program has its own `Cargo.toml` and `src/` directory +- Programs follow Anchor's instruction-based architecture + +### Testing Patterns +- Tests use `bankrun` for fast, deterministic testing +- Complex transactions use lookup tables and `TransactionMessage` for large account lists +- Tests often require full proposal lifecycle setup (draft → stake → initialize → crank → finalize). + We generally do that setup in `before` and `beforeEach` blocks + +### SDK Development +- TypeScript SDK provides client interfaces for all programs +- SDK handles PDA derivation, instruction building, and transaction management +- Versioned SDKs maintain backward compatibility + +## Key Concepts + +### Futarchy +- Governance through markets +- Proposals create conditional tokens (PASS/FAIL) +- Market prices determine proposal outcomes + +### Conditional Tokens +- Represent outcomes of events (e.g., "Will proposal X pass?") +- Can be split, merged, and redeemed based on outcomes +- Used for prediction market trading + +### Shared Liquidity +- Pools that provide liquidity on both spot markets and decision markets +- Managed through the Shared Liquidity Manager program + +## Common Development Tasks + +### Adding New Instructions +1. Add instruction to Rust program in `programs/[program]/src/instructions/` +2. Update client methods in SDK +3. Add unit tests in `tests/[program]/unit/` +4. Add integration tests if needed + +### Running Tests +```bash +# Run all tests +anchor test + +# Run specific test file +anchor test tests/sharedLiquidityManager/unit/removeProposalLiquidity.test.ts + +# Skip build for faster iteration, you should run this if you haven't changed a rust file +anchor test --skip-build +``` + +### Building Programs +```bash +# Build all programs +anchor build + +# Build specific program +anchor build --program-name [program_name] +``` + +## File References +- Main configuration: [Anchor.toml](mdc:Anchor.toml) +- SDK entry point: [sdk/src/index.ts](mdc:sdk/src/index.ts) +- Test utilities: [tests/utils.ts](mdc:tests/utils.ts) +- Example test: @tests/launchpad/unit/refund.test.ts + diff --git a/Anchor.toml b/Anchor.toml index d98f3fa4b..35191d023 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -9,6 +9,7 @@ amm = "AMMyu265tkBpRW21iGQxKGLaves3gKm2JcMUqfXNSpqD" autocrat = "autowMzCbM29YXMgVG3T62Hkgo7RcyrvgQQkd54fDQL" autocrat_migrator = "MigRDW6uxyNMDBD8fX2njCRyJC4YZk2Rx9pDUZiAESt" conditional_vault = "VLTX1ishMBbcX3rdBWGssxawAo1Q2X2qxYFYqiGodVg" +shared_liquidity_manager = "EoJc1PYxZbnCjszampLcwJGYcB5Md47jM4oSQacRtD4d" launchpad = "AfJJJ5UqxhBKoE3grkKAZZsoXDE9kncbMKvqSHGsCNrE" optimistic_timelock = "tiME1hz9F5C5ZecbvE5z6Msjy8PKfTqo1UuRYXfndKF" @@ -20,13 +21,13 @@ cluster = "Localnet" wallet = "~/.config/solana/id.json" [scripts] -test = "npx mocha --import=tsx tests/main.test.ts" add-v03-metadata = "yarn run tsx scripts/addV03Metadata.ts" -initialize-launch = "yarn run tsx scripts/initializeLaunch.ts" +add-v04-metadata = "yarn run tsx scripts/addV04Metadata.ts" create-proposal = "yarn run tsx scripts/createProposal.ts" create-v04-dao = "yarn run tsx scripts/createV04DAO.ts" create-v04-proposal = "yarn run tsx scripts/createV04Proposal.ts" -add-v04-metadata = "yarn run tsx scripts/addV04Metadata.ts" +initialize-launch = "yarn run tsx scripts/initializeLaunch.ts" +test = "npx mocha --import=tsx --bail tests/main.test.ts" [test] startup_wait = 5000 diff --git a/Cargo.lock b/Cargo.lock index f09cee4ac..1679c4dbb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1819,6 +1819,23 @@ dependencies = [ "keccak", ] +[[package]] +name = "shared_liquidity_manager" +version = "0.1.0" +dependencies = [ + "ahash 0.8.6", + "amm", + "anchor-lang", + "anchor-spl", + "autocrat", + "conditional_vault", + "raydium-cpmm-cpi", + "solana-program", + "solana-security-txt", + "spl-memo", + "spl-token", +] + [[package]] name = "signature" version = "1.6.4" diff --git a/package.json b/package.json index c2ec352d9..9e9f079e2 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "dependencies": { "@coral-xyz/anchor": "0.29.0", "@inquirer/prompts": "^7.3.3", - "@metadaoproject/futarchy": "0.4.0-alpha.73", + "@metadaoproject/futarchy": "0.4.0-alpha.74", "@metaplex-foundation/mpl-token-metadata": "^3.2.0", "@metaplex-foundation/umi": "^0.9.1", "@metaplex-foundation/umi-bundle-defaults": "^0.9.1", diff --git a/programs/amm/src/instructions/common.rs b/programs/amm/src/instructions/common.rs index ae5e87011..4bf96c48c 100644 --- a/programs/amm/src/instructions/common.rs +++ b/programs/amm/src/instructions/common.rs @@ -23,13 +23,13 @@ pub struct AddOrRemoveLiquidity<'info> { pub user_lp_account: Box>, #[account( mut, - token::mint = amm.base_mint, + // token::mint = amm.base_mint, token::authority = user, )] pub user_base_account: Box>, #[account( mut, - token::mint = amm.quote_mint, + // token::mint = amm.quote_mint, token::authority = user, )] pub user_quote_account: Box>, diff --git a/programs/amm/src/instructions/create_amm.rs b/programs/amm/src/instructions/create_amm.rs index ca99c8319..463e581e8 100644 --- a/programs/amm/src/instructions/create_amm.rs +++ b/programs/amm/src/instructions/create_amm.rs @@ -121,6 +121,9 @@ impl CreateAmm<'_> { ), seq_num: 0, + + vault_ata_base: vault_ata_base.key(), + vault_ata_quote: vault_ata_quote.key(), }); let clock = Clock::get()?; diff --git a/programs/amm/src/state/amm.rs b/programs/amm/src/state/amm.rs index 2955db869..af67a97c5 100644 --- a/programs/amm/src/state/amm.rs +++ b/programs/amm/src/state/amm.rs @@ -88,6 +88,9 @@ pub struct Amm { pub oracle: TwapOracle, pub seq_num: u64, + + pub vault_ata_base: Pubkey, + pub vault_ata_quote: Pubkey, } impl Amm { @@ -171,7 +174,11 @@ impl Amm { pub fn get_twap(&self) -> Result { let start_slot = self.created_at_slot + self.oracle.start_delay_slots; - require_gt!(self.oracle.last_updated_slot, start_slot, AmmError::NoSlotsPassed); + require_gt!( + self.oracle.last_updated_slot, + start_slot, + AmmError::NoSlotsPassed + ); let slots_passed = (self.oracle.last_updated_slot - start_slot) as u128; require_neq!(slots_passed, 0, AmmError::NoSlotsPassed); diff --git a/programs/autocrat/src/instructions/initialize_proposal.rs b/programs/autocrat/src/instructions/initialize_proposal.rs index ac3199e80..79c5bb7b2 100644 --- a/programs/autocrat/src/instructions/initialize_proposal.rs +++ b/programs/autocrat/src/instructions/initialize_proposal.rs @@ -18,7 +18,7 @@ pub struct InitializeProposalParams { pub struct InitializeProposal<'info> { #[account( init, - payer = proposer, + payer = payer, space = 2000, seeds = [b"proposal", proposer.key().as_ref(), &args.nonce.to_le_bytes()], bump @@ -78,8 +78,9 @@ pub struct InitializeProposal<'info> { associated_token::authority = dao.treasury, )] pub fail_lp_vault_account: Account<'info, TokenAccount>, - #[account(mut)] pub proposer: Signer<'info>, + #[account(mut)] + pub payer: Signer<'info>, pub token_program: Program<'info, Token>, pub system_program: Program<'info, System>, } @@ -136,6 +137,7 @@ impl InitializeProposal<'_> { pass_lp_vault_account, fail_lp_vault_account, proposer, + payer: _, token_program, system_program: _, event_authority: _, diff --git a/programs/autocrat/src/state/proposal.rs b/programs/autocrat/src/state/proposal.rs index 475c24093..a573b4b76 100644 --- a/programs/autocrat/src/state/proposal.rs +++ b/programs/autocrat/src/state/proposal.rs @@ -1,6 +1,6 @@ use super::*; -#[derive(Clone, Copy, AnchorSerialize, AnchorDeserialize, PartialEq, Eq)] +#[derive(Clone, Copy, AnchorSerialize, AnchorDeserialize, PartialEq, Eq, Debug)] pub enum ProposalState { Pending, Passed, @@ -8,6 +8,12 @@ pub enum ProposalState { Executed, } +impl std::fmt::Display for ProposalState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + #[derive(Clone, AnchorSerialize, AnchorDeserialize, Debug, PartialEq, Eq)] pub struct ProposalAccount { pub pubkey: Pubkey, diff --git a/programs/launchpad/src/instructions/complete_launch.rs b/programs/launchpad/src/instructions/complete_launch.rs index ceec713b2..772c8601d 100644 --- a/programs/launchpad/src/instructions/complete_launch.rs +++ b/programs/launchpad/src/instructions/complete_launch.rs @@ -22,7 +22,7 @@ use raydium_cpmm_cpi::{ use autocrat::program::Autocrat; use autocrat::InitializeDaoParams; -use autocrat::{DAY_IN_SLOTS}; +use autocrat::DAY_IN_SLOTS; pub const PRICE_SCALE: u128 = 1_000_000_000_000; diff --git a/programs/launchpad/src/instructions/initialize_launch.rs b/programs/launchpad/src/instructions/initialize_launch.rs index cbdc3f523..404fce45d 100644 --- a/programs/launchpad/src/instructions/initialize_launch.rs +++ b/programs/launchpad/src/instructions/initialize_launch.rs @@ -90,7 +90,6 @@ pub struct InitializeLaunch<'info> { impl InitializeLaunch<'_> { pub fn validate(&self, args: &InitializeLaunchArgs) -> Result<()> { - #[cfg(not(feature = "devnet"))] require_gte!( args.seconds_for_launch, @@ -109,10 +108,7 @@ impl InitializeLaunch<'_> { LaunchpadError::FreezeAuthoritySet ); - require!( - self.token_mint.supply == 0, - LaunchpadError::SupplyNonZero - ); + require!(self.token_mint.supply == 0, LaunchpadError::SupplyNonZero); #[cfg(feature = "production")] { diff --git a/programs/shared_liquidity_manager/Cargo.toml b/programs/shared_liquidity_manager/Cargo.toml new file mode 100644 index 000000000..51cabea4f --- /dev/null +++ b/programs/shared_liquidity_manager/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "shared_liquidity_manager" +version = "0.1.0" +description = "Created with Anchor" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "shared_liquidity_manager" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = ["custom-heap"] +custom-heap = [] + +[dependencies] +anchor-lang = "0.29.0" +autocrat = { path = "../autocrat", features = ["cpi"] } +amm = { path = "../amm", features = ["cpi"] } +conditional_vault = { path = "../conditional_vault", features = ["cpi"] } +raydium-cpmm-cpi = { git = "https://github.com/raydium-io/raydium-cpi", package = "raydium-cpmm-cpi", branch = "anchor-0.29.0" } +spl-memo = "=4.0.0" +solana-program = "=1.17.14" +spl-token = "=4.0.0" +ahash = "=0.8.6" +solana-security-txt = "1.1.1" +anchor-spl = "^0.29.0" + + diff --git a/programs/shared_liquidity_manager/Xargo.toml b/programs/shared_liquidity_manager/Xargo.toml new file mode 100644 index 000000000..475fb71ed --- /dev/null +++ b/programs/shared_liquidity_manager/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/programs/shared_liquidity_manager/src/error.rs b/programs/shared_liquidity_manager/src/error.rs new file mode 100644 index 000000000..111305b7c --- /dev/null +++ b/programs/shared_liquidity_manager/src/error.rs @@ -0,0 +1,43 @@ +use anchor_lang::prelude::*; + +#[error_code] +pub enum SharedLiquidityManagerError { + #[msg("Insufficient stake amount")] + InsufficientStake, + #[msg("Proposal is not finalized")] + ProposalNotFinalized, + #[msg("No LP tokens to remove from AMM")] + NoLpTokensToRemove, + #[msg("No tokens received from AMM removal")] + NoTokensFromAmm, + #[msg("Insufficient reserves returned to spot AMM (less than 99.5%)")] + InsufficientReservesReturned, + #[msg("Pool is currently being used by an active proposal")] + PoolInUse, + #[msg("User does not have enough LP shares to withdraw")] + InsufficientLpShares, + #[msg("Slippage exceeded minimum token amounts")] + SlippageExceeded, + #[msg("No LP tokens in pool's LP token account")] + NoLpTokensInPool, + #[msg("Not enough LP tokens to provide liquidity to proposal")] + NotEnoughLpTokens, + #[msg("Insufficient funds")] + InsufficientFunds, + #[msg("No active proposal")] + NoActiveProposal, + #[msg("Proposal is not in draft status")] + ProposalNotInDraftStatus, + #[msg("Proposal already active")] + ProposalAlreadyActive, + #[msg("AMM already has liquidity")] + AmmAlreadyHasLiquidity, + #[msg("Question already resolved")] + QuestionAlreadyResolved, +} + + +// 1 PLTR = 100 USDC +// We put 1 PLTR in, & 100 USDC in +// I get 100 PLTR-DOWN and you get 100 PLTR-UP. +// If PLTR goes to 10 USDC, I get 90 USDC \ No newline at end of file diff --git a/programs/shared_liquidity_manager/src/instructions/common.rs b/programs/shared_liquidity_manager/src/instructions/common.rs new file mode 100644 index 000000000..7f1bbfd44 --- /dev/null +++ b/programs/shared_liquidity_manager/src/instructions/common.rs @@ -0,0 +1,43 @@ +use anchor_lang::prelude::*; + +use raydium_cpmm_cpi::program::RaydiumCpmm; +use raydium_cpmm_cpi::states::AMM_CONFIG_SEED; +use anchor_spl::associated_token::AssociatedToken; +use anchor_spl::token::{Token, TokenAccount}; + +/// Static accounts for initializing a Raydium pool, used as a common struct +/// to reduce code duplication and conserve stack space. +#[derive(Accounts)] +pub struct InitializeRaydiumPoolStaticAccounts<'info> { + /// CHECK: pool vault and lp mint authority + #[account( + seeds = [ + raydium_cpmm_cpi::AUTH_SEED.as_bytes(), + ], + seeds::program = cp_swap_program, + bump, + )] + pub raydium_authority: UncheckedAccount<'info>, + + #[account( + mut, + address = raydium_cpmm_cpi::create_pool_fee_reveiver::id(), + )] + pub create_pool_fee: Box>, + + /// CHECK: this is the amm config for the lowest fee pool, can see fees at https://api-v3.raydium.io/main/cpmm-config + #[account( + mut, + seeds = [ + AMM_CONFIG_SEED.as_bytes(), + &0_u16.to_be_bytes() + ], + seeds::program = cp_swap_program, + bump, + )] + pub amm_config: UncheckedAccount<'info>, + pub cp_swap_program: Program<'info, RaydiumCpmm>, + pub rent: Sysvar<'info, Rent>, + pub associated_token_program: Program<'info, AssociatedToken>, + pub token_program: Program<'info, Token>, +} \ No newline at end of file diff --git a/programs/shared_liquidity_manager/src/instructions/deposit_shared_liquidity.rs b/programs/shared_liquidity_manager/src/instructions/deposit_shared_liquidity.rs new file mode 100644 index 000000000..924afae4a --- /dev/null +++ b/programs/shared_liquidity_manager/src/instructions/deposit_shared_liquidity.rs @@ -0,0 +1,274 @@ +use anchor_lang::prelude::*; +use anchor_spl::{ + associated_token::AssociatedToken, + token::{Mint, Token, TokenAccount, TransferChecked}, + token_interface::Token2022, +}; + +use crate::error::SharedLiquidityManagerError; +use crate::state::{LiquidityPosition, SharedLiquidityPool}; +use raydium_cpmm_cpi::cpi::accounts::Deposit as RaydiumDeposit; +use raydium_cpmm_cpi::states::PoolState as RaydiumPoolState; + +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct DepositSharedLiquidityParams { + /// The amount of LP tokens to mint + pub lp_token_amount: u64, + /// The maximum amount of quote tokens to deposit + pub max_quote_token_amount: u64, + /// The maximum amount of base tokens to deposit + pub max_base_token_amount: u64, +} + +#[event_cpi] +#[derive(Accounts)] +pub struct DepositSharedLiquidity<'info> { + #[account( + mut, + has_one = active_spot_pool, + has_one = sl_pool_spot_lp_vault, + has_one = base_mint, + has_one = quote_mint, + )] + pub sl_pool: Account<'info, SharedLiquidityPool>, + + #[account(mut)] + pub active_spot_pool: AccountLoader<'info, RaydiumPoolState>, + + #[account(mut)] + pub sl_pool_spot_lp_vault: Box>, + + #[account( + mut, + token::mint = sl_pool.quote_mint, + token::authority = user, + )] + pub user_quote_token_account: Box>, + + #[account( + mut, + token::mint = sl_pool.base_mint, + token::authority = user, + )] + pub user_base_token_account: Box>, + + #[account(mut)] + pub spot_pool_base_vault: Box>, + #[account(mut)] + pub spot_pool_quote_vault: Box>, + + pub base_mint: Box>, + pub quote_mint: Box>, + + #[account(mut)] + pub spot_pool_lp_mint: Box>, + + #[account( + init_if_needed, + payer = payer, + associated_token::mint = spot_pool_lp_mint, + associated_token::authority = user, + )] + pub user_lp_token_account: Box>, + + #[account( + init_if_needed, + payer = payer, + space = 8 + std::mem::size_of::(), + seeds = [b"sl_pool_position", sl_pool.key().as_ref(), user.key().as_ref()], + bump + )] + pub user_sl_pool_position: Account<'info, LiquidityPosition>, + + pub user: Signer<'info>, + #[account(mut)] + pub payer: Signer<'info>, + + /// CHECK: pool vault and lp mint authority + #[account( + seeds = [ + raydium_cpmm_cpi::AUTH_SEED.as_bytes(), + ], + seeds::program = cp_swap_program, + bump, + )] + pub raydium_authority: UncheckedAccount<'info>, + pub associated_token_program: Program<'info, AssociatedToken>, + pub token_program: Program<'info, Token>, + pub token_program_2022: Program<'info, Token2022>, + pub cp_swap_program: Program<'info, raydium_cpmm_cpi::program::RaydiumCpmm>, + pub system_program: Program<'info, System>, +} + +impl DepositSharedLiquidity<'_> { + pub fn validate(&self, params: &DepositSharedLiquidityParams) -> Result<()> { + // Ensure the pool is not being used by an active proposal + require!( + self.sl_pool.active_proposal.is_none(), + SharedLiquidityManagerError::PoolInUse + ); + + require_gte!( + self.user_base_token_account.amount, + params.max_base_token_amount, + SharedLiquidityManagerError::InsufficientFunds + ); + require_gte!( + self.user_quote_token_account.amount, + params.max_quote_token_amount, + SharedLiquidityManagerError::InsufficientFunds + ); + + Ok(()) + } + + pub fn handle(ctx: Context, params: DepositSharedLiquidityParams) -> Result<()> { + let ( + token_0_account, + token_1_account, + token_0_vault, + token_1_vault, + vault_0_mint, + vault_1_mint, + maximum_token_0_amount, + maximum_token_1_amount, + ) = if ctx.accounts.sl_pool.is_base_token_0 { + ( + ctx.accounts.user_base_token_account.to_account_info(), + ctx.accounts.user_quote_token_account.to_account_info(), + ctx.accounts.spot_pool_base_vault.to_account_info(), + ctx.accounts.spot_pool_quote_vault.to_account_info(), + ctx.accounts.base_mint.to_account_info(), + ctx.accounts.quote_mint.to_account_info(), + params.max_base_token_amount, + params.max_quote_token_amount, + ) + } else { + ( + ctx.accounts.user_quote_token_account.to_account_info(), + ctx.accounts.user_base_token_account.to_account_info(), + ctx.accounts.spot_pool_quote_vault.to_account_info(), + ctx.accounts.spot_pool_base_vault.to_account_info(), + ctx.accounts.quote_mint.to_account_info(), + ctx.accounts.base_mint.to_account_info(), + params.max_quote_token_amount, + params.max_base_token_amount, + ) + }; + + raydium_cpmm_cpi::cpi::deposit( + CpiContext::new( + ctx.accounts.cp_swap_program.to_account_info(), + RaydiumDeposit { + owner: ctx.accounts.user.to_account_info(), + authority: ctx.accounts.raydium_authority.to_account_info(), + pool_state: ctx.accounts.active_spot_pool.to_account_info(), + owner_lp_token: ctx.accounts.user_lp_token_account.to_account_info(), + token_0_account, + token_1_account, + token_0_vault, + token_1_vault, + token_program: ctx.accounts.token_program.to_account_info(), + token_program_2022: ctx.accounts.token_program_2022.to_account_info(), + vault_0_mint, + vault_1_mint, + lp_mint: ctx.accounts.spot_pool_lp_mint.to_account_info(), + }, + ), + params.lp_token_amount, + maximum_token_0_amount, + maximum_token_1_amount, + )?; + + // Transfer LP tokens from user to pool vault + anchor_spl::token::transfer_checked( + CpiContext::new( + ctx.accounts.token_program.to_account_info(), + TransferChecked { + from: ctx.accounts.user_lp_token_account.to_account_info(), + mint: ctx.accounts.spot_pool_lp_mint.to_account_info(), + to: ctx.accounts.sl_pool_spot_lp_vault.to_account_info(), + authority: ctx.accounts.user.to_account_info(), + }, + ), + params.lp_token_amount, + ctx.accounts.spot_pool_lp_mint.decimals, + )?; + + // Update / initialize the position + let position = &mut ctx.accounts.user_sl_pool_position; + position.owner = ctx.accounts.user.key(); + position.pool = ctx.accounts.sl_pool.key(); + position.underlying_spot_lp_shares += params.lp_token_amount; + position.bump = ctx.bumps.user_sl_pool_position; + + Ok(()) + } +} + +#[cfg(test)] +mod deposit_tests { + use super::*; + use crate::state::SharedLiquidityPool; + + fn create_mock_sl_pool(active_proposal: Option) -> SharedLiquidityPool { + SharedLiquidityPool { + pda_bump: 0, + dao: Pubkey::default(), + base_mint: Pubkey::default(), + quote_mint: Pubkey::default(), + sl_pool_signer: Pubkey::default(), + sl_pool_signer_bump: 0, + sl_pool_base_vault: Pubkey::default(), + sl_pool_quote_vault: Pubkey::default(), + sl_pool_spot_lp_vault: Pubkey::default(), + active_proposal, + proposal_stake_rate_threshold_bps: 1000, + seq_num: 0, + active_spot_pool: Pubkey::default(), + active_spot_pool_index: 0, + is_base_token_0: true, + } + } + + #[test] + pub fn test_validate_pool_not_in_use() { + let sl_pool = create_mock_sl_pool(None); + let mock_ctx = MockDepositContext { sl_pool }; + + let result = mock_ctx.validate(); + assert!(result.is_ok()); + } + + #[test] + pub fn test_validate_pool_in_use() { + let sl_pool = create_mock_sl_pool(Some(Pubkey::new_unique())); + let mock_ctx = MockDepositContext { sl_pool }; + + let result = mock_ctx.validate(); + assert!(result.is_err()); + let error = result.unwrap_err(); + match error { + anchor_lang::error::Error::AnchorError(anchor_error) => { + assert_eq!(anchor_error.error_code_number, 6005); // PoolInUse error code + assert_eq!(anchor_error.error_name, "PoolInUse"); + } + _ => panic!("Expected AnchorError"), + } + } + + // Mock context struct for testing validation logic + struct MockDepositContext { + sl_pool: SharedLiquidityPool, + } + + impl MockDepositContext { + fn validate(&self) -> Result<()> { + require!( + self.sl_pool.active_proposal.is_none(), + SharedLiquidityManagerError::PoolInUse + ); + Ok(()) + } + } +} diff --git a/programs/shared_liquidity_manager/src/instructions/initialize_draft_proposal.rs b/programs/shared_liquidity_manager/src/instructions/initialize_draft_proposal.rs new file mode 100644 index 000000000..2e26c05fd --- /dev/null +++ b/programs/shared_liquidity_manager/src/instructions/initialize_draft_proposal.rs @@ -0,0 +1,60 @@ +use anchor_lang::prelude::*; + +use anchor_spl::associated_token::AssociatedToken; +use anchor_spl::token::{Mint, Token, TokenAccount}; + +use crate::state::{DraftProposal, DraftProposalStatus, ProposalInstruction, SharedLiquidityPool}; + + +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct InitializeDraftProposalParams { + pub instruction: ProposalInstruction, + /// The nonce for the draft proposal, not used for anything aside from the PDA + pub draft_proposal_nonce: u64, +} + +#[event_cpi] +#[derive(Accounts)] +#[instruction(args: InitializeDraftProposalParams)] +pub struct InitializeDraftProposal<'info> { + #[account( + init, + payer = payer, + space = 1500, + seeds = [b"draft_proposal", args.draft_proposal_nonce.to_le_bytes().as_ref()], + bump + )] + pub draft_proposal: Box>, + #[account(has_one = base_mint)] + pub shared_liquidity_pool: Box>, + pub base_mint: Account<'info, Mint>, + #[account( + init_if_needed, + payer = payer, + associated_token::mint = base_mint, + associated_token::authority = draft_proposal, + )] + pub staked_token_vault: Account<'info, TokenAccount>, + #[account(mut)] + pub payer: Signer<'info>, + pub token_program: Program<'info, Token>, + pub associated_token_program: Program<'info, AssociatedToken>, + pub system_program: Program<'info, System>, +} + +impl InitializeDraftProposal<'_> { + pub fn handle(ctx: Context, params: InitializeDraftProposalParams) -> Result<()> { + ctx.accounts.draft_proposal.set_inner(DraftProposal { + base_mint: ctx.accounts.base_mint.key(), + instruction: params.instruction, + staked_token_amount: 0, + status: DraftProposalStatus::Draft, + staked_token_vault: ctx.accounts.staked_token_vault.key(), + shared_liquidity_pool: ctx.accounts.shared_liquidity_pool.key(), + nonce: params.draft_proposal_nonce, + pda_bump: ctx.bumps.draft_proposal, + }); + + Ok(()) + } +} diff --git a/programs/shared_liquidity_manager/src/instructions/initialize_proposal_with_liquidity.rs b/programs/shared_liquidity_manager/src/instructions/initialize_proposal_with_liquidity.rs new file mode 100644 index 000000000..bd1d00401 --- /dev/null +++ b/programs/shared_liquidity_manager/src/instructions/initialize_proposal_with_liquidity.rs @@ -0,0 +1,626 @@ +use anchor_lang::prelude::*; +use anchor_spl::token::{Mint, TokenAccount}; + +use raydium_cpmm_cpi::cpi::accounts::Withdraw as RaydiumWithdraw; + +use crate::error::SharedLiquidityManagerError; +use crate::state::{DraftProposal, DraftProposalStatus, SharedLiquidityPool}; + +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct InitializeProposalWithLiquidityParams { + pub nonce: u64, +} + +#[derive(Accounts)] +pub struct InitializeProposalRaydiumAccounts<'info> { + #[account(mut)] + pub spot_pool: AccountLoader<'info, raydium_cpmm_cpi::states::PoolState>, + #[account(mut)] + pub spot_pool_base_vault: Box>, + #[account(mut)] + pub spot_pool_quote_vault: Box>, + #[account(mut)] + pub lp_mint: Box>, + /// CHECK: Raydium authority PDA + pub raydium_authority: UncheckedAccount<'info>, + pub token_program: Program<'info, anchor_spl::token::Token>, + pub token_program_2022: Program<'info, anchor_spl::token_interface::Token2022>, + pub cp_swap_program: Program<'info, raydium_cpmm_cpi::program::RaydiumCpmm>, + /// CHECK: SPL Memo program + #[account(address = spl_memo::id())] + pub memo_program: UncheckedAccount<'info>, +} + +#[derive(Accounts)] +pub struct InitializeProposalConditionalVaultAccounts<'info> { + #[account(mut)] + pub question: Account<'info, conditional_vault::state::Question>, + #[account(mut)] + pub base_vault: Account<'info, conditional_vault::state::ConditionalVault>, + #[account(mut)] + pub quote_vault: Account<'info, conditional_vault::state::ConditionalVault>, + #[account(mut, address = base_vault.underlying_token_account)] + pub base_vault_underlying_token_account: Box>, + #[account(mut, address = quote_vault.underlying_token_account)] + pub quote_vault_underlying_token_account: Box>, + pub conditional_vault_program: Program<'info, conditional_vault::program::ConditionalVault>, + #[account(mut)] + pub pass_base_mint: Box>, + #[account(mut)] + pub fail_base_mint: Box>, + #[account(mut)] + pub pass_quote_mint: Box>, + #[account(mut)] + pub fail_quote_mint: Box>, + #[account(init, payer = payer, token::mint = pass_base_mint, token::authority = sl_pool_signer)] + pub sl_pool_pass_base_vault: Box>, + #[account(init, payer = payer, token::mint = fail_base_mint, token::authority = sl_pool_signer)] + pub sl_pool_fail_base_vault: Box>, + #[account(init, payer = payer, token::mint = pass_quote_mint, token::authority = sl_pool_signer)] + pub sl_pool_pass_quote_vault: Box>, + #[account(init, payer = payer, token::mint = fail_quote_mint, token::authority = sl_pool_signer)] + pub sl_pool_fail_quote_vault: Box>, + /// CHECK: verified by conditional_vault + pub vault_event_authority: UncheckedAccount<'info>, + #[account(mut)] + pub payer: Signer<'info>, + pub token_program: Program<'info, anchor_spl::token::Token>, + pub system_program: Program<'info, System>, + /// CHECK: the signer + #[account(mut)] + pub sl_pool_signer: UncheckedAccount<'info>, +} + +#[derive(Accounts)] +pub struct InitializeProposalAmmAccounts<'info> { + #[account(mut)] + pub pass_amm: Account<'info, amm::state::Amm>, + #[account(mut)] + pub fail_amm: Account<'info, amm::state::Amm>, + #[account(mut)] + pub pass_lp_mint: Box>, + #[account(mut)] + pub fail_lp_mint: Box>, + #[account(init_if_needed, payer = payer, associated_token::mint = pass_lp_mint, associated_token::authority = sl_pool_signer)] + pub sl_pool_pass_lp_account: Box>, + #[account(init_if_needed, payer = payer, associated_token::mint = fail_lp_mint, associated_token::authority = sl_pool_signer)] + pub sl_pool_fail_lp_account: Box>, + #[account(mut)] + pub pass_amm_vault_ata_base: Box>, + #[account(mut)] + pub pass_amm_vault_ata_quote: Box>, + #[account(mut)] + pub fail_amm_vault_ata_base: Box>, + #[account(mut)] + pub fail_amm_vault_ata_quote: Box>, + #[account(mut)] + pub proposal_pass_lp_vault: Box>, + #[account(mut)] + pub proposal_fail_lp_vault: Box>, + pub amm_program: Program<'info, amm::program::Amm>, + /// CHECK: verified by amm + pub event_authority: UncheckedAccount<'info>, + #[account(mut)] + pub payer: Signer<'info>, + pub system_program: Program<'info, System>, + pub token_program: Program<'info, anchor_spl::token::Token>, + pub associated_token_program: Program<'info, anchor_spl::associated_token::AssociatedToken>, + /// CHECK: the signer + pub sl_pool_signer: UncheckedAccount<'info>, +} + +#[event_cpi] +#[derive(Accounts)] +pub struct InitializeProposalWithLiquidity<'info> { + // Shared liquidity pool state + #[account(mut, + has_one = sl_pool_base_vault, + has_one = sl_pool_quote_vault, + has_one = sl_pool_spot_lp_vault, + has_one = base_mint, + has_one = quote_mint, + constraint = shared_liquidity_pool.active_spot_pool == raydium.spot_pool.key() + )] + pub shared_liquidity_pool: Account<'info, SharedLiquidityPool>, + pub proposal_creator: Signer<'info>, + /// CHECK: initialized by autocrat + #[account(mut)] + pub proposal: UncheckedAccount<'info>, + + #[account(mut)] + pub sl_pool_base_vault: Box>, + #[account(mut)] + pub sl_pool_quote_vault: Box>, + #[account(mut)] + pub sl_pool_spot_lp_vault: Box>, + + pub base_mint: Box>, + pub quote_mint: Box>, + + // Raydium accounts + pub raydium: InitializeProposalRaydiumAccounts<'info>, + + // Conditional vault accounts + pub conditional_vault: InitializeProposalConditionalVaultAccounts<'info>, + + // AMM accounts + pub amm: InitializeProposalAmmAccounts<'info>, + + #[account(mut, has_one = shared_liquidity_pool)] + pub draft_proposal: Box>, + + // Autocrat accounts + #[account(mut)] + pub dao: Box>, + pub autocrat_program: Program<'info, autocrat::program::Autocrat>, + pub system_program: Program<'info, System>, + /// CHECK: verified by autocrat + pub autocrat_event_authority: UncheckedAccount<'info>, +} + +impl InitializeProposalWithLiquidity<'_> { + pub fn validate(&self) -> Result<()> { + // Check stake threshold + let total_supply = self.base_mint.supply; + let stake_threshold = (total_supply + * self.shared_liquidity_pool.proposal_stake_rate_threshold_bps as u64) + / 10_000; + require_gte!(self.draft_proposal.staked_token_amount, stake_threshold, SharedLiquidityManagerError::InsufficientStake); + + // Check draft proposal status + require_eq!(self.draft_proposal.status, DraftProposalStatus::Draft, SharedLiquidityManagerError::ProposalNotInDraftStatus); + + // Check that there's no active proposal + require!( + self.shared_liquidity_pool.active_proposal.is_none(), + SharedLiquidityManagerError::ProposalAlreadyActive + ); + + // Validate conditional vault account relationships + require_keys_eq!( + self.conditional_vault.fail_base_mint.key(), + self.conditional_vault.base_vault.conditional_token_mints[0] + ); + + require_keys_eq!( + self.conditional_vault.pass_base_mint.key(), + self.conditional_vault.base_vault.conditional_token_mints[1] + ); + + require_keys_eq!( + self.conditional_vault.fail_quote_mint.key(), + self.conditional_vault.quote_vault.conditional_token_mints[0] + ); + + require_keys_eq!( + self.conditional_vault.pass_quote_mint.key(), + self.conditional_vault.quote_vault.conditional_token_mints[1] + ); + + require_keys_eq!( + self.conditional_vault.sl_pool_signer.key(), + self.shared_liquidity_pool.sl_pool_signer + ); + + // Validate AMM account relationships + require_keys_eq!( + self.amm.pass_lp_mint.key(), + self.amm.pass_amm.lp_mint + ); + + require_keys_eq!( + self.amm.fail_lp_mint.key(), + self.amm.fail_amm.lp_mint + ); + + require_keys_eq!( + self.amm.pass_amm_vault_ata_base.key(), + self.amm.pass_amm.vault_ata_base + ); + + require_keys_eq!( + self.amm.pass_amm_vault_ata_quote.key(), + self.amm.pass_amm.vault_ata_quote + ); + + require_keys_eq!( + self.amm.fail_amm_vault_ata_base.key(), + self.amm.fail_amm.vault_ata_base + ); + + require_keys_eq!( + self.amm.fail_amm_vault_ata_quote.key(), + self.amm.fail_amm.vault_ata_quote + ); + + require_keys_eq!( + self.amm.sl_pool_signer.key(), + self.shared_liquidity_pool.sl_pool_signer + ); + + // Validate that AMMs are empty (no existing liquidity) + require_eq!(self.amm.pass_lp_mint.supply, 0, SharedLiquidityManagerError::AmmAlreadyHasLiquidity); + require_eq!(self.amm.fail_lp_mint.supply, 0, SharedLiquidityManagerError::AmmAlreadyHasLiquidity); + + // Validate that the question is not resolved yet + require!( + !self.conditional_vault.question.is_resolved(), + SharedLiquidityManagerError::QuestionAlreadyResolved + ); + + // Validate draft proposal belongs to this shared liquidity pool + require_keys_eq!( + self.draft_proposal.shared_liquidity_pool, + self.shared_liquidity_pool.key() + ); + + // Validate that the question belongs to the conditional vaults + require_keys_eq!( + self.conditional_vault.question.key(), + self.conditional_vault.base_vault.question + ); + + require_keys_eq!( + self.conditional_vault.question.key(), + self.conditional_vault.quote_vault.question + ); + + Ok(()) + } + + pub fn handle(ctx: Context, params: InitializeProposalWithLiquidityParams) -> Result<()> { + // 1. Withdraw half of the pool's LP tokens from Raydium + let pool_lp_balance = ctx.accounts.sl_pool_spot_lp_vault.amount; + require!( + pool_lp_balance > 0, + SharedLiquidityManagerError::NoLpTokensInPool + ); + let half_lp = pool_lp_balance / 2; + require!(half_lp > 0, SharedLiquidityManagerError::NotEnoughLpTokens); + + // Get initial token balances + let initial_base_balance = ctx.accounts.sl_pool_base_vault.amount; + let initial_quote_balance = ctx.accounts.sl_pool_quote_vault.amount; + + let ( + token_0_account, + token_1_account, + vault_0_mint, + vault_1_mint, + token_0_vault, + token_1_vault, + ) = if ctx.accounts.shared_liquidity_pool.is_base_token_0 { + ( + ctx.accounts.sl_pool_base_vault.to_account_info(), + ctx.accounts.sl_pool_quote_vault.to_account_info(), + ctx.accounts.base_mint.to_account_info(), + ctx.accounts.quote_mint.to_account_info(), + ctx.accounts.raydium.spot_pool_base_vault.to_account_info(), + ctx.accounts.raydium.spot_pool_quote_vault.to_account_info(), + ) + } else { + ( + ctx.accounts.sl_pool_quote_vault.to_account_info(), + ctx.accounts.sl_pool_base_vault.to_account_info(), + ctx.accounts.quote_mint.to_account_info(), + ctx.accounts.base_mint.to_account_info(), + ctx.accounts.raydium.spot_pool_quote_vault.to_account_info(), + ctx.accounts.raydium.spot_pool_base_vault.to_account_info(), + ) + }; + + let sl_pool_key = ctx.accounts.shared_liquidity_pool.key(); + let seeds = &[ + b"sl_pool_signer".as_ref(), + sl_pool_key.as_ref(), + &[ctx.accounts.shared_liquidity_pool.sl_pool_signer_bump], + ]; + let signer = &[&seeds[..]]; + + // Withdraw half from Raydium + raydium_cpmm_cpi::cpi::withdraw( + CpiContext::new_with_signer( + ctx.accounts.raydium.cp_swap_program.to_account_info(), + RaydiumWithdraw { + owner: ctx + .accounts + .conditional_vault + .sl_pool_signer + .to_account_info(), + authority: ctx.accounts.raydium.raydium_authority.to_account_info(), + pool_state: ctx.accounts.raydium.spot_pool.to_account_info(), + lp_mint: ctx.accounts.raydium.lp_mint.to_account_info(), + memo_program: ctx.accounts.raydium.memo_program.to_account_info(), + owner_lp_token: ctx.accounts.sl_pool_spot_lp_vault.to_account_info(), + token_0_account, + token_1_account, + vault_0_mint, + vault_1_mint, + token_0_vault, + token_1_vault, + token_program: ctx.accounts.raydium.token_program.to_account_info(), + token_program_2022: ctx.accounts.raydium.token_program_2022.to_account_info(), + }, + signer, + ), + half_lp, + 0, + 0, + )?; + + // Calculate how many tokens we got from the withdraw + + ctx.accounts.sl_pool_base_vault.reload()?; + ctx.accounts.sl_pool_quote_vault.reload()?; + + let base_withdrawn = ctx.accounts.sl_pool_base_vault.amount - initial_base_balance; + let quote_withdrawn = ctx.accounts.sl_pool_quote_vault.amount - initial_quote_balance; + + require!( + base_withdrawn > ctx.accounts.dao.min_base_futarchic_liquidity, + SharedLiquidityManagerError::NotEnoughLpTokens + ); + require!( + quote_withdrawn > ctx.accounts.dao.min_quote_futarchic_liquidity, + SharedLiquidityManagerError::NotEnoughLpTokens + ); + + // Split base + conditional_vault::cpi::split_tokens( + CpiContext::new_with_signer( + ctx.accounts + .conditional_vault + .conditional_vault_program + .to_account_info(), + conditional_vault::cpi::accounts::InteractWithVault { + question: ctx.accounts.conditional_vault.question.to_account_info(), + vault: ctx.accounts.conditional_vault.base_vault.to_account_info(), + vault_underlying_token_account: ctx + .accounts + .conditional_vault + .base_vault_underlying_token_account + .to_account_info(), + authority: ctx + .accounts + .conditional_vault + .sl_pool_signer + .to_account_info(), + user_underlying_token_account: ctx + .accounts + .sl_pool_base_vault + .to_account_info(), + event_authority: ctx + .accounts + .conditional_vault + .vault_event_authority + .to_account_info(), + program: ctx + .accounts + .conditional_vault + .conditional_vault_program + .to_account_info(), + token_program: ctx.accounts.raydium.token_program.to_account_info(), + }, + signer, + ) + .with_remaining_accounts(vec![ + ctx.accounts + .conditional_vault + .fail_base_mint + .to_account_info(), + ctx.accounts + .conditional_vault + .pass_base_mint + .to_account_info(), + ctx.accounts + .conditional_vault + .sl_pool_fail_base_vault + .to_account_info(), + ctx.accounts + .conditional_vault + .sl_pool_pass_base_vault + .to_account_info(), + ]), + base_withdrawn, + )?; + + // Split quote + conditional_vault::cpi::split_tokens( + CpiContext::new_with_signer( + ctx.accounts + .conditional_vault + .conditional_vault_program + .to_account_info(), + conditional_vault::cpi::accounts::InteractWithVault { + question: ctx.accounts.conditional_vault.question.to_account_info(), + vault: ctx.accounts.conditional_vault.quote_vault.to_account_info(), + vault_underlying_token_account: ctx + .accounts + .conditional_vault + .quote_vault_underlying_token_account + .to_account_info(), + authority: ctx + .accounts + .conditional_vault + .sl_pool_signer + .to_account_info(), + user_underlying_token_account: ctx + .accounts + .sl_pool_quote_vault + .to_account_info(), + event_authority: ctx + .accounts + .conditional_vault + .vault_event_authority + .to_account_info(), + program: ctx + .accounts + .conditional_vault + .conditional_vault_program + .to_account_info(), + token_program: ctx.accounts.raydium.token_program.to_account_info(), + }, + signer, + ) + .with_remaining_accounts(vec![ + ctx.accounts + .conditional_vault + .fail_quote_mint + .to_account_info(), + ctx.accounts + .conditional_vault + .pass_quote_mint + .to_account_info(), + ctx.accounts + .conditional_vault + .sl_pool_fail_quote_vault + .to_account_info(), + ctx.accounts + .conditional_vault + .sl_pool_pass_quote_vault + .to_account_info(), + ]), + quote_withdrawn, + )?; + + // LP into the pass and fail AMMs + + require_eq!(ctx.accounts.amm.pass_lp_mint.supply, 0); + require_eq!(ctx.accounts.amm.fail_lp_mint.supply, 0); + + amm::cpi::add_liquidity( + CpiContext::new_with_signer( + ctx.accounts.amm.amm_program.to_account_info(), + amm::cpi::accounts::AddOrRemoveLiquidity { + amm: ctx.accounts.amm.pass_amm.to_account_info(), + user: ctx + .accounts + .conditional_vault + .sl_pool_signer + .to_account_info(), + user_lp_account: ctx.accounts.amm.sl_pool_pass_lp_account.to_account_info(), + user_base_account: ctx + .accounts + .conditional_vault + .sl_pool_pass_base_vault + .to_account_info(), + user_quote_account: ctx + .accounts + .conditional_vault + .sl_pool_pass_quote_vault + .to_account_info(), + vault_ata_base: ctx.accounts.amm.pass_amm_vault_ata_base.to_account_info(), + vault_ata_quote: ctx.accounts.amm.pass_amm_vault_ata_quote.to_account_info(), + event_authority: ctx.accounts.amm.event_authority.to_account_info(), + program: ctx.accounts.amm.amm_program.to_account_info(), + lp_mint: ctx.accounts.amm.pass_lp_mint.to_account_info(), + token_program: ctx.accounts.raydium.token_program.to_account_info(), + }, + signer, + ), + amm::instructions::AddLiquidityArgs { + max_base_amount: base_withdrawn, + quote_amount: quote_withdrawn, + min_lp_tokens: quote_withdrawn, + }, + )?; + + amm::cpi::add_liquidity( + CpiContext::new_with_signer( + ctx.accounts.amm.amm_program.to_account_info(), + amm::cpi::accounts::AddOrRemoveLiquidity { + amm: ctx.accounts.amm.fail_amm.to_account_info(), + user: ctx + .accounts + .conditional_vault + .sl_pool_signer + .to_account_info(), + user_lp_account: ctx.accounts.amm.sl_pool_fail_lp_account.to_account_info(), + user_base_account: ctx + .accounts + .conditional_vault + .sl_pool_fail_base_vault + .to_account_info(), + user_quote_account: ctx + .accounts + .conditional_vault + .sl_pool_fail_quote_vault + .to_account_info(), + vault_ata_base: ctx.accounts.amm.fail_amm_vault_ata_base.to_account_info(), + vault_ata_quote: ctx.accounts.amm.fail_amm_vault_ata_quote.to_account_info(), + event_authority: ctx.accounts.amm.event_authority.to_account_info(), + program: ctx.accounts.amm.amm_program.to_account_info(), + lp_mint: ctx.accounts.amm.fail_lp_mint.to_account_info(), + token_program: ctx.accounts.raydium.token_program.to_account_info(), + }, + signer, + ), + amm::instructions::AddLiquidityArgs { + max_base_amount: base_withdrawn, + quote_amount: quote_withdrawn, + min_lp_tokens: quote_withdrawn, + }, + )?; + + autocrat::cpi::initialize_proposal( + CpiContext::new_with_signer( + ctx.accounts.autocrat_program.to_account_info(), + autocrat::cpi::accounts::InitializeProposal { + proposal: ctx.accounts.proposal.to_account_info(), + dao: ctx.accounts.dao.to_account_info(), + question: ctx.accounts.conditional_vault.question.to_account_info(), + quote_vault: ctx.accounts.conditional_vault.quote_vault.to_account_info(), + base_vault: ctx.accounts.conditional_vault.base_vault.to_account_info(), + pass_amm: ctx.accounts.amm.pass_amm.to_account_info(), + pass_lp_mint: ctx.accounts.amm.pass_lp_mint.to_account_info(), + fail_amm: ctx.accounts.amm.fail_amm.to_account_info(), + fail_lp_mint: ctx.accounts.amm.fail_lp_mint.to_account_info(), + pass_lp_user_account: ctx + .accounts + .amm + .sl_pool_pass_lp_account + .to_account_info(), + fail_lp_user_account: ctx + .accounts + .amm + .sl_pool_fail_lp_account + .to_account_info(), + pass_lp_vault_account: ctx + .accounts + .amm + .proposal_pass_lp_vault + .to_account_info(), + fail_lp_vault_account: ctx + .accounts + .amm + .proposal_fail_lp_vault + .to_account_info(), + proposer: ctx + .accounts + .conditional_vault + .sl_pool_signer + .to_account_info(), + payer: ctx.accounts.proposal_creator.to_account_info(), + event_authority: ctx.accounts.autocrat_event_authority.to_account_info(), + program: ctx.accounts.autocrat_program.to_account_info(), + token_program: ctx.accounts.raydium.token_program.to_account_info(), + system_program: ctx.accounts.system_program.to_account_info(), + }, + signer, + ), + autocrat::instructions::InitializeProposalParams { + description_url: "".to_string(), + instruction: ctx.accounts.draft_proposal.instruction.clone().into(), + pass_lp_tokens_to_lock: quote_withdrawn, + fail_lp_tokens_to_lock: quote_withdrawn, + nonce: params.nonce, + }, + )?; + + ctx.accounts.draft_proposal.status = DraftProposalStatus::Initialized; + + ctx.accounts.shared_liquidity_pool.active_proposal = Some(ctx.accounts.proposal.key()); + + Ok(()) + } +} diff --git a/programs/shared_liquidity_manager/src/instructions/initialize_shared_liquidity_pool.rs b/programs/shared_liquidity_manager/src/instructions/initialize_shared_liquidity_pool.rs new file mode 100644 index 000000000..97949a3a3 --- /dev/null +++ b/programs/shared_liquidity_manager/src/instructions/initialize_shared_liquidity_pool.rs @@ -0,0 +1,362 @@ +//! Initializes a shared liquidity pool. +//! +//! The pool creator provides the initial liquidity and can't +//! be frontrun +use anchor_lang::prelude::*; +use anchor_lang::Discriminator; +use anchor_spl::associated_token; + +use crate::error::SharedLiquidityManagerError; +use crate::state::{LiquidityPosition, SharedLiquidityPool}; +use crate::instructions::common::*; + +use anchor_spl::associated_token::get_associated_token_address; +use anchor_spl::token::{Mint, TokenAccount, Transfer}; + +use autocrat::state::Dao; +use raydium_cpmm_cpi::{ + cpi, instruction, + program::RaydiumCpmm, + states::{OBSERVATION_SEED, POOL_LP_MINT_SEED, POOL_VAULT_SEED}, +}; + +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct InitializeSharedLiquidityPoolParams { + pub base_amount: u64, + pub quote_amount: u64, + pub proposal_stake_rate_threshold_bps: u16, +} + +#[event_cpi] +#[derive(Accounts)] +#[instruction(params: InitializeSharedLiquidityPoolParams)] +pub struct InitializeSharedLiquidityPool<'info> { + #[account( + init, + payer = creator, + space = 8 + std::mem::size_of::(), + seeds = [b"sl_pool", dao.key().as_ref(), creator.key().as_ref(), ¶ms.proposal_stake_rate_threshold_bps.to_le_bytes()], + bump + )] + pub sl_pool: Box>, + pub dao: Box>, + // normally we'd separate out the payer, but raydium requires the creator to pay for the pool creation fee anyway + #[account(mut)] + pub creator: Signer<'info>, + + #[account( + init_if_needed, + payer = creator, + space = 8 + std::mem::size_of::(), + seeds = [b"sl_pool_position", sl_pool.key().as_ref(), creator.key().as_ref()], + bump + )] + pub creator_sl_pool_position: Box>, + + pub base_mint: Box>, + pub quote_mint: Box>, + + /// CHECK: this is the shared liquidity pool's lp vault, we initialize it post initializing the spot pool + #[account(mut, address = get_associated_token_address(sl_pool_signer.key, spot_pool_lp_mint.key))] + pub sl_pool_spot_lp_vault: UncheckedAccount<'info>, + + #[account( + mut, + token::mint = quote_mint, + token::authority = creator, + )] + pub creator_quote_token_account: Box>, + + #[account( + mut, + token::mint = base_mint, + token::authority = creator, + )] + pub creator_base_token_account: Box>, + + /// CHECK: this can't be initialized because the lp mint is not created yet, + /// so Raydium will create it + #[account( + mut, + address = get_associated_token_address( + creator.key, + spot_pool_lp_mint.key, + ) + )] + pub creator_lp_account: UncheckedAccount<'info>, + + pub raydium_init_pool_static: InitializeRaydiumPoolStaticAccounts<'info>, + + /// CHECK: this is the first spot pool, init by cp-swap, we use 0 in the seed to indicate it's the first spot pool + #[account( + mut, + seeds = [ + b"spot_pool", + sl_pool.key().as_ref(), + &0_u32.to_le_bytes() + ], + bump, + )] + pub spot_pool: UncheckedAccount<'info>, + + /// CHECK: pool lp mint, init by cp-swap + #[account( + mut, + seeds = [ + POOL_LP_MINT_SEED.as_bytes(), + spot_pool.key().as_ref(), + ], + seeds::program = cp_swap_program, + bump, + )] + pub spot_pool_lp_mint: UncheckedAccount<'info>, + + /// CHECK: Base vault for the spot pool, init by cp-swap + #[account( + mut, + seeds = [ + POOL_VAULT_SEED.as_bytes(), + spot_pool.key().as_ref(), + base_mint.key().as_ref() + ], + seeds::program = cp_swap_program, + bump, + )] + pub spot_pool_base_vault: UncheckedAccount<'info>, + + /// CHECK: Quote vault for the spot pool, init by cp-swap + #[account( + mut, + seeds = [ + POOL_VAULT_SEED.as_bytes(), + spot_pool.key().as_ref(), + quote_mint.key().as_ref() + ], + seeds::program = cp_swap_program, + bump, + )] + pub spot_pool_quote_vault: UncheckedAccount<'info>, + + + /// CHECK: an account to store oracle observations, init by cp-swap + #[account( + mut, + seeds = [ + OBSERVATION_SEED.as_bytes(), + spot_pool.key().as_ref(), + ], + seeds::program = cp_swap_program, + bump, + )] + pub spot_pool_observation_state: UncheckedAccount<'info>, + + /// CHECK: This is the shared liquidity pool signer + #[account( + seeds = [b"sl_pool_signer", sl_pool.key().as_ref()], + bump + )] + pub sl_pool_signer: UncheckedAccount<'info>, + + // We don't need the following two accounts, but nice to verify that they are created + #[account( + associated_token::mint = base_mint, + associated_token::authority = sl_pool_signer, + )] + pub sl_pool_base_vault: Box>, + + #[account( + associated_token::mint = quote_mint, + associated_token::authority = sl_pool_signer, + )] + pub sl_pool_quote_vault: Box>, + + pub system_program: Program<'info, System>, + pub cp_swap_program: Program<'info, RaydiumCpmm>, +} + +impl InitializeSharedLiquidityPool<'_> { + pub fn validate(&self, params: &InitializeSharedLiquidityPoolParams) -> Result<()> { + require_eq!(self.dao.token_mint, self.base_mint.key()); + require_eq!(self.dao.usdc_mint, self.quote_mint.key()); + + require_neq!(self.base_mint.key(), self.quote_mint.key()); + + // Ensure pool creator has enough tokens + require_gte!( + self.creator_base_token_account.amount, + params.base_amount, + SharedLiquidityManagerError::InsufficientFunds + ); + require_gte!( + self.creator_quote_token_account.amount, + params.quote_amount, + SharedLiquidityManagerError::InsufficientFunds + ); + + Ok(()) + } + + pub fn handle(ctx: Context, params: InitializeSharedLiquidityPoolParams) -> Result<()> { + // Raydium requires that token_0 < token_1 + let ( + token_0_mint, + token_1_mint, + token_0_vault, + token_1_vault, + creator_token_0, + creator_token_1, + init_amount_0, + init_amount_1, + ) = if ctx.accounts.base_mint.key() < ctx.accounts.quote_mint.key() { + ( + ctx.accounts.base_mint.to_account_info(), + ctx.accounts.quote_mint.to_account_info(), + ctx.accounts.spot_pool_base_vault.to_account_info(), + ctx.accounts.spot_pool_quote_vault.to_account_info(), + ctx.accounts.creator_base_token_account.to_account_info(), + ctx.accounts.creator_quote_token_account.to_account_info(), + params.base_amount, + params.quote_amount, + ) + } else { + ( + ctx.accounts.quote_mint.to_account_info(), + ctx.accounts.base_mint.to_account_info(), + ctx.accounts.spot_pool_quote_vault.to_account_info(), + ctx.accounts.spot_pool_base_vault.to_account_info(), + ctx.accounts.creator_quote_token_account.to_account_info(), + ctx.accounts.creator_base_token_account.to_account_info(), + params.quote_amount, + params.base_amount, + ) + }; + + let cpi_accounts = cpi::accounts::Initialize { + creator: ctx.accounts.creator.to_account_info(), + amm_config: ctx.accounts.raydium_init_pool_static.amm_config.to_account_info(), + authority: ctx.accounts.raydium_init_pool_static.raydium_authority.to_account_info(), + pool_state: ctx.accounts.spot_pool.to_account_info(), + lp_mint: ctx.accounts.spot_pool_lp_mint.to_account_info(), + creator_lp_token: ctx.accounts.creator_lp_account.to_account_info(), + create_pool_fee: ctx.accounts.raydium_init_pool_static.create_pool_fee.to_account_info(), + observation_state: ctx.accounts.spot_pool_observation_state.to_account_info(), + token_program: ctx.accounts.raydium_init_pool_static.token_program.to_account_info(), + token_0_program: ctx.accounts.raydium_init_pool_static.token_program.to_account_info(), + token_1_program: ctx.accounts.raydium_init_pool_static.token_program.to_account_info(), + associated_token_program: ctx.accounts.raydium_init_pool_static.associated_token_program.to_account_info(), + system_program: ctx.accounts.system_program.to_account_info(), + rent: ctx.accounts.raydium_init_pool_static.rent.to_account_info(), + token_0_mint, + token_1_mint, + token_0_vault, + token_1_vault, + creator_token_0, + creator_token_1, + }; + + let ix = instruction::Initialize { + init_amount_0, + init_amount_1, + open_time: 0, + }; + let mut ix_data = Vec::with_capacity(256); + ix_data.extend_from_slice(&instruction::Initialize::discriminator()); + AnchorSerialize::serialize(&ix, &mut ix_data)?; + + let ix = solana_program::instruction::Instruction { + program_id: ctx.accounts.cp_swap_program.key(), + accounts: cpi_accounts + .to_account_metas(None) + .into_iter() + .zip(cpi_accounts.to_account_infos()) + .map(|mut pair| { + pair.0.is_signer = pair.1.is_signer; + if pair.0.pubkey == ctx.accounts.creator.key() + || pair.0.pubkey == ctx.accounts.spot_pool.key() + { + pair.0.is_signer = true; + } + pair.0 + }) + .collect(), + data: ix_data, + }; + + let spot_pool_index = 0_u32.to_le_bytes(); + let sl_pool_key = ctx.accounts.sl_pool.key(); + let pool_seeds = &[ + b"spot_pool", + sl_pool_key.as_ref(), + &spot_pool_index[..], + &[ctx.bumps.spot_pool], + ]; + let raydium_signer = &[&pool_seeds[..]]; + + solana_program::program::invoke_signed( + &ix, + &cpi_accounts.to_account_infos(), + raydium_signer, + )?; + + // First, initialize the shared liquidity pool's lp vault + + associated_token::create(CpiContext::new( + ctx.accounts.raydium_init_pool_static.associated_token_program.to_account_info(), + associated_token::Create { + payer: ctx.accounts.creator.to_account_info(), + mint: ctx.accounts.spot_pool_lp_mint.to_account_info(), + authority: ctx.accounts.sl_pool_signer.to_account_info(), + associated_token: ctx.accounts.sl_pool_spot_lp_vault.to_account_info(), + system_program: ctx.accounts.system_program.to_account_info(), + token_program: ctx.accounts.raydium_init_pool_static.token_program.to_account_info(), + }, + ))?; + + // Transfer LP tokens from pool creator to shared liquidity pool. We can transfer + // the full amount because they should have had 0 before + let creator_lp_account = ctx.accounts.creator_lp_account.to_account_info(); + let creator_lp_account: TokenAccount = + TokenAccount::try_deserialize(&mut &creator_lp_account.data.borrow()[..])?; + let lp_amount = creator_lp_account.amount; + + anchor_spl::token::transfer( + CpiContext::new( + ctx.accounts.raydium_init_pool_static.token_program.to_account_info(), + Transfer { + from: ctx.accounts.creator_lp_account.to_account_info(), + to: ctx.accounts.sl_pool_spot_lp_vault.to_account_info(), + authority: ctx.accounts.creator.to_account_info(), + }, + ), + lp_amount, + )?; + + // Initialize the shared liquidity pool state + ctx.accounts.sl_pool.set_inner(SharedLiquidityPool { + dao: ctx.accounts.dao.key(), + base_mint: ctx.accounts.base_mint.key(), + quote_mint: ctx.accounts.quote_mint.key(), + proposal_stake_rate_threshold_bps: params.proposal_stake_rate_threshold_bps, + is_base_token_0: ctx.accounts.base_mint.key() < ctx.accounts.quote_mint.key(), + sl_pool_signer: ctx.accounts.sl_pool_signer.key(), + sl_pool_signer_bump: ctx.bumps.sl_pool_signer, + sl_pool_base_vault: ctx.accounts.sl_pool_base_vault.key(), + sl_pool_quote_vault: ctx.accounts.sl_pool_quote_vault.key(), + sl_pool_spot_lp_vault: ctx.accounts.sl_pool_spot_lp_vault.key(), + active_spot_pool: ctx.accounts.spot_pool.key(), + active_spot_pool_index: 0, + active_proposal: None, + pda_bump: ctx.bumps.sl_pool, + seq_num: 0, + }); + + let creator_sl_pool_position = &mut ctx.accounts.creator_sl_pool_position; + creator_sl_pool_position.owner = ctx.accounts.creator.key(); + creator_sl_pool_position.pool = ctx.accounts.sl_pool.key(); + creator_sl_pool_position.underlying_spot_lp_shares += lp_amount; + creator_sl_pool_position.bump = ctx.bumps.creator_sl_pool_position; + + + Ok(()) + } +} diff --git a/programs/shared_liquidity_manager/src/instructions/mod.rs b/programs/shared_liquidity_manager/src/instructions/mod.rs new file mode 100644 index 000000000..cd8a03c22 --- /dev/null +++ b/programs/shared_liquidity_manager/src/instructions/mod.rs @@ -0,0 +1,19 @@ +pub mod deposit_shared_liquidity; +pub mod initialize_draft_proposal; +pub mod initialize_proposal_with_liquidity; +pub mod initialize_shared_liquidity_pool; +pub mod remove_proposal_liquidity; +pub mod stake_to_draft_proposal; +pub mod unstake_from_draft_proposal; +pub mod withdraw_shared_liquidity; +pub mod common; + +pub use deposit_shared_liquidity::*; +pub use initialize_draft_proposal::*; +pub use initialize_proposal_with_liquidity::*; +pub use initialize_shared_liquidity_pool::*; +pub use remove_proposal_liquidity::*; +pub use stake_to_draft_proposal::*; +pub use unstake_from_draft_proposal::*; +pub use withdraw_shared_liquidity::*; +pub use common::*; diff --git a/programs/shared_liquidity_manager/src/instructions/remove_proposal_liquidity.rs b/programs/shared_liquidity_manager/src/instructions/remove_proposal_liquidity.rs new file mode 100644 index 000000000..d8003fc57 --- /dev/null +++ b/programs/shared_liquidity_manager/src/instructions/remove_proposal_liquidity.rs @@ -0,0 +1,756 @@ +use anchor_lang::prelude::*; +use anchor_spl::associated_token::get_associated_token_address; +use anchor_spl::token::{Mint, TokenAccount, Token}; + +use anchor_lang::Discriminator; +use raydium_cpmm_cpi::{ + instruction, + states::{OBSERVATION_SEED, POOL_LP_MINT_SEED, POOL_VAULT_SEED}, +}; + +use autocrat::state::ProposalState; + +use crate::instructions::common::*; +use crate::error::SharedLiquidityManagerError; +use crate::state::SharedLiquidityPool; + +#[derive(Accounts)] +pub struct RemoveProposalLiquidityRaydiumAccounts<'info> { + #[account(mut)] + pub active_spot_pool: AccountLoader<'info, raydium_cpmm_cpi::states::PoolState>, + + #[account( + mut, + seeds = [ + POOL_LP_MINT_SEED.as_bytes(), + active_spot_pool.key().as_ref(), + ], + seeds::program = cp_swap_program, + bump, + )] + pub active_spot_pool_lp_mint: Box>, + + #[account( + mut, + seeds = [ + POOL_VAULT_SEED.as_bytes(), + active_spot_pool.key().as_ref(), + base_mint.key().as_ref(), + ], + seeds::program = cp_swap_program, + bump, + )] + pub active_spot_pool_base_vault: Box>, + + #[account( + mut, + seeds = [ + POOL_VAULT_SEED.as_bytes(), + active_spot_pool.key().as_ref(), + quote_mint.key().as_ref(), + ], + seeds::program = cp_swap_program, + bump, + )] + pub active_spot_pool_quote_vault: Box>, + + + pub token_program_2022: Program<'info, anchor_spl::token_interface::Token2022>, + + pub cp_swap_program: Program<'info, raydium_cpmm_cpi::program::RaydiumCpmm>, + + /// CHECK: SPL Memo program + #[account(address = spl_memo::id())] + pub memo_program: UncheckedAccount<'info>, + + /// CHECK: this is the next spot pool, init by cp-swap, + #[account( + mut, + seeds = [ + b"spot_pool", + sl_pool.key().as_ref(), + &(sl_pool.active_spot_pool_index + 1).to_le_bytes() + ], + bump, + )] + pub next_spot_pool: UncheckedAccount<'info>, + + /// CHECK: next spot pool lp mint, init by cp-swap + #[account( + mut, + seeds = [ + POOL_LP_MINT_SEED.as_bytes(), + next_spot_pool.key().as_ref(), + ], + seeds::program = cp_swap_program, + bump, + )] + pub next_spot_pool_lp_mint: UncheckedAccount<'info>, + + + /// CHECK: next spot pool base vault, init by cp-swap + #[account( + mut, + seeds = [ + POOL_VAULT_SEED.as_bytes(), + next_spot_pool.key().as_ref(), + base_mint.key().as_ref(), + ], + seeds::program = cp_swap_program, + bump, + )] + pub next_spot_pool_base_vault: UncheckedAccount<'info>, + + /// CHECK: next spot pool quote vault, init by cp-swap + #[account( + mut, + seeds = [ + POOL_VAULT_SEED.as_bytes(), + next_spot_pool.key().as_ref(), + quote_mint.key().as_ref(), + ], + seeds::program = cp_swap_program, + bump, + )] + pub next_spot_pool_quote_vault: UncheckedAccount<'info>, + + /// CHECK: next spot pool observation state, init by cp-swap + #[account( + mut, + seeds = [ + OBSERVATION_SEED.as_bytes(), + next_spot_pool.key().as_ref(), + ], + seeds::program = cp_swap_program, + bump, + )] + pub next_spot_pool_observation_state: UncheckedAccount<'info>, + + /// CHECK: next spot pool lp vault, init by cp-swap + #[account( + mut, + address = get_associated_token_address(sl_pool_signer.key, next_spot_pool_lp_mint.key) + )] + pub sl_pool_next_spot_lp_vault: UncheckedAccount<'info>, + + #[account(has_one = sl_pool_signer)] + pub sl_pool: Box>, + /// CHECK: This is the shared liquidity pool signer + pub sl_pool_signer: UncheckedAccount<'info>, + pub base_mint: Box>, + pub quote_mint: Box>, +} + +#[derive(Accounts)] +pub struct RemoveProposalLiquidityConditionalVaultAccounts<'info> { + #[account(mut)] + pub question: Account<'info, conditional_vault::state::Question>, + #[account(mut)] + pub base_vault: Account<'info, conditional_vault::state::ConditionalVault>, + #[account(mut)] + pub quote_vault: Account<'info, conditional_vault::state::ConditionalVault>, + #[account(mut, address = base_vault.underlying_token_account)] + pub base_vault_underlying_token_account: Box>, + #[account(mut, address = quote_vault.underlying_token_account)] + pub quote_vault_underlying_token_account: Box>, + pub conditional_vault_program: Program<'info, conditional_vault::program::ConditionalVault>, + #[account(mut)] + pub pass_base_mint: Box>, + #[account(mut)] + pub fail_base_mint: Box>, + #[account(mut)] + pub pass_quote_mint: Box>, + #[account(mut)] + pub fail_quote_mint: Box>, + #[account(mut, associated_token::mint = pass_base_mint, associated_token::authority = sl_pool_signer)] + pub sl_pool_pass_base_vault: Box>, + #[account(mut, associated_token::mint = fail_base_mint, associated_token::authority = sl_pool_signer)] + pub sl_pool_fail_base_vault: Box>, + #[account(mut, associated_token::mint = pass_quote_mint, associated_token::authority = sl_pool_signer)] + pub sl_pool_pass_quote_vault: Box>, + #[account(mut, associated_token::mint = fail_quote_mint, associated_token::authority = sl_pool_signer)] + pub sl_pool_fail_quote_vault: Box>, + /// CHECK: verified by conditional_vault + pub vault_event_authority: UncheckedAccount<'info>, + pub token_program: Program<'info, Token>, + /// CHECK: This is the shared liquidity pool signer + #[account(mut)] + pub sl_pool_signer: UncheckedAccount<'info>, +} + +#[derive(Accounts)] +pub struct RemoveProposalLiquidityAmmAccounts<'info> { + #[account(mut)] + pub pass_amm: Account<'info, amm::state::Amm>, + #[account(mut)] + pub fail_amm: Account<'info, amm::state::Amm>, + #[account(mut)] + pub pass_lp_mint: Box>, + #[account(mut)] + pub fail_lp_mint: Box>, + #[account(mut, associated_token::mint = pass_lp_mint, associated_token::authority = sl_pool_signer)] + pub sl_pool_pass_lp_account: Box>, + #[account(mut, associated_token::mint = fail_lp_mint, associated_token::authority = sl_pool_signer)] + pub sl_pool_fail_lp_account: Box>, + #[account(mut)] + pub pass_amm_vault_ata_base: Box>, + #[account(mut)] + pub pass_amm_vault_ata_quote: Box>, + #[account(mut)] + pub fail_amm_vault_ata_base: Box>, + #[account(mut)] + pub fail_amm_vault_ata_quote: Box>, + pub amm_program: Program<'info, amm::program::Amm>, + /// CHECK: verified by amm + pub event_authority: UncheckedAccount<'info>, + /// CHECK: This is the shared liquidity pool signer + pub sl_pool_signer: UncheckedAccount<'info>, +} + +#[event_cpi] +#[derive(Accounts)] +pub struct RemoveProposalLiquidity<'info> { + // Shared liquidity pool state + #[account(mut, + has_one = sl_pool_base_vault, + has_one = sl_pool_quote_vault, + has_one = sl_pool_spot_lp_vault, + has_one = base_mint, + has_one = quote_mint, + )] + pub sl_pool: Account<'info, SharedLiquidityPool>, + pub proposal: Box>, + + #[account(mut)] + pub sl_pool_base_vault: Box>, + #[account(mut)] + pub sl_pool_quote_vault: Box>, + #[account(mut)] + pub sl_pool_spot_lp_vault: Box>, + + pub base_mint: Box>, + pub quote_mint: Box>, + + pub raydium_init_pool_static: InitializeRaydiumPoolStaticAccounts<'info>, + + pub raydium: RemoveProposalLiquidityRaydiumAccounts<'info>, + + pub conditional_vault: RemoveProposalLiquidityConditionalVaultAccounts<'info>, + + pub ammm: RemoveProposalLiquidityAmmAccounts<'info>, + + pub system_program: Program<'info, System>, + pub token_program: Program<'info, Token>, + + #[account(mut)] + pub payer: Signer<'info>, +} + +impl RemoveProposalLiquidity<'_> { + pub fn validate(&self) -> Result<()> { + match self.proposal.state { + ProposalState::Pending => { + return Err(SharedLiquidityManagerError::ProposalNotFinalized.into()); + } + ProposalState::Passed | ProposalState::Failed | ProposalState::Executed => {} + } + + require_keys_eq!( + self.raydium.active_spot_pool.key(), + self.sl_pool.active_spot_pool + ); + + require_keys_eq!( + self.raydium.sl_pool.key(), + self.sl_pool.key() + ); + + require!( + self.sl_pool.active_proposal.is_some(), + SharedLiquidityManagerError::NoActiveProposal + ); + + require_keys_eq!( + self.proposal.key(), + self.sl_pool.active_proposal.unwrap(), + ); + + + require_keys_eq!( + self.conditional_vault.question.key(), + self.proposal.question + ); + + require_keys_eq!( + self.conditional_vault.base_vault.key(), + self.proposal.base_vault + ); + + require_keys_eq!( + self.conditional_vault.quote_vault.key(), + self.proposal.quote_vault + ); + + require_keys_eq!( + self.conditional_vault.fail_base_mint.key(), + self.conditional_vault.base_vault.conditional_token_mints[0] + ); + + require_keys_eq!( + self.conditional_vault.pass_base_mint.key(), + self.conditional_vault.base_vault.conditional_token_mints[1] + ); + + require_keys_eq!( + self.conditional_vault.fail_quote_mint.key(), + self.conditional_vault.quote_vault.conditional_token_mints[0] + ); + + require_keys_eq!( + self.conditional_vault.pass_quote_mint.key(), + self.conditional_vault.quote_vault.conditional_token_mints[1] + ); + + require_keys_eq!( + self.conditional_vault.sl_pool_signer.key(), + self.sl_pool.sl_pool_signer + ); + + require_keys_eq!( + self.ammm.pass_amm.key(), + self.proposal.pass_amm + ); + + require_keys_eq!( + self.ammm.fail_amm.key(), + self.proposal.fail_amm + ); + + require_keys_eq!( + self.ammm.pass_lp_mint.key(), + self.ammm.pass_amm.lp_mint + ); + + require_keys_eq!( + self.ammm.fail_lp_mint.key(), + self.ammm.fail_amm.lp_mint + ); + + require_keys_eq!( + self.ammm.pass_amm_vault_ata_base.key(), + self.ammm.pass_amm.vault_ata_base + ); + require_keys_eq!( + self.ammm.pass_amm_vault_ata_quote.key(), + self.ammm.pass_amm.vault_ata_quote + ); + + require_keys_eq!( + self.ammm.fail_amm_vault_ata_base.key(), + self.ammm.fail_amm.vault_ata_base + ); + require_keys_eq!( + self.ammm.fail_amm_vault_ata_quote.key(), + self.ammm.fail_amm.vault_ata_quote + ); + + require_keys_eq!( + self.ammm.sl_pool_signer.key(), + self.sl_pool.sl_pool_signer + ); + + require!( + self.conditional_vault.question.is_resolved(), + SharedLiquidityManagerError::ProposalNotFinalized + ); + + require_eq!( + self.sl_pool.active_spot_pool, + self.raydium.active_spot_pool.key() + ); + + Ok(()) + } + + pub fn handle(ctx: Context) -> Result<()> { + let sl_pool_key = ctx.accounts.sl_pool.to_account_info().key; + let seeds = &[ + b"sl_pool_signer".as_ref(), + sl_pool_key.as_ref(), + &[ctx.accounts.sl_pool.sl_pool_signer_bump], + ]; + let signer = &[&seeds[..]]; + + let is_proposal_passed = match ctx.accounts.proposal.state { + ProposalState::Passed | ProposalState::Executed => true, + ProposalState::Failed => false, + ProposalState::Pending => panic!("Proposal is pending"), + }; + + { + let ( + amm, + user_lp_account, + user_base_account, + user_quote_account, + lp_mint, + vault_ata_base, + vault_ata_quote, + ) = if is_proposal_passed { + ( + ctx.accounts.ammm.pass_amm.to_account_info(), + &ctx.accounts.ammm.sl_pool_pass_lp_account, + ctx.accounts.conditional_vault.sl_pool_pass_base_vault.to_account_info(), + ctx.accounts.conditional_vault.sl_pool_pass_quote_vault.to_account_info(), + ctx.accounts.ammm.pass_lp_mint.to_account_info(), + ctx.accounts.ammm.pass_amm_vault_ata_base.to_account_info(), + ctx.accounts + .ammm + .pass_amm_vault_ata_quote + .to_account_info(), + ) + } else { + ( + ctx.accounts.ammm.fail_amm.to_account_info(), + &ctx.accounts.ammm.sl_pool_fail_lp_account, + ctx.accounts.conditional_vault.sl_pool_fail_base_vault.to_account_info(), + ctx.accounts.conditional_vault.sl_pool_fail_quote_vault.to_account_info(), + ctx.accounts.ammm.fail_lp_mint.to_account_info(), + ctx.accounts.ammm.fail_amm_vault_ata_base.to_account_info(), + ctx.accounts + .ammm + .fail_amm_vault_ata_quote + .to_account_info(), + ) + }; + + require!( + user_lp_account.amount > 0, + SharedLiquidityManagerError::NoLpTokensToRemove + ); + + + // Remove liquidity from the winning AMM + amm::cpi::remove_liquidity( + CpiContext::new_with_signer( + ctx.accounts.ammm.amm_program.to_account_info(), + amm::cpi::accounts::AddOrRemoveLiquidity { + amm, + user: ctx.accounts.conditional_vault.sl_pool_signer.to_account_info(), + user_lp_account: user_lp_account.to_account_info(), + user_base_account, + user_quote_account, + vault_ata_base, + vault_ata_quote, + event_authority: ctx.accounts.ammm.event_authority.to_account_info(), + program: ctx.accounts.ammm.amm_program.to_account_info(), + lp_mint, + token_program: ctx.accounts.token_program.to_account_info(), + }, + signer, + ), + amm::instructions::RemoveLiquidityArgs { + lp_tokens_to_burn: user_lp_account.amount, + min_base_amount: 0, + min_quote_amount: 0, + }, + )?; + } + + // Redeem base tokens + conditional_vault::cpi::redeem_tokens( + CpiContext::new_with_signer( + ctx.accounts + .conditional_vault + .conditional_vault_program + .to_account_info(), + conditional_vault::cpi::accounts::InteractWithVault { + question: ctx.accounts.conditional_vault.question.to_account_info(), + vault: ctx.accounts.conditional_vault.base_vault.to_account_info(), + vault_underlying_token_account: ctx + .accounts + .conditional_vault + .base_vault_underlying_token_account + .to_account_info(), + authority: ctx.accounts.conditional_vault.sl_pool_signer.to_account_info(), + user_underlying_token_account: ctx + .accounts + .sl_pool_base_vault + .to_account_info(), + event_authority: ctx.accounts.conditional_vault.vault_event_authority.to_account_info(), + program: ctx + .accounts + .conditional_vault + .conditional_vault_program + .to_account_info(), + token_program: ctx.accounts.token_program.to_account_info(), + }, + signer, + ) + .with_remaining_accounts(vec![ + ctx.accounts.conditional_vault.fail_base_mint.to_account_info(), + ctx.accounts.conditional_vault.pass_base_mint.to_account_info(), + ctx.accounts.conditional_vault.sl_pool_fail_base_vault.to_account_info(), + ctx.accounts.conditional_vault.sl_pool_pass_base_vault.to_account_info(), + ]), + )?; + + let pre_redeem_quote_balance = ctx.accounts.sl_pool_quote_vault.amount; + let pre_redeem_base_balance = ctx.accounts.sl_pool_base_vault.amount; + + anchor_lang::system_program::transfer( + CpiContext::new( + ctx.accounts.system_program.to_account_info(), + anchor_lang::system_program::Transfer { + from: ctx.accounts.payer.to_account_info(), + to: ctx.accounts.conditional_vault.sl_pool_signer.to_account_info(), + }, + ), + // pool fee + 0.1 SOL for rent, we only need 0.05 now but Raydium + // is upgradeable so I'd rather leave buffer + ctx.accounts.raydium_init_pool_static.create_pool_fee.amount + 100_000_000, + )?; + + // Redeem quote tokens + conditional_vault::cpi::redeem_tokens( + CpiContext::new_with_signer( + ctx.accounts + .conditional_vault + .conditional_vault_program + .to_account_info(), + conditional_vault::cpi::accounts::InteractWithVault { + question: ctx.accounts.conditional_vault.question.to_account_info(), + vault: ctx.accounts.conditional_vault.quote_vault.to_account_info(), + vault_underlying_token_account: ctx + .accounts + .conditional_vault + .quote_vault_underlying_token_account + .to_account_info(), + authority: ctx.accounts.conditional_vault.sl_pool_signer.to_account_info(), + user_underlying_token_account: ctx + .accounts + .sl_pool_quote_vault + .to_account_info(), + event_authority: ctx.accounts.conditional_vault.vault_event_authority.to_account_info(), + program: ctx + .accounts + .conditional_vault + .conditional_vault_program + .to_account_info(), + token_program: ctx.accounts.token_program.to_account_info(), + }, + signer, + ) + .with_remaining_accounts(vec![ + ctx.accounts.conditional_vault.fail_quote_mint.to_account_info(), + ctx.accounts.conditional_vault.pass_quote_mint.to_account_info(), + ctx.accounts.conditional_vault.sl_pool_fail_quote_vault.to_account_info(), + ctx.accounts.conditional_vault.sl_pool_pass_quote_vault.to_account_info(), + ]), + )?; + + let ( + vault_0_mint, + vault_1_mint, + token_0_vault, + token_1_vault, + token_0_account, + token_1_account, + ) = if ctx.accounts.sl_pool.is_base_token_0 { + ( + ctx.accounts.base_mint.to_account_info(), + ctx.accounts.quote_mint.to_account_info(), + ctx.accounts + .raydium + .active_spot_pool_base_vault + .to_account_info(), + ctx.accounts + .raydium + .active_spot_pool_quote_vault + .to_account_info(), + ctx.accounts.sl_pool_base_vault.to_account_info(), + ctx.accounts.sl_pool_quote_vault.to_account_info(), + ) + } else { + ( + ctx.accounts.quote_mint.to_account_info(), + ctx.accounts.base_mint.to_account_info(), + ctx.accounts + .raydium + .active_spot_pool_quote_vault + .to_account_info(), + ctx.accounts + .raydium + .active_spot_pool_base_vault + .to_account_info(), + ctx.accounts.sl_pool_quote_vault.to_account_info(), + ctx.accounts.sl_pool_base_vault.to_account_info(), + ) + }; + + raydium_cpmm_cpi::cpi::withdraw( + CpiContext::new_with_signer( + ctx.accounts.raydium.cp_swap_program.to_account_info(), + raydium_cpmm_cpi::cpi::accounts::Withdraw { + owner: ctx.accounts.raydium.sl_pool_signer.to_account_info(), + authority: ctx.accounts.raydium_init_pool_static.raydium_authority.to_account_info(), + pool_state: ctx.accounts.raydium.active_spot_pool.to_account_info(), + lp_mint: ctx.accounts.raydium.active_spot_pool_lp_mint.to_account_info(), + memo_program: ctx.accounts.raydium.memo_program.to_account_info(), + owner_lp_token: ctx.accounts.sl_pool_spot_lp_vault.to_account_info(), + token_0_account, + token_1_account, + vault_0_mint, + vault_1_mint, + token_0_vault, + token_1_vault, + token_program: ctx.accounts.token_program.to_account_info(), + token_program_2022: ctx.accounts.raydium.token_program_2022.to_account_info(), + }, + signer, + ), + ctx.accounts.sl_pool_spot_lp_vault.amount, + 0, + 0, + )?; + ctx.accounts.sl_pool_base_vault.reload()?; + ctx.accounts.sl_pool_quote_vault.reload()?; + + let post_redeem_base_balance = ctx.accounts.sl_pool_base_vault.amount; + let post_redeem_quote_balance = ctx.accounts.sl_pool_quote_vault.amount; + + let base_redeemed = post_redeem_base_balance - pre_redeem_base_balance; + let quote_redeemed = post_redeem_quote_balance - pre_redeem_quote_balance; + + require!( + base_redeemed > 0, + SharedLiquidityManagerError::NoTokensFromAmm + ); + require!( + quote_redeemed > 0, + SharedLiquidityManagerError::NoTokensFromAmm + ); + + let ( + init_amount_0, + init_amount_1, + token_0_mint, + token_1_mint, + creator_token_0, + creator_token_1, + token_0_vault, + token_1_vault, + ) = if ctx.accounts.sl_pool.is_base_token_0 { + ( + base_redeemed, + quote_redeemed, + ctx.accounts.base_mint.to_account_info(), + ctx.accounts.quote_mint.to_account_info(), + ctx.accounts.sl_pool_base_vault.to_account_info(), + ctx.accounts.sl_pool_quote_vault.to_account_info(), + ctx.accounts.raydium.next_spot_pool_base_vault.to_account_info(), + ctx.accounts + .raydium + .next_spot_pool_quote_vault + .to_account_info(), + ) + } else { + ( + quote_redeemed, + base_redeemed, + ctx.accounts.quote_mint.to_account_info(), + ctx.accounts.base_mint.to_account_info(), + ctx.accounts.sl_pool_quote_vault.to_account_info(), + ctx.accounts.sl_pool_base_vault.to_account_info(), + ctx.accounts + .raydium + .next_spot_pool_quote_vault + .to_account_info(), + ctx.accounts.raydium.next_spot_pool_base_vault.to_account_info(), + ) + }; + + let cpi_accounts = raydium_cpmm_cpi::cpi::accounts::Initialize { + creator: ctx.accounts.conditional_vault.sl_pool_signer.to_account_info(), + authority: ctx.accounts.raydium_init_pool_static.raydium_authority.to_account_info(), + pool_state: ctx.accounts.raydium.next_spot_pool.to_account_info(), + amm_config: ctx.accounts.raydium_init_pool_static.amm_config.to_account_info(), + token_0_mint, + token_1_mint, + lp_mint: ctx.accounts.raydium.next_spot_pool_lp_mint.to_account_info(), + creator_token_0, + creator_token_1, + creator_lp_token: ctx + .accounts + .raydium + .sl_pool_next_spot_lp_vault + .to_account_info(), + token_0_program: ctx.accounts.token_program.to_account_info(), + token_1_program: ctx.accounts.token_program.to_account_info(), + token_program: ctx.accounts.token_program.to_account_info(), + observation_state: ctx + .accounts + .raydium + .next_spot_pool_observation_state + .to_account_info(), + create_pool_fee: ctx.accounts.raydium_init_pool_static.create_pool_fee.to_account_info(), + rent: ctx.accounts.raydium_init_pool_static.rent.to_account_info(), + system_program: ctx.accounts.system_program.to_account_info(), + token_0_vault, + token_1_vault, + associated_token_program: ctx.accounts.raydium_init_pool_static.associated_token_program.to_account_info(), + }; + + let ix = instruction::Initialize { + init_amount_0, + init_amount_1, + open_time: 0, + }; + let mut ix_data = Vec::with_capacity(256); + ix_data.extend_from_slice(&instruction::Initialize::discriminator()); + AnchorSerialize::serialize(&ix, &mut ix_data)?; + + let ix = solana_program::instruction::Instruction { + program_id: ctx.accounts.raydium.cp_swap_program.key(), + accounts: cpi_accounts + .to_account_metas(None) + .into_iter() + .zip(cpi_accounts.to_account_infos()) + .map(|mut pair| { + pair.0.is_signer = pair.1.is_signer; + if pair.0.pubkey == ctx.accounts.conditional_vault.sl_pool_signer.key() + || pair.0.pubkey == ctx.accounts.raydium.next_spot_pool.key() + { + pair.0.is_signer = true; + } + pair.0 + }) + .collect(), + data: ix_data, + }; + + let spot_pool_index = 1_u32.to_le_bytes(); + let sl_pool_key = ctx.accounts.sl_pool.key(); + let pool_seeds = &[ + b"spot_pool", + sl_pool_key.as_ref(), + &spot_pool_index[..], + &[ctx.bumps.raydium.next_spot_pool], + ]; + let raydium_signer = &[&pool_seeds[..], &seeds[..]]; + + solana_program::program::invoke_signed( + &ix, + &cpi_accounts.to_account_infos(), + raydium_signer, + )?; + + ctx.accounts.sl_pool.active_spot_pool = ctx.accounts.raydium.next_spot_pool.key(); + ctx.accounts.sl_pool.active_spot_pool_index += 1; + ctx.accounts.sl_pool.sl_pool_spot_lp_vault = ctx.accounts.raydium.sl_pool_next_spot_lp_vault.key(); + ctx.accounts.sl_pool.active_proposal = None; + + + Ok(()) + } +} diff --git a/programs/shared_liquidity_manager/src/instructions/stake_to_draft_proposal.rs b/programs/shared_liquidity_manager/src/instructions/stake_to_draft_proposal.rs new file mode 100644 index 000000000..798043cc3 --- /dev/null +++ b/programs/shared_liquidity_manager/src/instructions/stake_to_draft_proposal.rs @@ -0,0 +1,62 @@ +use anchor_lang::prelude::*; +use anchor_spl::token::{Token, TokenAccount, Transfer}; + +use crate::{ + error::SharedLiquidityManagerError, + state::{DraftProposal, StakeRecord}, +}; + +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct StakeToDraftProposalParams { + pub amount: u64, +} + +#[event_cpi] +#[derive(Accounts)] +pub struct StakeToDraftProposal<'info> { + #[account(mut, has_one = staked_token_vault)] + pub draft_proposal: Account<'info, DraftProposal>, + pub staker: Signer<'info>, + #[account(mut, associated_token::mint = draft_proposal.base_mint, associated_token::authority = staker)] + pub staker_token_account: Account<'info, TokenAccount>, + #[account(mut)] + pub staked_token_vault: Account<'info, TokenAccount>, + #[account(mut)] + pub payer: Signer<'info>, + #[account(init_if_needed, payer = payer, space = 8 + std::mem::size_of::(), seeds = [b"stake_record", draft_proposal.key().as_ref(), staker.key().as_ref()], bump)] + pub stake_record: Account<'info, StakeRecord>, + pub token_program: Program<'info, Token>, + pub system_program: Program<'info, System>, +} + +impl StakeToDraftProposal<'_> { + pub fn validate(&self, params: &StakeToDraftProposalParams) -> Result<()> { + require_gte!( + self.staker_token_account.amount, + params.amount, + SharedLiquidityManagerError::InsufficientFunds + ); + Ok(()) + } + + pub fn handle(ctx: Context, params: StakeToDraftProposalParams) -> Result<()> { + anchor_spl::token::transfer( + CpiContext::new( + ctx.accounts.token_program.to_account_info(), + Transfer { + from: ctx.accounts.staker_token_account.to_account_info(), + to: ctx.accounts.staked_token_vault.to_account_info(), + authority: ctx.accounts.staker.to_account_info(), + }, + ), + params.amount, + )?; + + ctx.accounts.stake_record.staker = ctx.accounts.staker.key(); + ctx.accounts.stake_record.amount += params.amount; + + ctx.accounts.draft_proposal.staked_token_amount += params.amount; + + Ok(()) + } +} diff --git a/programs/shared_liquidity_manager/src/instructions/unstake_from_draft_proposal.rs b/programs/shared_liquidity_manager/src/instructions/unstake_from_draft_proposal.rs new file mode 100644 index 000000000..7a82d0c55 --- /dev/null +++ b/programs/shared_liquidity_manager/src/instructions/unstake_from_draft_proposal.rs @@ -0,0 +1,185 @@ +use anchor_lang::prelude::*; +use anchor_spl::token::{Token, TokenAccount, Transfer}; + +use crate::error::SharedLiquidityManagerError; +use crate::state::{DraftProposal, StakeRecord}; + +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct UnstakeFromDraftProposalParams { + pub amount: u64, +} + +#[event_cpi] +#[derive(Accounts)] +pub struct UnstakeFromDraftProposal<'info> { + #[account(mut, has_one = staked_token_vault)] + pub draft_proposal: Account<'info, DraftProposal>, + pub staker: Signer<'info>, + #[account(mut, associated_token::mint = draft_proposal.base_mint, associated_token::authority = staker)] + pub staker_token_account: Account<'info, TokenAccount>, + #[account(mut)] + pub staked_token_vault: Account<'info, TokenAccount>, + #[account(mut, seeds = [b"stake_record", draft_proposal.key().as_ref(), staker.key().as_ref()], bump)] + pub stake_record: Account<'info, StakeRecord>, + pub token_program: Program<'info, Token>, +} + +impl UnstakeFromDraftProposal<'_> { + pub fn validate(&self, params: &UnstakeFromDraftProposalParams) -> Result<()> { + require_gte!( + self.stake_record.amount, + params.amount, + SharedLiquidityManagerError::InsufficientStake + ); + + Ok(()) + } + + pub fn handle(ctx: Context, params: UnstakeFromDraftProposalParams) -> Result<()> { + // Transfer tokens from staked vault back to staker + // The draft_proposal account itself is the authority for the staked_token_vault + anchor_spl::token::transfer( + CpiContext::new_with_signer( + ctx.accounts.token_program.to_account_info(), + Transfer { + from: ctx.accounts.staked_token_vault.to_account_info(), + to: ctx.accounts.staker_token_account.to_account_info(), + authority: ctx.accounts.draft_proposal.to_account_info(), + }, + &[&[ + b"draft_proposal", + &ctx.accounts.draft_proposal.nonce.to_le_bytes(), + &[ctx.accounts.draft_proposal.pda_bump], + ]], + ), + params.amount, + )?; + + // Update stake record + ctx.accounts.stake_record.amount -= params.amount; + + // Update draft proposal staked amount + ctx.accounts.draft_proposal.staked_token_amount -= params.amount; + + Ok(()) + } +} + +#[cfg(test)] +mod unstake_tests { + use super::*; + use crate::state::{DraftProposal, DraftProposalStatus, StakeRecord}; + + fn create_mock_stake_record(amount: u64) -> StakeRecord { + StakeRecord { + staker: Pubkey::default(), + amount, + } + } + + fn create_mock_draft_proposal(staked_amount: u64) -> DraftProposal { + DraftProposal { + shared_liquidity_pool: Pubkey::default(), + base_mint: Pubkey::default(), + instruction: crate::state::ProposalInstruction { + program_id: Pubkey::default(), + accounts: vec![], + data: vec![], + }, + status: DraftProposalStatus::Draft, + staked_token_amount: staked_amount, + staked_token_vault: Pubkey::default(), + nonce: 0, + pda_bump: 0, + } + } + + #[test] + pub fn test_validate_sufficient_stake() { + let stake_record = create_mock_stake_record(1000); + let draft_proposal = create_mock_draft_proposal(1000); + + let mock_ctx = MockUnstakeContext { + stake_record, + draft_proposal, + }; + + let params = UnstakeFromDraftProposalParams { amount: 500 }; + let result = mock_ctx.validate(¶ms); + + assert!(result.is_ok()); + } + + #[test] + pub fn test_validate_exact_stake_amount() { + let stake_record = create_mock_stake_record(1000); + let draft_proposal = create_mock_draft_proposal(1000); + + let mock_ctx = MockUnstakeContext { + stake_record, + draft_proposal, + }; + + let params = UnstakeFromDraftProposalParams { amount: 1000 }; + let result = mock_ctx.validate(¶ms); + + assert!(result.is_ok()); + } + + #[test] + pub fn test_validate_insufficient_stake() { + let stake_record = create_mock_stake_record(500); + let draft_proposal = create_mock_draft_proposal(500); + + let mock_ctx = MockUnstakeContext { + stake_record, + draft_proposal, + }; + + let params = UnstakeFromDraftProposalParams { amount: 1000 }; + let result = mock_ctx.validate(¶ms); + + assert!(result.is_err()); + let error = result.unwrap_err(); + match error { + anchor_lang::error::Error::AnchorError(anchor_error) => { + assert_eq!(anchor_error.error_code_number, 6000); // InsufficientStake error code + assert_eq!(anchor_error.error_name, "InsufficientStake"); + } + _ => panic!("Expected AnchorError"), + } + } + + #[test] + pub fn test_validate_zero_unstake_amount() { + let stake_record = create_mock_stake_record(1000); + let draft_proposal = create_mock_draft_proposal(1000); + + let mock_ctx = MockUnstakeContext { + stake_record, + draft_proposal, + }; + + let params = UnstakeFromDraftProposalParams { amount: 0 }; + let result = mock_ctx.validate(¶ms); + + assert!(result.is_ok()); + } + + // Mock context struct for testing validation logic + struct MockUnstakeContext { + stake_record: StakeRecord, + draft_proposal: DraftProposal, + } + + impl MockUnstakeContext { + fn validate(&self, params: &UnstakeFromDraftProposalParams) -> Result<()> { + require_gte!( + self.stake_record.amount, + params.amount, + SharedLiquidityManagerError::InsufficientStake + ); + Ok(()) + } + } +} diff --git a/programs/shared_liquidity_manager/src/instructions/withdraw_shared_liquidity.rs b/programs/shared_liquidity_manager/src/instructions/withdraw_shared_liquidity.rs new file mode 100644 index 000000000..47f145f1a --- /dev/null +++ b/programs/shared_liquidity_manager/src/instructions/withdraw_shared_liquidity.rs @@ -0,0 +1,465 @@ +use anchor_lang::prelude::*; +use anchor_spl::{ + token::{Mint, Token, TokenAccount}, + token_interface::Token2022, +}; + +use crate::error::SharedLiquidityManagerError; +use crate::state::{LiquidityPosition, SharedLiquidityPool}; +use raydium_cpmm_cpi::cpi::accounts::Withdraw as RaydiumWithdraw; +use raydium_cpmm_cpi::states::PoolState as RaydiumPoolState; + +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct WithdrawSharedLiquidityParams { + /// The amount of LP tokens to withdraw + pub lp_token_amount: u64, + /// The minimum amount of token0 to receive + pub minimum_token_0_amount: u64, + /// The minimum amount of token1 to receive + pub minimum_token_1_amount: u64, +} + +#[event_cpi] +#[derive(Accounts)] +pub struct WithdrawSharedLiquidity<'info> { + #[account( + mut, + has_one = active_spot_pool, + has_one = sl_pool_spot_lp_vault, + has_one = base_mint, + has_one = quote_mint, + has_one = sl_pool_signer, + )] + pub sl_pool: Account<'info, SharedLiquidityPool>, + + /// CHECK: sl_pool_signer is a PDA + pub sl_pool_signer: UncheckedAccount<'info>, + + #[account(mut)] + pub active_spot_pool: AccountLoader<'info, RaydiumPoolState>, + + #[account(mut)] + pub sl_pool_spot_lp_vault: Box>, + + #[account( + mut, + token::mint = sl_pool.quote_mint, + token::authority = user, + )] + pub user_quote_token_account: Box>, + + #[account( + mut, + token::mint = sl_pool.base_mint, + token::authority = user, + )] + pub user_base_token_account: Box>, + + #[account(mut)] + pub spot_pool_base_vault: Box>, + #[account(mut)] + pub spot_pool_quote_vault: Box>, + + pub base_mint: Box>, + pub quote_mint: Box>, + + #[account(mut)] + pub spot_pool_lp_mint: Box>, + + #[account( + mut, + associated_token::mint = spot_pool_lp_mint, + associated_token::authority = user, + )] + pub user_lp_token_account: Box>, + + #[account( + mut, + seeds = [b"sl_pool_position", sl_pool.key().as_ref(), user.key().as_ref()], + bump, + )] + pub user_sl_pool_position: Account<'info, LiquidityPosition>, + + #[account(mut)] + pub user: Signer<'info>, + + /// CHECK: Receives SOL when position is closed + pub fee_receiver: UncheckedAccount<'info>, + + /// CHECK: pool vault and lp mint authority + #[account( + seeds = [ + raydium_cpmm_cpi::AUTH_SEED.as_bytes(), + ], + seeds::program = cp_swap_program, + bump, + )] + pub raydium_authority: UncheckedAccount<'info>, + pub token_program: Program<'info, Token>, + pub token_program_2022: Program<'info, Token2022>, + pub cp_swap_program: Program<'info, raydium_cpmm_cpi::program::RaydiumCpmm>, + /// CHECK: SPL Memo program + #[account(address = spl_memo::id())] + pub memo_program: UncheckedAccount<'info>, +} + +impl WithdrawSharedLiquidity<'_> { + pub fn validate(&self, params: &WithdrawSharedLiquidityParams) -> Result<()> { + // Ensure the pool is not being used by an active proposal + require!( + self.sl_pool.active_proposal.is_none(), + SharedLiquidityManagerError::PoolInUse + ); + + require!( + self.user_sl_pool_position.underlying_spot_lp_shares >= params.lp_token_amount, + SharedLiquidityManagerError::InsufficientLpShares + ); + + // Neither of these should get triggered because of the PDA derivation, but we'll keep them here for safety + require_eq!( + self.user_sl_pool_position.owner, + self.user.key(), + ); + require_eq!( + self.user_sl_pool_position.pool, + self.sl_pool.key(), + ); + + Ok(()) + } + + pub fn handle(ctx: Context, params: WithdrawSharedLiquidityParams) -> Result<()> { + // Get initial token balances to calculate how much was withdrawn + let initial_base_balance = ctx.accounts.user_base_token_account.amount; + let initial_quote_balance = ctx.accounts.user_quote_token_account.amount; + + let ( + token_0_account, + token_1_account, + vault_0_mint, + vault_1_mint, + token_0_vault, + token_1_vault, + ) = if ctx.accounts.sl_pool.is_base_token_0 { + ( + ctx.accounts.user_base_token_account.to_account_info(), + ctx.accounts.user_quote_token_account.to_account_info(), + ctx.accounts.base_mint.to_account_info(), + ctx.accounts.quote_mint.to_account_info(), + ctx.accounts.spot_pool_base_vault.to_account_info(), + ctx.accounts.spot_pool_quote_vault.to_account_info(), + ) + } else { + ( + ctx.accounts.user_quote_token_account.to_account_info(), + ctx.accounts.user_base_token_account.to_account_info(), + ctx.accounts.quote_mint.to_account_info(), + ctx.accounts.base_mint.to_account_info(), + ctx.accounts.spot_pool_quote_vault.to_account_info(), + ctx.accounts.spot_pool_base_vault.to_account_info(), + ) + }; + + // Generate PDA seeds for signing + let sl_pool_key = ctx.accounts.sl_pool.key(); + let seeds = &[ + b"sl_pool_signer".as_ref(), + sl_pool_key.as_ref(), + &[ctx.accounts.sl_pool.sl_pool_signer_bump], + ]; + let signer = &[&seeds[..]]; + + // Withdraw from Raydium + raydium_cpmm_cpi::cpi::withdraw( + CpiContext::new_with_signer( + ctx.accounts.cp_swap_program.to_account_info(), + RaydiumWithdraw { + owner: ctx.accounts.sl_pool_signer.to_account_info(), + authority: ctx.accounts.raydium_authority.to_account_info(), + pool_state: ctx.accounts.active_spot_pool.to_account_info(), + lp_mint: ctx.accounts.spot_pool_lp_mint.to_account_info(), + memo_program: ctx.accounts.memo_program.to_account_info(), + owner_lp_token: ctx.accounts.sl_pool_spot_lp_vault.to_account_info(), + token_0_account, + token_1_account, + vault_0_mint, + vault_1_mint, + token_0_vault, + token_1_vault, + token_program: ctx.accounts.token_program.to_account_info(), + token_program_2022: ctx.accounts.token_program_2022.to_account_info(), + }, + signer, + ), + params.lp_token_amount, + params.minimum_token_0_amount, + params.minimum_token_1_amount, + )?; + + // Reload accounts to get updated balances + ctx.accounts.user_base_token_account.reload()?; + ctx.accounts.user_quote_token_account.reload()?; + + // Calculate how many tokens the user received + let base_received = ctx.accounts.user_base_token_account.amount - initial_base_balance; + let quote_received = ctx.accounts.user_quote_token_account.amount - initial_quote_balance; + + // Verify minimum amounts were received + require!( + base_received >= params.minimum_token_0_amount + || base_received >= params.minimum_token_1_amount, + SharedLiquidityManagerError::SlippageExceeded + ); + require!( + quote_received >= params.minimum_token_0_amount + || quote_received >= params.minimum_token_1_amount, + SharedLiquidityManagerError::SlippageExceeded + ); + + // Update the user's position + ctx.accounts.user_sl_pool_position.underlying_spot_lp_shares -= params.lp_token_amount; + + // If user has no more shares, close the position and send SOL to fee_receiver + if ctx.accounts.user_sl_pool_position.underlying_spot_lp_shares == 0 { + ctx.accounts + .user_sl_pool_position + .close(ctx.accounts.fee_receiver.to_account_info())?; + } + + Ok(()) + } +} + +#[cfg(test)] +mod withdraw_tests { + use super::*; + use crate::state::{LiquidityPosition, SharedLiquidityPool}; + + fn create_mock_sl_pool(active_proposal: Option) -> SharedLiquidityPool { + SharedLiquidityPool { + pda_bump: 0, + dao: Pubkey::default(), + base_mint: Pubkey::default(), + quote_mint: Pubkey::default(), + sl_pool_signer: Pubkey::default(), + sl_pool_signer_bump: 0, + sl_pool_base_vault: Pubkey::default(), + sl_pool_quote_vault: Pubkey::default(), + sl_pool_spot_lp_vault: Pubkey::default(), + active_proposal, + proposal_stake_rate_threshold_bps: 1000, + seq_num: 0, + active_spot_pool: Pubkey::default(), + active_spot_pool_index: 0, + is_base_token_0: true, + } + } + + fn create_mock_position(owner: Pubkey, pool: Pubkey, shares: u64) -> LiquidityPosition { + LiquidityPosition { + owner, + pool, + underlying_spot_lp_shares: shares, + bump: 0, + } + } + + #[test] + pub fn test_validate_pool_not_in_use() { + let sl_pool = create_mock_sl_pool(None); + let user = Pubkey::default(); + let position = create_mock_position(user, Pubkey::default(), 1000); + + let mock_ctx = MockWithdrawContext { + sl_pool, + position, + user, + }; + + let params = WithdrawSharedLiquidityParams { + lp_token_amount: 500, + minimum_token_0_amount: 100, + minimum_token_1_amount: 100, + }; + + let result = mock_ctx.validate(¶ms); + assert!(result.is_ok()); + } + + #[test] + pub fn test_validate_pool_in_use() { + let sl_pool = create_mock_sl_pool(Some(Pubkey::new_unique())); + let user = Pubkey::default(); + let position = create_mock_position(user, Pubkey::default(), 1000); + + let mock_ctx = MockWithdrawContext { + sl_pool, + position, + user, + }; + + let params = WithdrawSharedLiquidityParams { + lp_token_amount: 500, + minimum_token_0_amount: 100, + minimum_token_1_amount: 100, + }; + + let result = mock_ctx.validate(¶ms); + assert!(result.is_err()); + let error = result.unwrap_err(); + match error { + anchor_lang::error::Error::AnchorError(anchor_error) => { + assert_eq!(anchor_error.error_code_number, 6005); // PoolInUse error code + assert_eq!(anchor_error.error_name, "PoolInUse"); + } + _ => panic!("Expected AnchorError"), + } + } + + #[test] + pub fn test_validate_unauthorized_user() { + let sl_pool = create_mock_sl_pool(None); + let user = Pubkey::new_unique(); + let different_user = Pubkey::new_unique(); + let position = create_mock_position(different_user, Pubkey::default(), 1000); + + let mock_ctx = MockWithdrawContext { + sl_pool, + position, + user, + }; + + let params = WithdrawSharedLiquidityParams { + lp_token_amount: 500, + minimum_token_0_amount: 100, + minimum_token_1_amount: 100, + }; + + let result = mock_ctx.validate(¶ms); + assert!(result.is_err()); + let error = result.unwrap_err(); + match error { + anchor_lang::error::Error::AnchorError(anchor_error) => { + assert_eq!(anchor_error.error_code_number, 6007); // Unauthorized error code + assert_eq!(anchor_error.error_name, "Unauthorized"); + } + _ => panic!("Expected AnchorError"), + } + } + + #[test] + pub fn test_validate_invalid_pool() { + let sl_pool = create_mock_sl_pool(None); + let user = Pubkey::default(); + let different_pool = Pubkey::new_unique(); + let position = create_mock_position(user, different_pool, 1000); + + let mock_ctx = MockWithdrawContext { + sl_pool, + position, + user, + }; + + let params = WithdrawSharedLiquidityParams { + lp_token_amount: 500, + minimum_token_0_amount: 100, + minimum_token_1_amount: 100, + }; + + let result = mock_ctx.validate(¶ms); + assert!(result.is_err()); + let error = result.unwrap_err(); + match error { + anchor_lang::error::Error::AnchorError(anchor_error) => { + assert_eq!(anchor_error.error_code_number, 6008); // InvalidPool error code + assert_eq!(anchor_error.error_name, "InvalidPool"); + } + _ => panic!("Expected AnchorError"), + } + } + + #[test] + pub fn test_validate_insufficient_lp_shares() { + let sl_pool = create_mock_sl_pool(None); + let user = Pubkey::default(); + let position = create_mock_position(user, Pubkey::default(), 200); + + let mock_ctx = MockWithdrawContext { + sl_pool, + position, + user, + }; + + let params = WithdrawSharedLiquidityParams { + lp_token_amount: 500, + minimum_token_0_amount: 100, + minimum_token_1_amount: 100, + }; + + let result = mock_ctx.validate(¶ms); + assert!(result.is_err()); + let error = result.unwrap_err(); + match error { + anchor_lang::error::Error::AnchorError(anchor_error) => { + assert_eq!(anchor_error.error_code_number, 6006); // InsufficientLpShares error code + assert_eq!(anchor_error.error_name, "InsufficientLpShares"); + } + _ => panic!("Expected AnchorError"), + } + } + + #[test] + pub fn test_validate_exact_lp_shares() { + let sl_pool = create_mock_sl_pool(None); + let user = Pubkey::default(); + let position = create_mock_position(user, Pubkey::default(), 500); + + let mock_ctx = MockWithdrawContext { + sl_pool, + position, + user, + }; + + let params = WithdrawSharedLiquidityParams { + lp_token_amount: 500, + minimum_token_0_amount: 100, + minimum_token_1_amount: 100, + }; + + let result = mock_ctx.validate(¶ms); + assert!(result.is_ok()); + } + + // Mock context struct for testing validation logic + struct MockWithdrawContext { + sl_pool: SharedLiquidityPool, + position: LiquidityPosition, + user: Pubkey, + } + + impl MockWithdrawContext { + fn validate(&self, params: &WithdrawSharedLiquidityParams) -> Result<()> { + require!( + self.sl_pool.active_proposal.is_none(), + SharedLiquidityManagerError::PoolInUse + ); + + require!( + self.position.owner == self.user, + SharedLiquidityManagerError::Unauthorized + ); + require!( + self.position.pool == Pubkey::default(), // Mock pool key + SharedLiquidityManagerError::InvalidPool + ); + + require!( + self.position.underlying_spot_lp_shares >= params.lp_token_amount, + SharedLiquidityManagerError::InsufficientLpShares + ); + + Ok(()) + } + } +} diff --git a/programs/shared_liquidity_manager/src/lib.rs b/programs/shared_liquidity_manager/src/lib.rs new file mode 100644 index 000000000..5eb7e61fd --- /dev/null +++ b/programs/shared_liquidity_manager/src/lib.rs @@ -0,0 +1,95 @@ +//! Enables LPs to provide liquidity that is by default stored in a Raydium +//! constant-product pool, but that can be rented for the purpose of decision +//! markets. +//! +//! How it works: +//! - A DAO creates a shared liquidity pool with some protocol-owned-liquidity and +//! sets the % of the token supply that needs to be staked on a proposal for it +//! to go to a DAO proposal. By default, all the liquidity is in a Raydium spot pool. +//! - Anyone can create draft proposals. +//! - Anyone can stake/unstake their DAO tokens on draft proposals. +//! - When a proposal receives enough staked DAO tokens, anyone can call +//! `initialize_proposal_with_liquidity` to initialize the proposal with the +//! shared liquidity pool. While this proposal is active, noone else can initialize +//! proposals through this shared liquidity pool. +//! - When a proposal is finalized, anyone can call `remove_proposal_liquidity` to +//! remove the liquidity from both the proposal and the current Raydium pool and +//! provide it all to a new Raydium spot pool. +use anchor_lang::prelude::*; + +declare_id!("EoJc1PYxZbnCjszampLcwJGYcB5Md47jM4oSQacRtD4d"); + +mod error; +mod instructions; +mod state; + +use instructions::*; + +/// TODO: +/// - add unstake +/// - add unit tests + +#[program] +pub mod shared_liquidity_manager { + use super::*; + + #[access_control(ctx.accounts.validate(¶ms))] + pub fn initialize_shared_liquidity_pool( + ctx: Context, + params: InitializeSharedLiquidityPoolParams, + ) -> Result<()> { + InitializeSharedLiquidityPool::handle(ctx, params) + } + + pub fn initialize_draft_proposal( + ctx: Context, + params: InitializeDraftProposalParams, + ) -> Result<()> { + InitializeDraftProposal::handle(ctx, params) + } + + #[access_control(ctx.accounts.validate(¶ms))] + pub fn stake_to_draft_proposal( + ctx: Context, + params: StakeToDraftProposalParams, + ) -> Result<()> { + StakeToDraftProposal::handle(ctx, params) + } + + #[access_control(ctx.accounts.validate(¶ms))] + pub fn unstake_from_draft_proposal( + ctx: Context, + params: UnstakeFromDraftProposalParams, + ) -> Result<()> { + UnstakeFromDraftProposal::handle(ctx, params) + } + + #[access_control(ctx.accounts.validate(¶ms))] + pub fn deposit_shared_liquidity( + ctx: Context, + params: DepositSharedLiquidityParams, + ) -> Result<()> { + DepositSharedLiquidity::handle(ctx, params) + } + + #[access_control(ctx.accounts.validate(¶ms))] + pub fn withdraw_shared_liquidity( + ctx: Context, + params: WithdrawSharedLiquidityParams, + ) -> Result<()> { + WithdrawSharedLiquidity::handle(ctx, params) + } + + #[access_control(ctx.accounts.validate())] + pub fn initialize_proposal_with_liquidity( + ctx: Context, + params: InitializeProposalWithLiquidityParams, + ) -> Result<()> { + InitializeProposalWithLiquidity::handle(ctx, params) + } + + #[access_control(ctx.accounts.validate())] + pub fn remove_proposal_liquidity(ctx: Context) -> Result<()> { + RemoveProposalLiquidity::handle(ctx) + } +} diff --git a/programs/shared_liquidity_manager/src/state/draft_proposal.rs b/programs/shared_liquidity_manager/src/state/draft_proposal.rs new file mode 100644 index 000000000..135bc9242 --- /dev/null +++ b/programs/shared_liquidity_manager/src/state/draft_proposal.rs @@ -0,0 +1,140 @@ +use anchor_lang::prelude::*; + +#[derive(Clone, AnchorSerialize, AnchorDeserialize, Debug, PartialEq, Eq)] +pub struct ProposalAccount { + pub pubkey: Pubkey, + pub is_signer: bool, + pub is_writable: bool, +} + +#[derive(Clone, AnchorSerialize, AnchorDeserialize, Debug, PartialEq, Eq)] +pub struct ProposalInstruction { + pub program_id: Pubkey, + pub accounts: Vec, + pub data: Vec, +} + +impl From for autocrat::ProposalInstruction { + fn from(instruction: ProposalInstruction) -> Self { + Self { + program_id: instruction.program_id, + accounts: instruction + .accounts + .into_iter() + .map(|acc| autocrat::ProposalAccount { + pubkey: acc.pubkey, + is_signer: acc.is_signer, + is_writable: acc.is_writable, + }) + .collect(), + data: instruction.data, + } + } +} + +#[derive(AnchorSerialize, AnchorDeserialize, Debug, PartialEq, Eq, Clone, Copy)] +pub enum DraftProposalStatus { + Draft, + Initialized, +} + +impl std::fmt::Display for DraftProposalStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +#[account] +pub struct DraftProposal { + pub shared_liquidity_pool: Pubkey, + pub base_mint: Pubkey, + pub instruction: ProposalInstruction, + pub status: DraftProposalStatus, + /// The amount of tokens that have been staked on this draft proposal + pub staked_token_amount: u64, + /// The vault that holds the staked tokens + pub staked_token_vault: Pubkey, + /// The nonce used to create this draft proposal PDA + pub nonce: u64, + pub pda_bump: u8, +} + +#[cfg(test)] +mod draft_proposal_tests { + use super::*; + + #[test] + pub fn test_draft_proposal_status_display() { + assert_eq!(DraftProposalStatus::Draft.to_string(), "Draft"); + assert_eq!(DraftProposalStatus::Initialized.to_string(), "Initialized"); + } + + #[test] + pub fn test_draft_proposal_status_equality() { + assert_eq!(DraftProposalStatus::Draft, DraftProposalStatus::Draft); + assert_eq!( + DraftProposalStatus::Initialized, + DraftProposalStatus::Initialized + ); + assert_ne!(DraftProposalStatus::Draft, DraftProposalStatus::Initialized); + } + + #[test] + pub fn test_proposal_instruction_conversion() { + let proposal_instruction = ProposalInstruction { + program_id: Pubkey::default(), + accounts: vec![ + ProposalAccount { + pubkey: Pubkey::default(), + is_signer: true, + is_writable: false, + }, + ProposalAccount { + pubkey: Pubkey::default(), + is_signer: false, + is_writable: true, + }, + ], + data: vec![1, 2, 3, 4], + }; + + let autocrat_instruction: autocrat::ProposalInstruction = + proposal_instruction.clone().into(); + + assert_eq!( + autocrat_instruction.program_id, + proposal_instruction.program_id + ); + assert_eq!( + autocrat_instruction.accounts.len(), + proposal_instruction.accounts.len() + ); + assert_eq!(autocrat_instruction.data, proposal_instruction.data); + assert_eq!(autocrat_instruction.accounts[0].is_signer, true); + assert_eq!(autocrat_instruction.accounts[0].is_writable, false); + assert_eq!(autocrat_instruction.accounts[1].is_signer, false); + assert_eq!(autocrat_instruction.accounts[1].is_writable, true); + } + + #[test] + pub fn test_proposal_account_equality() { + let account1 = ProposalAccount { + pubkey: Pubkey::default(), + is_signer: true, + is_writable: false, + }; + let account2 = ProposalAccount { + pubkey: Pubkey::default(), + is_signer: true, + is_writable: false, + }; + let account3 = ProposalAccount { + pubkey: Pubkey::default(), + is_signer: false, + is_writable: false, + }; + + assert_eq!(account1, account2); + assert_ne!(account1, account3); + } +} diff --git a/programs/shared_liquidity_manager/src/state/liquidity_position.rs b/programs/shared_liquidity_manager/src/state/liquidity_position.rs new file mode 100644 index 000000000..7fc1ec739 --- /dev/null +++ b/programs/shared_liquidity_manager/src/state/liquidity_position.rs @@ -0,0 +1,13 @@ +use anchor_lang::prelude::*; + +#[account] +pub struct LiquidityPosition { + /// The owner of this position + pub owner: Pubkey, + /// The shared liquidity pool this position belongs to + pub pool: Pubkey, + /// The amount of underlying spot LP shares this position represents + pub underlying_spot_lp_shares: u64, + /// The PDA bump + pub bump: u8, +} diff --git a/programs/shared_liquidity_manager/src/state/mod.rs b/programs/shared_liquidity_manager/src/state/mod.rs new file mode 100644 index 000000000..8042a071b --- /dev/null +++ b/programs/shared_liquidity_manager/src/state/mod.rs @@ -0,0 +1,9 @@ +pub mod draft_proposal; +pub mod liquidity_position; +pub mod shared_liquidity_pool; +pub mod stake_record; + +pub use draft_proposal::*; +pub use liquidity_position::*; +pub use shared_liquidity_pool::*; +pub use stake_record::*; diff --git a/programs/shared_liquidity_manager/src/state/shared_liquidity_pool.rs b/programs/shared_liquidity_manager/src/state/shared_liquidity_pool.rs new file mode 100644 index 000000000..d0d1294bf --- /dev/null +++ b/programs/shared_liquidity_manager/src/state/shared_liquidity_pool.rs @@ -0,0 +1,37 @@ +use anchor_lang::prelude::*; + +#[account] +#[derive(Debug)] +pub struct SharedLiquidityPool { + /// The PDA bump. + pub pda_bump: u8, + /// The DAO. + pub dao: Pubkey, + /// The base mint. + pub base_mint: Pubkey, + /// The quote mint. + pub quote_mint: Pubkey, + /// The signer of this pool, used because Raydium pools need a SOL payer and this PDA can't hold SOL. + pub sl_pool_signer: Pubkey, + /// The pda bump of the signer. + pub sl_pool_signer_bump: u8, + /// Holds the base tokens for the shared liquidity pool when it's moving liquidity around. + pub sl_pool_base_vault: Pubkey, + /// Holds the quote tokens for the shared liquidity pool when it's moving liquidity around. + pub sl_pool_quote_vault: Pubkey, + /// Holds the LP tokens for the shared liquidity pool. + pub sl_pool_spot_lp_vault: Pubkey, + /// The proposal that's using liquidity from this pool. + pub active_proposal: Option, + /// The percentage of a token's supply, in basis points, that needs to be + /// staked to a draft proposal before it can be initialized. + pub proposal_stake_rate_threshold_bps: u16, + /// The sequence number of this shared liquidity pool. Useful for sorting events. + pub seq_num: u64, + /// The current Raydium spot pool. Changes when a proposal is removed. + pub active_spot_pool: Pubkey, + /// The index of the current Raydium spot pool. Starts at 0 and increments by 1 for each new spot pool. + pub active_spot_pool_index: u32, + /// Whether the base token is token0 in the current Raydium spot pool (otherwise it's token1). + pub is_base_token_0: bool, +} diff --git a/programs/shared_liquidity_manager/src/state/stake_record.rs b/programs/shared_liquidity_manager/src/state/stake_record.rs new file mode 100644 index 000000000..cd4a93145 --- /dev/null +++ b/programs/shared_liquidity_manager/src/state/stake_record.rs @@ -0,0 +1,7 @@ +use anchor_lang::prelude::*; + +#[account] +pub struct StakeRecord { + pub staker: Pubkey, + pub amount: u64, +} diff --git a/run.sh b/run.sh index 79f326b53..467ef727a 100755 --- a/run.sh +++ b/run.sh @@ -20,6 +20,14 @@ build_launchpad() { find programs | entr -sc 'anchor build -p launchpad' } +build_shared_liquidity_manager() { + find programs | entr -sc 'anchor build -p shared_liquidity_manager' +} + +test_shared_liquidity_manager_logs() { + find programs tests sdk | entr -sc 'anchor build -p shared_liquidity_manager && (cd sdk && yarn build) && anchor test --skip-build' +} + test_vault() { # anchor doesn't let you past test files, so we do this weird thing where we # modify the Anchor.toml and then put it back @@ -151,6 +159,8 @@ case "$1" in test_logs_no_build) test_logs_no_build ;; vault) test_vault ;; build_vault) build_vault ;; + build_shared_liquidity_manager) build_shared_liquidity_manager ;; + test_shared_liquidity_manager_logs) test_shared_liquidity_manager_logs ;; test_no_build) test_no_build ;; build_verifiable) build_verifiable "$2" ;; deploy) deploy "$2" "$3" ;; diff --git a/scripts/addV04Metadata.ts b/scripts/addV04Metadata.ts index b1c4f59e2..9d30e4ecb 100644 --- a/scripts/addV04Metadata.ts +++ b/scripts/addV04Metadata.ts @@ -48,7 +48,6 @@ async function main() { ) .transaction(); - const basePass = await vaultProgram .addMetadataToConditionalTokensIx( baseVault, @@ -59,9 +58,18 @@ async function main() { ) .transaction(); - const tx = new Transaction().add(ComputeBudgetProgram.setComputeUnitLimit({ units: 300_000 }), ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 100 }), quoteFail, quotePass, baseFail, basePass); + const tx = new Transaction().add( + ComputeBudgetProgram.setComputeUnitLimit({ units: 300_000 }), + ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 100 }), + quoteFail, + quotePass, + baseFail, + basePass + ); - const sig = await provider.sendAndConfirm(tx, undefined, { commitment: "confirmed" }); + const sig = await provider.sendAndConfirm(tx, undefined, { + commitment: "confirmed", + }); console.log(sig); } diff --git a/scripts/createV04Proposal.ts b/scripts/createV04Proposal.ts index d3f963b7d..94002736c 100644 --- a/scripts/createV04Proposal.ts +++ b/scripts/createV04Proposal.ts @@ -16,7 +16,7 @@ async function main() { // Use the existing DAO address const dao = new PublicKey("Hv7b7Kw2Xy7fGZZ8qWiciwfivay2hARmY7qC9HH4qWuS"); - + // Get the DAO's data const storedDao = await autocratProgram.getDao(dao); console.log("DAO Token Mint:", storedDao.tokenMint.toString()); @@ -49,7 +49,10 @@ async function main() { const requiredMeta = PriceMath.getChainAmount(10, 9); // 10 META for more liquidity const requiredUsdc = PriceMath.getChainAmount(10000, 6); // 10000 USDC for more liquidity - if (metaBalance < BigInt(requiredMeta.toString()) || usdcBalance < BigInt(requiredUsdc.toString())) { + if ( + metaBalance < BigInt(requiredMeta.toString()) || + usdcBalance < BigInt(requiredUsdc.toString()) + ) { console.log("Insufficient balance for proposal creation"); console.log("Required META:", requiredMeta.toString()); console.log("Required USDC:", requiredUsdc.toString()); diff --git a/scripts/initializeLaunch.ts b/scripts/initializeLaunch.ts index 34252cccb..d788e4658 100644 --- a/scripts/initializeLaunch.ts +++ b/scripts/initializeLaunch.ts @@ -15,7 +15,13 @@ import { createUmi } from "@metaplex-foundation/umi-bundle-defaults"; import { mplTokenMetadata } from "@metaplex-foundation/mpl-token-metadata"; import { walletAdapterIdentity } from "@metaplex-foundation/umi-signer-wallet-adapters"; import { fromWeb3JsPublicKey } from "@metaplex-foundation/umi-web3js-adapters"; -import { ComputeBudgetProgram, Keypair, PublicKey, SystemProgram, Transaction } from "@solana/web3.js"; +import { + ComputeBudgetProgram, + Keypair, + PublicKey, + SystemProgram, + Transaction, +} from "@solana/web3.js"; import * as fs from "fs"; // Use the RPC endpoint of your choice. @@ -35,21 +41,26 @@ const ONE_MINUTE_IN_SECONDS = 60; const ONE_HOUR_IN_SECONDS = ONE_MINUTE_IN_SECONDS * 60; const ONE_DAY_IN_SECONDS = ONE_HOUR_IN_SECONDS * 24; const SEVEN_DAYS_IN_SECONDS = ONE_DAY_IN_SECONDS * 7; -const KOLLAN_PUBKEY = new PublicKey("CRANkLNAUCPFapK5zpc1BvXA1WjfZpo6wEmssyECxuxf"); +const KOLLAN_PUBKEY = new PublicKey( + "CRANkLNAUCPFapK5zpc1BvXA1WjfZpo6wEmssyECxuxf" +); async function main() { const seed = "186fMCnZjcoD8i9K"; - const MTN = await PublicKey.createWithSeed(payer.publicKey, seed, token.TOKEN_PROGRAM_ID); + const MTN = await PublicKey.createWithSeed( + payer.publicKey, + seed, + token.TOKEN_PROGRAM_ID + ); const [launch] = getLaunchAddr(launchpad.getProgramId(), MTN); - const [launchSigner] = getLaunchSignerAddr( - launchpad.getProgramId(), - launch - ); + const [launchSigner] = getLaunchSignerAddr(launchpad.getProgramId(), launch); console.log(launch.toBase58()); - const lamports = await provider.connection.getMinimumBalanceForRentExemption(token.MINT_SIZE); + const lamports = await provider.connection.getMinimumBalanceForRentExemption( + token.MINT_SIZE + ); const tx = new Transaction().add( SystemProgram.createAccountWithSeed({ @@ -63,7 +74,9 @@ async function main() { }), token.createInitializeMint2Instruction(MTN, 6, launchSigner, null) ); - tx.recentBlockhash = (await provider.connection.getLatestBlockhash()).blockhash; + tx.recentBlockhash = ( + await provider.connection.getLatestBlockhash() + ).blockhash; tx.feePayer = payer.publicKey; tx.sign(payer); @@ -79,7 +92,7 @@ async function main() { MTN, KOLLAN_PUBKEY, false, - payer.publicKey, + payer.publicKey ) .preInstructions([ ComputeBudgetProgram.setComputeUnitLimit({ units: 200_000 }), diff --git a/scripts/launchInit.ts b/scripts/launchInit.ts index b0e6e04c7..97af6eabc 100644 --- a/scripts/launchInit.ts +++ b/scripts/launchInit.ts @@ -1,5 +1,10 @@ import * as token from "@solana/spl-token"; -import { ComputeBudgetProgram, Keypair, PublicKey, Transaction } from "@solana/web3.js"; +import { + ComputeBudgetProgram, + Keypair, + PublicKey, + Transaction, +} from "@solana/web3.js"; import * as anchor from "@coral-xyz/anchor"; import { getLaunchAddr, @@ -47,8 +52,7 @@ const provider = anchor.AnchorProvider.local(rpcUrl, { const payer = provider.wallet["payer"]; const launchAuthorityAddress = await input({ - message: - "Enter the address of the launch authority", + message: "Enter the address of the launch authority", default: process.env.LAUNCH_AUTHORITY_ADDRESS, }); @@ -110,7 +114,7 @@ async function main() { ); console.log("Creating mint..."); - + const mint = await token.createMint( provider.connection, payer, @@ -123,7 +127,7 @@ async function main() { commitment: "finalized", } ); - + console.log("Mint created:", mint.toBase58()); console.log("Launching..."); @@ -138,7 +142,7 @@ async function main() { mint, new PublicKey(launchAuthorityAddress), isDevnet, - payer.publicKey, + payer.publicKey ) .preInstructions([ ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 }), diff --git a/scripts/redeemLaunch.ts b/scripts/redeemLaunch.ts index eece1d695..12b39e4b7 100644 --- a/scripts/redeemLaunch.ts +++ b/scripts/redeemLaunch.ts @@ -21,7 +21,10 @@ const rpcUrl = await input({ const walletPath = await input({ message: "Enter the path (relative to home directory) to your wallet file", - default: join(homedir(), process.env.WALLET_PATH || "/.config/solana/id.json"), + default: join( + homedir(), + process.env.WALLET_PATH || "/.config/solana/id.json" + ), }); process.env.ANCHOR_WALLET = walletPath; const provider = anchor.AnchorProvider.local(rpcUrl, { @@ -41,52 +44,55 @@ const autocrat: AutocratClient = AutocratClient.createClient({ provider }); async function main() { const launch = await launchpad.getLaunch(launchAddr); - + // Get all funding records - const allFundingRecords = await launchpad.launchpad.account.fundingRecord.all(); + const allFundingRecords = + await launchpad.launchpad.account.fundingRecord.all(); // Filter funding records for this specific launch const launchFundingRecords = allFundingRecords.filter( - record => record.account.launch.toString() === launchAddr.toString() + (record) => record.account.launch.toString() === launchAddr.toString() + ); + + console.log( + `Found ${launchFundingRecords.length} funding records for this launch` ); - - console.log(`Found ${launchFundingRecords.length} funding records for this launch`); - + if (launchFundingRecords.length === 0) { console.log("No funding records found for this launch"); return; } - + // Process in batches of 5 claims per transaction const batchSize = 5; for (let i = 0; i < launchFundingRecords.length; i += batchSize) { const batch = launchFundingRecords.slice(i, i + batchSize); console.log(batch); - - console.log(`Processing batch ${i/batchSize + 1} with ${batch.length} records`); - + + console.log( + `Processing batch ${i / batchSize + 1} with ${batch.length} records` + ); + const tx = new Transaction(); - + // Add compute budget instruction to handle multiple claims - tx.add( - ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }) - ); - + tx.add(ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 })); + // Add claim instructions for each record in the batch for (const record of batch) { const claimIx = await launchpad .claimIx(launchAddr, launch.tokenMint, record.account.funder) .transaction(); - + tx.add(claimIx); } - - await sendAndConfirmTransaction(tx, `Claim batch ${i/batchSize + 1}`); + + await sendAndConfirmTransaction(tx, `Claim batch ${i / batchSize + 1}`); } - + console.log("All claims processed successfully!"); - + // Uncomment if you want to see DAO details /* const dao = await autocrat.getDao(launch.dao); diff --git a/scripts/setupMetricMarket.ts b/scripts/setupMetricMarket.ts index 7b11877a3..b4b80b85c 100644 --- a/scripts/setupMetricMarket.ts +++ b/scripts/setupMetricMarket.ts @@ -16,65 +16,87 @@ import { import { sha256 } from "@metadaoproject/futarchy"; import { Question, Amm } from "@metadaoproject/futarchy/v0.4"; import { BN } from "bn.js"; -import { homedir } from 'os'; -import { join } from 'path'; +import { homedir } from "os"; +import { join } from "path"; -import { input, select } from '@inquirer/prompts'; +import { input, select } from "@inquirer/prompts"; const network = await select({ - message: 'Which network do you want to use?', + message: "Which network do you want to use?", choices: [ - { value: 'devnet', name: 'devnet - https://api.devnet.solana.com' }, - { value: 'mainnet', name: 'mainnet - https://api.mainnet-beta.solana.com' }, - { value: 'custom', name: 'custom RPC URL' } - ] + { value: "devnet", name: "devnet - https://api.devnet.solana.com" }, + { value: "mainnet", name: "mainnet - https://api.mainnet-beta.solana.com" }, + { value: "custom", name: "custom RPC URL" }, + ], }); -const rpcUrl = network === 'custom' - ? await input({ message: 'Enter your custom RPC URL:' }) - : network === 'devnet' +const rpcUrl = + network === "custom" + ? await input({ message: "Enter your custom RPC URL:" }) + : network === "devnet" ? "https://api.devnet.solana.com" : "https://api.mainnet-beta.solana.com"; // Add default oracle addresses const DEFAULT_ORACLE = "6awyHMshBGVjJ3ozdSJdyyDE1CTAXUwrpNMaRGMsb4sf"; -const oracleAddress = await input({ - message: 'Enter the oracle address', - default: DEFAULT_ORACLE +const oracleAddress = await input({ + message: "Enter the oracle address", + default: DEFAULT_ORACLE, }); -const walletPath = await input({ message: 'Enter the path to your wallet file', default: join(homedir(), '.config/solana/id.json') }); +const walletPath = await input({ + message: "Enter the path to your wallet file", + default: join(homedir(), ".config/solana/id.json"), +}); process.env.ANCHOR_WALLET = walletPath; -const provider = anchor.AnchorProvider.local(rpcUrl, { commitment: "confirmed" }); +const provider = anchor.AnchorProvider.local(rpcUrl, { + commitment: "confirmed", +}); const payer = provider.wallet["payer"]; const vaultProgram: ConditionalVaultClient = ConditionalVaultClient.createClient({ provider }); const ammProgram: AmmClient = AmmClient.createClient({ provider }); -const outcomeQuestionText = await input({ message: 'Enter the outcome question text (example: Will Jito approve Switchboard\'s 300k JTO NCN Grant?/No/Yes):\n' }); -const metricQuestionText = await input({ message: 'Enter the metric question text (example: Will Switchboard\'s NCN generate more than $1M by 12/01/25?/No/Yes):\n' }); +const outcomeQuestionText = await input({ + message: + "Enter the outcome question text (example: Will Jito approve Switchboard's 300k JTO NCN Grant?/No/Yes):\n", +}); +const metricQuestionText = await input({ + message: + "Enter the metric question text (example: Will Switchboard's NCN generate more than $1M by 12/01/25?/No/Yes):\n", +}); -const liquidityAmount = await input({ message: 'Enter the amount of USDC to provide as liquidity (example: 1000, must be at least 100):\n' }); +const liquidityAmount = await input({ + message: + "Enter the amount of USDC to provide as liquidity (example: 1000, must be at least 100):\n", +}); // const USDC = new PublicKey("CRWxbGNtVrTr9FAJX6SZpsvPZyi9R7VetuqecoZ1jCdD"); const USDC = MAINNET_USDC; -async function sendAndConfirmTransaction( - tx: Transaction, - label: string -) { +async function sendAndConfirmTransaction(tx: Transaction, label: string) { tx.feePayer = payer.publicKey; - tx.recentBlockhash = (await provider.connection.getLatestBlockhash()).blockhash; + tx.recentBlockhash = ( + await provider.connection.getLatestBlockhash() + ).blockhash; tx.partialSign(payer); - const txHash = await provider.connection.sendRawTransaction(tx.serialize(), { skipPreflight: true }); + const txHash = await provider.connection.sendRawTransaction(tx.serialize(), { + skipPreflight: true, + }); console.log(`${label} transaction sent:`, txHash); - + await provider.connection.confirmTransaction(txHash, "confirmed"); - const txStatus = await provider.connection.getTransaction(txHash, { maxSupportedTransactionVersion: 0 }); + const txStatus = await provider.connection.getTransaction(txHash, { + maxSupportedTransactionVersion: 0, + }); if (txStatus?.meta?.err) { - throw new Error(`Transaction failed: ${txHash}\nError: ${JSON.stringify(txStatus?.meta?.err)}`); + throw new Error( + `Transaction failed: ${txHash}\nError: ${JSON.stringify( + txStatus?.meta?.err + )}` + ); } console.log(`${label} transaction confirmed`); return txHash; @@ -97,9 +119,7 @@ async function main() { new TextEncoder().encode(outcomeQuestionText) ); - const metricQuestionId = sha256( - new TextEncoder().encode(metricQuestionText) - ); + const metricQuestionId = sha256(new TextEncoder().encode(metricQuestionText)); const outcomeQuestion = getQuestionAddr( vaultProgram.vaultProgram.programId, @@ -118,9 +138,10 @@ async function main() { outcomeQuestion ); if (!storedOutcomeQuestion) { - tx.add(await vaultProgram - .initializeQuestionIx(outcomeQuestionId, oracle, 2) - .transaction() + tx.add( + await vaultProgram + .initializeQuestionIx(outcomeQuestionId, oracle, 2) + .transaction() ); storedOutcomeQuestion = await vaultProgram.fetchQuestion(outcomeQuestion); } @@ -129,14 +150,14 @@ async function main() { metricQuestion ); if (!storedMetricQuestion) { - tx.add(await vaultProgram - .initializeQuestionIx(metricQuestionId, oracle, 2) - .transaction() + tx.add( + await vaultProgram + .initializeQuestionIx(metricQuestionId, oracle, 2) + .transaction() ); storedMetricQuestion = await vaultProgram.fetchQuestion(metricQuestion); } - console.log("OUTCOME QUESTION"); console.log(outcomeQuestion.toBase58()); // console.log(storedOutcomeQuestion); @@ -154,7 +175,11 @@ async function main() { await vaultProgram.fetchVault(outcomeVault); if (!storedOutcomeVault) { - tx.add(await vaultProgram.initializeVaultIx(outcomeQuestion, USDC, 2).transaction()); + tx.add( + await vaultProgram + .initializeVaultIx(outcomeQuestion, USDC, 2) + .transaction() + ); } const pUSDC = getFailAndPassMintAddrs( @@ -171,7 +196,11 @@ async function main() { await vaultProgram.fetchVault(metricVault); if (!storedMetricVault) { - tx.add(await vaultProgram.initializeVaultIx(metricQuestion, pUSDC, 2).transaction()); + tx.add( + await vaultProgram + .initializeVaultIx(metricQuestion, pUSDC, 2) + .transaction() + ); storedMetricVault = await vaultProgram.fetchVault(metricVault); } @@ -188,42 +217,57 @@ async function main() { ); await sendAndConfirmTransaction(tx, "First"); - + // NOW ADD METADATA TO THE VAULTS tx = new Transaction(); - tx.add(await vaultProgram.addMetadataToConditionalTokensIx( - outcomeVault, - 0, - "Fail USDC", - "fUSDC", - "https://raw.githubusercontent.com/metaDAOproject/futarchy/refs/heads/develop/scripts/assets/USDC/fUSDC.json", - ).transaction()); - - tx.add(await vaultProgram.addMetadataToConditionalTokensIx( - outcomeVault, - 1, - "Pass USDC", - "pUSDC", - "https://raw.githubusercontent.com/metaDAOproject/futarchy/refs/heads/develop/scripts/assets/USDC/pUSDC.json", - ).transaction()); - - tx.add(await vaultProgram.addMetadataToConditionalTokensIx( - metricVault, - 0, - "NO", - "pNO", - "https://raw.githubusercontent.com/metaDAOproject/futarchy/refs/heads/develop/scripts/assets/Binary/NO.json", - ).transaction()); - - tx.add(await vaultProgram.addMetadataToConditionalTokensIx( - metricVault, - 1, - "YES", - "pYES", - "https://raw.githubusercontent.com/metaDAOproject/futarchy/refs/heads/develop/scripts/assets/Binary/YES.json", - ).transaction()); + tx.add( + await vaultProgram + .addMetadataToConditionalTokensIx( + outcomeVault, + 0, + "Fail USDC", + "fUSDC", + "https://raw.githubusercontent.com/metaDAOproject/futarchy/refs/heads/develop/scripts/assets/USDC/fUSDC.json" + ) + .transaction() + ); + + tx.add( + await vaultProgram + .addMetadataToConditionalTokensIx( + outcomeVault, + 1, + "Pass USDC", + "pUSDC", + "https://raw.githubusercontent.com/metaDAOproject/futarchy/refs/heads/develop/scripts/assets/USDC/pUSDC.json" + ) + .transaction() + ); + + tx.add( + await vaultProgram + .addMetadataToConditionalTokensIx( + metricVault, + 0, + "NO", + "pNO", + "https://raw.githubusercontent.com/metaDAOproject/futarchy/refs/heads/develop/scripts/assets/Binary/NO.json" + ) + .transaction() + ); + tx.add( + await vaultProgram + .addMetadataToConditionalTokensIx( + metricVault, + 1, + "YES", + "pYES", + "https://raw.githubusercontent.com/metaDAOproject/futarchy/refs/heads/develop/scripts/assets/Binary/YES.json" + ) + .transaction() + ); await sendAndConfirmTransaction(tx, "Second"); @@ -237,39 +281,58 @@ async function main() { console.log(amm.toBase58()); if (!storedAmm) { - tx.add(await ammProgram.initializeAmmIx(pUp, pDown, new BN(0), new BN(10 ** 12), new BN(10 ** 10)).transaction()); + tx.add( + await ammProgram + .initializeAmmIx( + pUp, + pDown, + new BN(0), + new BN(10 ** 12), + new BN(10 ** 10) + ) + .transaction() + ); storedAmm = await ammProgram.fetchAmm(amm); } - tx.add(await vaultProgram - .splitTokensIx( - outcomeQuestion, - outcomeVault, - USDC, - new BN(liquidityAmountNum * 10 ** 6), - 2, - payer.publicKey - ) - .transaction() + tx.add( + await vaultProgram + .splitTokensIx( + outcomeQuestion, + outcomeVault, + USDC, + new BN(liquidityAmountNum * 10 ** 6), + 2, + payer.publicKey + ) + .transaction() ); - tx.add(await vaultProgram - .splitTokensIx( - metricQuestion, - metricVault, - pUSDC, - new BN(liquidityAmountNum * 10 ** 6), - 2, - payer.publicKey - ) - .transaction() + tx.add( + await vaultProgram + .splitTokensIx( + metricQuestion, + metricVault, + pUSDC, + new BN(liquidityAmountNum * 10 ** 6), + 2, + payer.publicKey + ) + .transaction() ); - tx.add(await ammProgram.addLiquidityIx(amm, pUp, pDown, - new BN(liquidityAmountNum * 10 ** 6), - new BN(liquidityAmountNum * 10 ** 6), - new BN(0), - payer.publicKey - ).transaction()); + tx.add( + await ammProgram + .addLiquidityIx( + amm, + pUp, + pDown, + new BN(liquidityAmountNum * 10 ** 6), + new BN(liquidityAmountNum * 10 ** 6), + new BN(0), + payer.publicKey + ) + .transaction() + ); await sendAndConfirmTransaction(tx, "Third"); } diff --git a/sdk/package.json b/sdk/package.json index 60e55fe32..ad0f1bce5 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@metadaoproject/futarchy", - "version": "0.4.0-alpha.73", + "version": "0.4.0-alpha.74", "type": "module", "main": "dist/index.js", "module": "dist/index.js", @@ -25,6 +25,7 @@ "@metaplex-foundation/umi-bundle-defaults": "^0.9.2", "@metaplex-foundation/umi-uploader-bundlr": "^0.9.2", "@noble/hashes": "^1.4.0", + "@solana/spl-memo": "^0.2.5", "@solana/spl-token": "^0.3.7", "@solana/web3.js": "^1.74.0", "bn.js": "^5.2.1", diff --git a/sdk/src/v0.4/SharedLiquidityManagerClient.ts b/sdk/src/v0.4/SharedLiquidityManagerClient.ts new file mode 100644 index 000000000..f85f72c54 --- /dev/null +++ b/sdk/src/v0.4/SharedLiquidityManagerClient.ts @@ -0,0 +1,878 @@ +import { AnchorProvider, IdlTypes, Program } from "@coral-xyz/anchor"; +import { + AccountInfo, + AddressLookupTableAccount, + ComputeBudgetProgram, + Keypair, + PublicKey, + SystemProgram, +} from "@solana/web3.js"; +import { MEMO_PROGRAM_ID } from "@solana/spl-memo"; +import { + TOKEN_PROGRAM_ID, + TOKEN_2022_PROGRAM_ID, + createAssociatedTokenAccountIdempotentInstruction, +} from "@solana/spl-token"; +import { getAssociatedTokenAddressSync } from "@solana/spl-token"; +import * as anchor from "@coral-xyz/anchor"; + +import { + SharedLiquidityManager as SharedLiquidityManagerIDLType, + IDL as SharedLiquidityManagerIDL, +} from "./types/shared_liquidity_manager.js"; +import { + SharedLiquidityPool, + SharedLiquidityPoolPosition, +} from "./types/index.js"; + +import BN from "bn.js"; +import { + SHARED_LIQUIDITY_MANAGER_PROGRAM_ID, + RAYDIUM_CP_SWAP_PROGRAM_ID, + RAYDIUM_AUTHORITY, + CONDITIONAL_VAULT_PROGRAM_ID, + AMM_PROGRAM_ID, + AUTOCRAT_PROGRAM_ID, + LOW_FEE_RAYDIUM_CONFIG, + RAYDIUM_CREATE_POOL_FEE_RECEIVE, +} from "./constants.js"; +import { + getSharedLiquidityPoolAddr, + getRaydiumCpmmPoolVaultAddr, + getRaydiumCpmmLpMintAddr, + getEventAuthorityAddr, + getDaoTreasuryAddr, + getProposalAddr, + getRaydiumCpmmObservationStateAddr, + getSharedLiquidityPoolSignerAddr, + getSpotPoolAddr, + getDraftProposalAddr, + getStakeRecordAddr, + getSlPoolPositionAddr, +} from "./utils/pda.js"; +import { AutocratClient } from "./AutocratClient.js"; +import { ProposalInstruction } from "./types/index.js"; + +export type CreateSharedLiquidityManagerClientParams = { + provider: AnchorProvider; + sharedLiquidityManagerProgramId?: PublicKey; + autocratProgramId?: PublicKey; + conditionalVaultProgramId?: PublicKey; + ammProgramId?: PublicKey; +}; + +export const RAYDIUM_INIT_POOL_STATIC_ACCOUNTS = { + raydiumAuthority: RAYDIUM_AUTHORITY, + ammConfig: LOW_FEE_RAYDIUM_CONFIG, + cpSwapProgram: RAYDIUM_CP_SWAP_PROGRAM_ID, + createPoolFee: RAYDIUM_CREATE_POOL_FEE_RECEIVE, +}; + +export class SharedLiquidityManagerClient { + public readonly provider: AnchorProvider; + public readonly program: Program; + public autocratClient: AutocratClient; + + constructor(params: CreateSharedLiquidityManagerClientParams) { + this.provider = params.provider; + this.program = new Program( + SharedLiquidityManagerIDL, + params.sharedLiquidityManagerProgramId || + SHARED_LIQUIDITY_MANAGER_PROGRAM_ID, + this.provider + ); + this.autocratClient = AutocratClient.createClient({ + provider: this.provider, + autocratProgramId: params.autocratProgramId, + conditionalVaultProgramId: params.conditionalVaultProgramId, + ammProgramId: params.ammProgramId, + }); + } + + public static createClient( + params: CreateSharedLiquidityManagerClientParams + ): SharedLiquidityManagerClient { + return new SharedLiquidityManagerClient(params); + } + + getProgramId(): PublicKey { + return this.program.programId; + } + + async getSlPool(slPool: PublicKey): Promise { + return await this.program.account.sharedLiquidityPool.fetch(slPool); + } + + async getSlPoolPosition( + position: PublicKey + ): Promise { + return await this.program.account.liquidityPosition.fetch(position); + } + + initializeSharedLiquidityPoolIx( + dao: PublicKey, + baseMint: PublicKey, + quoteMint: PublicKey, + baseAmount: BN, + quoteAmount: BN, + proposalStakeRateThresholdBps: number = 100, + creator: PublicKey = this.provider.wallet.publicKey + ) { + let slPool = getSharedLiquidityPoolAddr( + this.program.programId, + dao, + creator, + proposalStakeRateThresholdBps + )[0]; + + const [creatorSlPoolPosition] = getSlPoolPositionAddr( + this.program.programId, + slPool, + creator + ); + + let spotPool = getSpotPoolAddr(this.program.programId, slPool, 0)[0]; + + let [slPoolSigner] = getSharedLiquidityPoolSignerAddr( + this.program.programId, + slPool + ); + + return this.program.methods + .initializeSharedLiquidityPool({ + baseAmount, + quoteAmount, + proposalStakeRateThresholdBps, + }) + .accounts({ + slPool, + baseMint, + quoteMint, + dao, + spotPool, + spotPoolLpMint: getRaydiumCpmmLpMintAddr(spotPool, false)[0], + creator, + creatorSlPoolPosition, + creatorLpAccount: getAssociatedTokenAddressSync( + getRaydiumCpmmLpMintAddr(spotPool, false)[0], + this.provider.wallet.publicKey, + true + ), + creatorBaseTokenAccount: getAssociatedTokenAddressSync( + baseMint, + creator, + true + ), + creatorQuoteTokenAccount: getAssociatedTokenAddressSync( + quoteMint, + this.provider.wallet.publicKey, + true + ), + spotPoolBaseVault: getRaydiumCpmmPoolVaultAddr( + spotPool, + baseMint, + false + )[0], + spotPoolQuoteVault: getRaydiumCpmmPoolVaultAddr( + spotPool, + quoteMint, + false + )[0], + slPoolSpotLpVault: getAssociatedTokenAddressSync( + getRaydiumCpmmLpMintAddr(spotPool, false)[0], + slPoolSigner, + true + ), + slPoolBaseVault: getAssociatedTokenAddressSync( + baseMint, + slPoolSigner, + true + ), + slPoolQuoteVault: getAssociatedTokenAddressSync( + quoteMint, + slPoolSigner, + true + ), + raydiumInitPoolStatic: RAYDIUM_INIT_POOL_STATIC_ACCOUNTS, + spotPoolObservationState: getRaydiumCpmmObservationStateAddr( + spotPool, + false + )[0], + slPoolSigner, + cpSwapProgram: RAYDIUM_CP_SWAP_PROGRAM_ID, + }) + .preInstructions([ + createAssociatedTokenAccountIdempotentInstruction( + this.provider.wallet.publicKey, + getAssociatedTokenAddressSync(baseMint, slPoolSigner, true), + slPoolSigner, + baseMint + ), + createAssociatedTokenAccountIdempotentInstruction( + this.provider.wallet.publicKey, + getAssociatedTokenAddressSync(quoteMint, slPoolSigner, true), + slPoolSigner, + quoteMint + ), + ]); + } + + depositSharedLiquidityIx( + slPool: PublicKey, + activeSpotPool: PublicKey, + baseMint: PublicKey, + quoteMint: PublicKey, + lpTokenAmount: BN, + maxBaseTokenAmount: BN, + maxQuoteTokenAmount: BN, + user: PublicKey = this.provider.wallet.publicKey + ) { + const [slPoolSigner] = getSharedLiquidityPoolSignerAddr( + this.program.programId, + slPool + ); + + const [userSlPoolPosition] = getSlPoolPositionAddr( + this.program.programId, + slPool, + user + ); + + return this.program.methods + .depositSharedLiquidity({ + lpTokenAmount, + maxBaseTokenAmount, + maxQuoteTokenAmount, + }) + .accounts({ + slPool, + activeSpotPool, + user, + userBaseTokenAccount: getAssociatedTokenAddressSync(baseMint, user), + userQuoteTokenAccount: getAssociatedTokenAddressSync(quoteMint, user), + spotPoolBaseVault: getRaydiumCpmmPoolVaultAddr( + activeSpotPool, + baseMint, + false + )[0], + spotPoolQuoteVault: getRaydiumCpmmPoolVaultAddr( + activeSpotPool, + quoteMint, + false + )[0], + baseMint, + quoteMint, + spotPoolLpMint: getRaydiumCpmmLpMintAddr(activeSpotPool, false)[0], + slPoolSpotLpVault: getAssociatedTokenAddressSync( + getRaydiumCpmmLpMintAddr(activeSpotPool, false)[0], + slPoolSigner, + true + ), + userLpTokenAccount: getAssociatedTokenAddressSync( + getRaydiumCpmmLpMintAddr(activeSpotPool, false)[0], + user, + true + ), + userSlPoolPosition, + raydiumAuthority: RAYDIUM_AUTHORITY, + tokenProgram2022: TOKEN_2022_PROGRAM_ID, + cpSwapProgram: RAYDIUM_CP_SWAP_PROGRAM_ID, + }) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ + units: 300_000, + }), + ]); + } + + withdrawSharedLiquidityIx( + slPool: PublicKey, + activeSpotPool: PublicKey, + baseMint: PublicKey, + quoteMint: PublicKey, + lpTokenAmount: BN, + minimumToken0Amount: BN, + minimumToken1Amount: BN, + user: PublicKey = this.provider.wallet.publicKey + ) { + const [slPoolSigner] = getSharedLiquidityPoolSignerAddr( + this.program.programId, + slPool + ); + + const [userSlPoolPosition] = PublicKey.findProgramAddressSync( + [Buffer.from("sl_pool_position"), slPool.toBuffer(), user.toBuffer()], + this.program.programId + ); + + return this.program.methods + .withdrawSharedLiquidity({ + lpTokenAmount, + minimumToken0Amount, + minimumToken1Amount, + }) + .accounts({ + slPool, + slPoolSigner, + activeSpotPool, + slPoolSpotLpVault: getAssociatedTokenAddressSync( + getRaydiumCpmmLpMintAddr(activeSpotPool, false)[0], + slPoolSigner, + true + ), + userQuoteTokenAccount: getAssociatedTokenAddressSync(quoteMint, user), + userBaseTokenAccount: getAssociatedTokenAddressSync(baseMint, user), + spotPoolBaseVault: getRaydiumCpmmPoolVaultAddr( + activeSpotPool, + baseMint, + false + )[0], + spotPoolQuoteVault: getRaydiumCpmmPoolVaultAddr( + activeSpotPool, + quoteMint, + false + )[0], + baseMint, + quoteMint, + spotPoolLpMint: getRaydiumCpmmLpMintAddr(activeSpotPool, false)[0], + userLpTokenAccount: getAssociatedTokenAddressSync( + getRaydiumCpmmLpMintAddr(activeSpotPool, false)[0], + user, + true + ), + userSlPoolPosition, + user, + feeReceiver: this.provider.wallet.publicKey, + raydiumAuthority: RAYDIUM_AUTHORITY, + tokenProgram: TOKEN_PROGRAM_ID, + tokenProgram2022: TOKEN_2022_PROGRAM_ID, + cpSwapProgram: RAYDIUM_CP_SWAP_PROGRAM_ID, + memoProgram: MEMO_PROGRAM_ID, + eventAuthority: getEventAuthorityAddr(this.program.programId)[0], + program: this.program.programId, + }); + } + + initializeProposalWithLiquidityIx( + dao: PublicKey, + baseMint: PublicKey, + quoteMint: PublicKey, + nonce: BN, + draftProposal: PublicKey, + spotPoolIndex: number = 0, + proposalStakeRateThresholdBps: number = 100 + ) { + const [slPool] = getSharedLiquidityPoolAddr( + this.program.programId, + dao, + this.provider.wallet.publicKey, + proposalStakeRateThresholdBps + ); + + const [slPoolSigner] = getSharedLiquidityPoolSignerAddr( + this.program.programId, + slPool + ); + + const [spotPool] = getSpotPoolAddr( + this.program.programId, + slPool, + spotPoolIndex + ); + + const [proposal] = getProposalAddr( + this.autocratClient.getProgramId(), + slPoolSigner, + nonce + ); + + const { + passAmm, + failAmm, + question, + baseVault, + quoteVault, + passBaseMint, + failBaseMint, + passQuoteMint, + failQuoteMint, + passLp: passLpMint, + failLp: failLpMint, + } = this.autocratClient.getProposalPdas(proposal, baseMint, quoteMint, dao); + + const [daoTreasury] = getDaoTreasuryAddr( + this.autocratClient.getProgramId(), + dao + ); + + return this.program.methods + .initializeProposalWithLiquidity({ + nonce, + }) + .accounts({ + sharedLiquidityPool: slPool, + draftProposal, + proposalCreator: this.provider.wallet.publicKey, + proposal, + baseMint, + quoteMint, + slPoolBaseVault: getAssociatedTokenAddressSync( + baseMint, + slPoolSigner, + true + ), + slPoolQuoteVault: getAssociatedTokenAddressSync( + quoteMint, + slPoolSigner, + true + ), + slPoolSpotLpVault: getAssociatedTokenAddressSync( + getRaydiumCpmmLpMintAddr(spotPool, false)[0], + slPoolSigner, + true + ), + raydium: { + spotPool: spotPool, + spotPoolBaseVault: getRaydiumCpmmPoolVaultAddr( + spotPool, + baseMint, + false + )[0], + spotPoolQuoteVault: getRaydiumCpmmPoolVaultAddr( + spotPool, + quoteMint, + false + )[0], + lpMint: getRaydiumCpmmLpMintAddr(spotPool, false)[0], + raydiumAuthority: RAYDIUM_AUTHORITY, + tokenProgram: TOKEN_PROGRAM_ID, + tokenProgram2022: TOKEN_2022_PROGRAM_ID, + cpSwapProgram: RAYDIUM_CP_SWAP_PROGRAM_ID, + memoProgram: MEMO_PROGRAM_ID, + }, + conditionalVault: { + slPoolSigner, + question, + baseVault, + quoteVault, + baseVaultUnderlyingTokenAccount: getAssociatedTokenAddressSync( + baseMint, + baseVault, + true + ), + quoteVaultUnderlyingTokenAccount: getAssociatedTokenAddressSync( + quoteMint, + quoteVault, + true + ), + conditionalVaultProgram: CONDITIONAL_VAULT_PROGRAM_ID, + passBaseMint, + failBaseMint, + passQuoteMint, + failQuoteMint, + slPoolPassBaseVault: getAssociatedTokenAddressSync( + passBaseMint, + slPoolSigner, + true + ), + slPoolFailBaseVault: getAssociatedTokenAddressSync( + failBaseMint, + slPoolSigner, + true + ), + slPoolPassQuoteVault: getAssociatedTokenAddressSync( + passQuoteMint, + slPoolSigner, + true + ), + slPoolFailQuoteVault: getAssociatedTokenAddressSync( + failQuoteMint, + slPoolSigner, + true + ), + vaultEventAuthority: getEventAuthorityAddr( + CONDITIONAL_VAULT_PROGRAM_ID + )[0], + }, + amm: { + passAmm, + failAmm, + passLpMint, + failLpMint, + slPoolPassLpAccount: getAssociatedTokenAddressSync( + passLpMint, + slPoolSigner, + true + ), + slPoolFailLpAccount: getAssociatedTokenAddressSync( + failLpMint, + slPoolSigner, + true + ), + passAmmVaultAtaBase: getAssociatedTokenAddressSync( + passBaseMint, + passAmm, + true + ), + passAmmVaultAtaQuote: getAssociatedTokenAddressSync( + passQuoteMint, + passAmm, + true + ), + failAmmVaultAtaBase: getAssociatedTokenAddressSync( + failBaseMint, + failAmm, + true + ), + failAmmVaultAtaQuote: getAssociatedTokenAddressSync( + failQuoteMint, + failAmm, + true + ), + proposalPassLpVault: getAssociatedTokenAddressSync( + passLpMint, + daoTreasury, + true + ), + proposalFailLpVault: getAssociatedTokenAddressSync( + failLpMint, + daoTreasury, + true + ), + ammProgram: AMM_PROGRAM_ID, + eventAuthority: getEventAuthorityAddr(AMM_PROGRAM_ID)[0], + slPoolSigner, + }, + autocratEventAuthority: getEventAuthorityAddr(AUTOCRAT_PROGRAM_ID)[0], + dao, + autocratProgram: AUTOCRAT_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .preInstructions([ + createAssociatedTokenAccountIdempotentInstruction( + this.provider.wallet.publicKey, + getAssociatedTokenAddressSync(passLpMint, daoTreasury, true), + daoTreasury, + passLpMint + ), + createAssociatedTokenAccountIdempotentInstruction( + this.provider.wallet.publicKey, + getAssociatedTokenAddressSync(failLpMint, daoTreasury, true), + daoTreasury, + failLpMint + ), + ]); + } + + initializeDraftProposalIx( + sharedLiquidityPool: PublicKey, + baseMint: PublicKey, + instruction: ProposalInstruction, + draftProposalNonce: BN = new BN(Math.floor(Math.random() * 1000000)) + ) { + let [draftProposal] = getDraftProposalAddr( + this.program.programId, + draftProposalNonce + ); + + return this.program.methods + .initializeDraftProposal({ + instruction, + draftProposalNonce, + }) + .accounts({ + draftProposal, + sharedLiquidityPool, + baseMint, + stakedTokenVault: getAssociatedTokenAddressSync( + baseMint, + draftProposal, + true + ), + }); + } + + stakeToDraftProposalIx( + draftProposal: PublicKey, + baseMint: PublicKey, + amount: BN, + staker: PublicKey = this.provider.wallet.publicKey + ) { + const [stakeRecord] = getStakeRecordAddr( + this.program.programId, + draftProposal, + staker + ); + + return this.program.methods + .stakeToDraftProposal({ + amount, + }) + .accounts({ + draftProposal, + staker, + stakerTokenAccount: getAssociatedTokenAddressSync( + baseMint, + staker, + true + ), + stakedTokenVault: getAssociatedTokenAddressSync( + baseMint, + draftProposal, + true + ), + stakeRecord, + }); + } + + unstakeFromDraftProposalIx( + draftProposal: PublicKey, + baseMint: PublicKey, + amount: BN, + staker: PublicKey = this.provider.wallet.publicKey + ) { + const [stakeRecord] = getStakeRecordAddr( + this.program.programId, + draftProposal, + staker + ); + + return this.program.methods + .unstakeFromDraftProposal({ + amount, + }) + .accounts({ + draftProposal, + staker, + stakerTokenAccount: getAssociatedTokenAddressSync( + baseMint, + staker, + true + ), + stakedTokenVault: getAssociatedTokenAddressSync( + baseMint, + draftProposal, + true + ), + stakeRecord, + }); + } + + removeProposalLiquidityIx( + dao: PublicKey, + spotPool: PublicKey, + baseMint: PublicKey, + quoteMint: PublicKey, + proposalNonce: BN, + proposalStakeRateThresholdBps: number = 100, + spotPoolIndex: number = 0 + ) { + const [slPool] = getSharedLiquidityPoolAddr( + this.program.programId, + dao, + this.provider.wallet.publicKey, + proposalStakeRateThresholdBps + ); + + const [slPoolSigner] = getSharedLiquidityPoolSignerAddr( + this.program.programId, + slPool + ); + + const [proposal] = getProposalAddr( + this.autocratClient.getProgramId(), + slPoolSigner, + proposalNonce + ); + + const { + passAmm, + failAmm, + question, + baseVault, + quoteVault, + passBaseMint, + failBaseMint, + passQuoteMint, + failQuoteMint, + passLp: passLpMint, + failLp: failLpMint, + } = this.autocratClient.getProposalPdas(proposal, baseMint, quoteMint, dao); + + const [daoTreasury] = getDaoTreasuryAddr( + this.autocratClient.getProgramId(), + dao + ); + + const poolStateKp = Keypair.generate(); + const poolState = poolStateKp.publicKey; + + const [observationState] = getRaydiumCpmmObservationStateAddr( + poolState, + false + ); + + const [activeSpotPool] = getSpotPoolAddr( + this.program.programId, + slPool, + spotPoolIndex + ); + const [nextSpotPool] = getSpotPoolAddr( + this.program.programId, + slPool, + spotPoolIndex + 1 + ); + + return this.program.methods.removeProposalLiquidity().accounts({ + slPool, + proposal, + baseMint, + quoteMint, + slPoolBaseVault: getAssociatedTokenAddressSync( + baseMint, + slPoolSigner, + true + ), + slPoolQuoteVault: getAssociatedTokenAddressSync( + quoteMint, + slPoolSigner, + true + ), + slPoolSpotLpVault: getAssociatedTokenAddressSync( + getRaydiumCpmmLpMintAddr(activeSpotPool, false)[0], + slPoolSigner, + true + ), + raydium: { + activeSpotPool: activeSpotPool, + activeSpotPoolBaseVault: getRaydiumCpmmPoolVaultAddr( + activeSpotPool, + baseMint, + false + )[0], + activeSpotPoolQuoteVault: getRaydiumCpmmPoolVaultAddr( + activeSpotPool, + quoteMint, + false + )[0], + nextSpotPoolQuoteVault: getRaydiumCpmmPoolVaultAddr( + nextSpotPool, + quoteMint, + false + )[0], + slPoolSigner, + nextSpotPoolLpMint: getRaydiumCpmmLpMintAddr(nextSpotPool, false)[0], + nextSpotPoolBaseVault: getRaydiumCpmmPoolVaultAddr( + nextSpotPool, + baseMint, + false + )[0], + nextSpotPool, + nextSpotPoolObservationState: getRaydiumCpmmObservationStateAddr( + nextSpotPool, + false + )[0], + slPoolNextSpotLpVault: getAssociatedTokenAddressSync( + getRaydiumCpmmLpMintAddr(nextSpotPool, false)[0], + slPoolSigner, + true + ), + activeSpotPoolLpMint: getRaydiumCpmmLpMintAddr( + activeSpotPool, + false + )[0], + tokenProgram2022: TOKEN_2022_PROGRAM_ID, + cpSwapProgram: RAYDIUM_CP_SWAP_PROGRAM_ID, + memoProgram: MEMO_PROGRAM_ID, + baseMint, + quoteMint, + slPool, + }, + raydiumInitPoolStatic: RAYDIUM_INIT_POOL_STATIC_ACCOUNTS, + conditionalVault: { + question, + baseVault, + quoteVault, + baseVaultUnderlyingTokenAccount: getAssociatedTokenAddressSync( + baseMint, + baseVault, + true + ), + quoteVaultUnderlyingTokenAccount: getAssociatedTokenAddressSync( + quoteMint, + quoteVault, + true + ), + conditionalVaultProgram: CONDITIONAL_VAULT_PROGRAM_ID, + passBaseMint, + failBaseMint, + passQuoteMint, + failQuoteMint, + slPoolPassBaseVault: getAssociatedTokenAddressSync( + passBaseMint, + slPoolSigner, + true + ), + slPoolFailBaseVault: getAssociatedTokenAddressSync( + failBaseMint, + slPoolSigner, + true + ), + slPoolPassQuoteVault: getAssociatedTokenAddressSync( + passQuoteMint, + slPoolSigner, + true + ), + slPoolFailQuoteVault: getAssociatedTokenAddressSync( + failQuoteMint, + slPoolSigner, + true + ), + vaultEventAuthority: getEventAuthorityAddr( + CONDITIONAL_VAULT_PROGRAM_ID + )[0], + tokenProgram: TOKEN_PROGRAM_ID, + slPoolSigner, + }, + ammm: { + passAmm, + failAmm, + passLpMint, + failLpMint, + slPoolPassLpAccount: getAssociatedTokenAddressSync( + passLpMint, + slPoolSigner, + true + ), + slPoolFailLpAccount: getAssociatedTokenAddressSync( + failLpMint, + slPoolSigner, + true + ), + passAmmVaultAtaBase: getAssociatedTokenAddressSync( + passBaseMint, + passAmm, + true + ), + passAmmVaultAtaQuote: getAssociatedTokenAddressSync( + passQuoteMint, + passAmm, + true + ), + failAmmVaultAtaBase: getAssociatedTokenAddressSync( + failBaseMint, + failAmm, + true + ), + failAmmVaultAtaQuote: getAssociatedTokenAddressSync( + failQuoteMint, + failAmm, + true + ), + slPoolSigner, + ammProgram: AMM_PROGRAM_ID, + eventAuthority: getEventAuthorityAddr(AMM_PROGRAM_ID)[0], + }, + }); + } +} diff --git a/sdk/src/v0.4/constants.ts b/sdk/src/v0.4/constants.ts index 1b25fd677..cda9338df 100644 --- a/sdk/src/v0.4/constants.ts +++ b/sdk/src/v0.4/constants.ts @@ -11,6 +11,12 @@ export const AMM_PROGRAM_ID = new PublicKey( export const CONDITIONAL_VAULT_PROGRAM_ID = new PublicKey( "VLTX1ishMBbcX3rdBWGssxawAo1Q2X2qxYFYqiGodVg" ); +export const LAUNCHPAD_PROGRAM_ID = new PublicKey( + "AfJJJ5UqxhBKoE3grkKAZZsoXDE9kncbMKvqSHGsCNrE" +); +export const SHARED_LIQUIDITY_MANAGER_PROGRAM_ID = new PublicKey( + "EoJc1PYxZbnCjszampLcwJGYcB5Md47jM4oSQacRtD4d" +); export const MPL_TOKEN_METADATA_PROGRAM_ID = new PublicKey( "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" @@ -39,10 +45,6 @@ export const USDC_DECIMALS = 6; export const AUTOCRAT_LUTS: PublicKey[] = []; -export const LAUNCHPAD_PROGRAM_ID = new PublicKey( - "AfJJJ5UqxhBKoE3grkKAZZsoXDE9kncbMKvqSHGsCNrE" -); - export const RAYDIUM_AUTHORITY = PublicKey.findProgramAddressSync( [anchor.utils.bytes.utf8.encode("vault_and_lp_mint_auth_seed")], RAYDIUM_CP_SWAP_PROGRAM_ID diff --git a/sdk/src/v0.4/index.ts b/sdk/src/v0.4/index.ts index 06af1cf6c..32591eead 100644 --- a/sdk/src/v0.4/index.ts +++ b/sdk/src/v0.4/index.ts @@ -5,3 +5,4 @@ export * from "./AmmClient.js"; export * from "./AutocratClient.js"; export * from "./ConditionalVaultClient.js"; export * from "./LaunchpadClient.js"; +export * from "./SharedLiquidityManagerClient.js"; diff --git a/sdk/src/v0.4/types/amm.ts b/sdk/src/v0.4/types/amm.ts index 457efe875..e821c753c 100644 --- a/sdk/src/v0.4/types/amm.ts +++ b/sdk/src/v0.4/types/amm.ts @@ -342,6 +342,14 @@ export type Amm = { { name: "seqNum"; type: "u64"; + }, + { + name: "vaultAtaBase"; + type: "publicKey"; + }, + { + name: "vaultAtaQuote"; + type: "publicKey"; } ]; }; @@ -1167,6 +1175,14 @@ export const IDL: Amm = { name: "seqNum", type: "u64", }, + { + name: "vaultAtaBase", + type: "publicKey", + }, + { + name: "vaultAtaQuote", + type: "publicKey", + }, ], }, }, diff --git a/sdk/src/v0.4/types/autocrat.ts b/sdk/src/v0.4/types/autocrat.ts index c250e7ac0..cbd5d3692 100644 --- a/sdk/src/v0.4/types/autocrat.ts +++ b/sdk/src/v0.4/types/autocrat.ts @@ -120,6 +120,11 @@ export type Autocrat = { }, { name: "proposer"; + isMut: false; + isSigner: true; + }, + { + name: "payer"; isMut: true; isSigner: true; }, @@ -1127,6 +1132,11 @@ export const IDL: Autocrat = { }, { name: "proposer", + isMut: false, + isSigner: true, + }, + { + name: "payer", isMut: true, isSigner: true, }, diff --git a/sdk/src/v0.4/types/futarchy_amm.ts b/sdk/src/v0.4/types/futarchy_amm.ts new file mode 100644 index 000000000..b839c0906 --- /dev/null +++ b/sdk/src/v0.4/types/futarchy_amm.ts @@ -0,0 +1,23 @@ +export type FutarchyAmm = { + version: "0.1.0"; + name: "futarchy_amm"; + instructions: [ + { + name: "initialize"; + accounts: []; + args: []; + } + ]; +}; + +export const IDL: FutarchyAmm = { + version: "0.1.0", + name: "futarchy_amm", + instructions: [ + { + name: "initialize", + accounts: [], + args: [], + }, + ], +}; diff --git a/sdk/src/v0.4/types/index.ts b/sdk/src/v0.4/types/index.ts index dd2c33f34..d8696f80d 100644 --- a/sdk/src/v0.4/types/index.ts +++ b/sdk/src/v0.4/types/index.ts @@ -16,6 +16,12 @@ import { } from "./conditional_vault.js"; export { ConditionalVaultProgram, ConditionalVaultIDL }; +import { + SharedLiquidityManager as SharedLiquidityManagerProgram, + IDL as SharedLiquidityManagerIDL, +} from "./shared_liquidity_manager.js"; +export { SharedLiquidityManagerProgram, SharedLiquidityManagerIDL }; + export { LowercaseKeys } from "./utils.js"; import type { IdlAccounts, IdlTypes, IdlEvents } from "@coral-xyz/anchor"; @@ -36,6 +42,10 @@ export type Proposal = IdlAccounts["proposal"]; export type Amm = IdlAccounts["amm"]; export type Launch = IdlAccounts["launch"]; export type FundingRecord = IdlAccounts["fundingRecord"]; +export type SharedLiquidityPool = + IdlAccounts["sharedLiquidityPool"]; +export type SharedLiquidityPoolPosition = + IdlAccounts["liquidityPosition"]; export type SwapEvent = IdlEvents["SwapEvent"]; export type AddLiquidityEvent = IdlEvents["AddLiquidityEvent"]; diff --git a/sdk/src/v0.4/types/shared_liquidity_manager.ts b/sdk/src/v0.4/types/shared_liquidity_manager.ts new file mode 100644 index 000000000..b351a9fa5 --- /dev/null +++ b/sdk/src/v0.4/types/shared_liquidity_manager.ts @@ -0,0 +1,3509 @@ +export type SharedLiquidityManager = { + version: "0.1.0"; + name: "shared_liquidity_manager"; + docs: ["TODO:", "- add unstake", "- add unit tests"]; + instructions: [ + { + name: "initializeSharedLiquidityPool"; + accounts: [ + { + name: "slPool"; + isMut: true; + isSigner: false; + }, + { + name: "dao"; + isMut: false; + isSigner: false; + }, + { + name: "creator"; + isMut: true; + isSigner: true; + }, + { + name: "creatorSlPoolPosition"; + isMut: true; + isSigner: false; + }, + { + name: "baseMint"; + isMut: false; + isSigner: false; + }, + { + name: "quoteMint"; + isMut: false; + isSigner: false; + }, + { + name: "slPoolSpotLpVault"; + isMut: true; + isSigner: false; + }, + { + name: "creatorQuoteTokenAccount"; + isMut: true; + isSigner: false; + }, + { + name: "creatorBaseTokenAccount"; + isMut: true; + isSigner: false; + }, + { + name: "creatorLpAccount"; + isMut: true; + isSigner: false; + docs: ["so Raydium will create it"]; + }, + { + name: "raydiumInitPoolStatic"; + accounts: [ + { + name: "raydiumAuthority"; + isMut: false; + isSigner: false; + }, + { + name: "createPoolFee"; + isMut: true; + isSigner: false; + }, + { + name: "ammConfig"; + isMut: true; + isSigner: false; + }, + { + name: "cpSwapProgram"; + isMut: false; + isSigner: false; + }, + { + name: "rent"; + isMut: false; + isSigner: false; + }, + { + name: "associatedTokenProgram"; + isMut: false; + isSigner: false; + }, + { + name: "tokenProgram"; + isMut: false; + isSigner: false; + } + ]; + }, + { + name: "spotPool"; + isMut: true; + isSigner: false; + }, + { + name: "spotPoolLpMint"; + isMut: true; + isSigner: false; + }, + { + name: "spotPoolBaseVault"; + isMut: true; + isSigner: false; + }, + { + name: "spotPoolQuoteVault"; + isMut: true; + isSigner: false; + }, + { + name: "spotPoolObservationState"; + isMut: true; + isSigner: false; + }, + { + name: "slPoolSigner"; + isMut: false; + isSigner: false; + }, + { + name: "slPoolBaseVault"; + isMut: false; + isSigner: false; + }, + { + name: "slPoolQuoteVault"; + isMut: false; + isSigner: false; + }, + { + name: "systemProgram"; + isMut: false; + isSigner: false; + }, + { + name: "cpSwapProgram"; + isMut: false; + isSigner: false; + }, + { + name: "eventAuthority"; + isMut: false; + isSigner: false; + }, + { + name: "program"; + isMut: false; + isSigner: false; + } + ]; + args: [ + { + name: "params"; + type: { + defined: "InitializeSharedLiquidityPoolParams"; + }; + } + ]; + }, + { + name: "initializeDraftProposal"; + accounts: [ + { + name: "draftProposal"; + isMut: true; + isSigner: false; + }, + { + name: "sharedLiquidityPool"; + isMut: false; + isSigner: false; + }, + { + name: "baseMint"; + isMut: false; + isSigner: false; + }, + { + name: "stakedTokenVault"; + isMut: true; + isSigner: false; + }, + { + name: "payer"; + isMut: true; + isSigner: true; + }, + { + name: "tokenProgram"; + isMut: false; + isSigner: false; + }, + { + name: "associatedTokenProgram"; + isMut: false; + isSigner: false; + }, + { + name: "systemProgram"; + isMut: false; + isSigner: false; + }, + { + name: "eventAuthority"; + isMut: false; + isSigner: false; + }, + { + name: "program"; + isMut: false; + isSigner: false; + } + ]; + args: [ + { + name: "params"; + type: { + defined: "InitializeDraftProposalParams"; + }; + } + ]; + }, + { + name: "stakeToDraftProposal"; + accounts: [ + { + name: "draftProposal"; + isMut: true; + isSigner: false; + }, + { + name: "staker"; + isMut: false; + isSigner: true; + }, + { + name: "stakerTokenAccount"; + isMut: true; + isSigner: false; + }, + { + name: "stakedTokenVault"; + isMut: true; + isSigner: false; + }, + { + name: "payer"; + isMut: true; + isSigner: true; + }, + { + name: "stakeRecord"; + isMut: true; + isSigner: false; + }, + { + name: "tokenProgram"; + isMut: false; + isSigner: false; + }, + { + name: "systemProgram"; + isMut: false; + isSigner: false; + }, + { + name: "eventAuthority"; + isMut: false; + isSigner: false; + }, + { + name: "program"; + isMut: false; + isSigner: false; + } + ]; + args: [ + { + name: "params"; + type: { + defined: "StakeToDraftProposalParams"; + }; + } + ]; + }, + { + name: "unstakeFromDraftProposal"; + accounts: [ + { + name: "draftProposal"; + isMut: true; + isSigner: false; + }, + { + name: "staker"; + isMut: false; + isSigner: true; + }, + { + name: "stakerTokenAccount"; + isMut: true; + isSigner: false; + }, + { + name: "stakedTokenVault"; + isMut: true; + isSigner: false; + }, + { + name: "stakeRecord"; + isMut: true; + isSigner: false; + }, + { + name: "tokenProgram"; + isMut: false; + isSigner: false; + }, + { + name: "eventAuthority"; + isMut: false; + isSigner: false; + }, + { + name: "program"; + isMut: false; + isSigner: false; + } + ]; + args: [ + { + name: "params"; + type: { + defined: "UnstakeFromDraftProposalParams"; + }; + } + ]; + }, + { + name: "depositSharedLiquidity"; + accounts: [ + { + name: "slPool"; + isMut: true; + isSigner: false; + }, + { + name: "activeSpotPool"; + isMut: true; + isSigner: false; + }, + { + name: "slPoolSpotLpVault"; + isMut: true; + isSigner: false; + }, + { + name: "userQuoteTokenAccount"; + isMut: true; + isSigner: false; + }, + { + name: "userBaseTokenAccount"; + isMut: true; + isSigner: false; + }, + { + name: "spotPoolBaseVault"; + isMut: true; + isSigner: false; + }, + { + name: "spotPoolQuoteVault"; + isMut: true; + isSigner: false; + }, + { + name: "baseMint"; + isMut: false; + isSigner: false; + }, + { + name: "quoteMint"; + isMut: false; + isSigner: false; + }, + { + name: "spotPoolLpMint"; + isMut: true; + isSigner: false; + }, + { + name: "userLpTokenAccount"; + isMut: true; + isSigner: false; + }, + { + name: "userSlPoolPosition"; + isMut: true; + isSigner: false; + }, + { + name: "user"; + isMut: false; + isSigner: true; + }, + { + name: "payer"; + isMut: true; + isSigner: true; + }, + { + name: "raydiumAuthority"; + isMut: false; + isSigner: false; + }, + { + name: "associatedTokenProgram"; + isMut: false; + isSigner: false; + }, + { + name: "tokenProgram"; + isMut: false; + isSigner: false; + }, + { + name: "tokenProgram2022"; + isMut: false; + isSigner: false; + }, + { + name: "cpSwapProgram"; + isMut: false; + isSigner: false; + }, + { + name: "systemProgram"; + isMut: false; + isSigner: false; + }, + { + name: "eventAuthority"; + isMut: false; + isSigner: false; + }, + { + name: "program"; + isMut: false; + isSigner: false; + } + ]; + args: [ + { + name: "params"; + type: { + defined: "DepositSharedLiquidityParams"; + }; + } + ]; + }, + { + name: "withdrawSharedLiquidity"; + accounts: [ + { + name: "slPool"; + isMut: true; + isSigner: false; + }, + { + name: "slPoolSigner"; + isMut: false; + isSigner: false; + }, + { + name: "activeSpotPool"; + isMut: true; + isSigner: false; + }, + { + name: "slPoolSpotLpVault"; + isMut: true; + isSigner: false; + }, + { + name: "userQuoteTokenAccount"; + isMut: true; + isSigner: false; + }, + { + name: "userBaseTokenAccount"; + isMut: true; + isSigner: false; + }, + { + name: "spotPoolBaseVault"; + isMut: true; + isSigner: false; + }, + { + name: "spotPoolQuoteVault"; + isMut: true; + isSigner: false; + }, + { + name: "baseMint"; + isMut: false; + isSigner: false; + }, + { + name: "quoteMint"; + isMut: false; + isSigner: false; + }, + { + name: "spotPoolLpMint"; + isMut: true; + isSigner: false; + }, + { + name: "userLpTokenAccount"; + isMut: true; + isSigner: false; + }, + { + name: "userSlPoolPosition"; + isMut: true; + isSigner: false; + }, + { + name: "user"; + isMut: true; + isSigner: true; + }, + { + name: "feeReceiver"; + isMut: false; + isSigner: false; + }, + { + name: "raydiumAuthority"; + isMut: false; + isSigner: false; + }, + { + name: "tokenProgram"; + isMut: false; + isSigner: false; + }, + { + name: "tokenProgram2022"; + isMut: false; + isSigner: false; + }, + { + name: "cpSwapProgram"; + isMut: false; + isSigner: false; + }, + { + name: "memoProgram"; + isMut: false; + isSigner: false; + }, + { + name: "eventAuthority"; + isMut: false; + isSigner: false; + }, + { + name: "program"; + isMut: false; + isSigner: false; + } + ]; + args: [ + { + name: "params"; + type: { + defined: "WithdrawSharedLiquidityParams"; + }; + } + ]; + }, + { + name: "initializeProposalWithLiquidity"; + accounts: [ + { + name: "sharedLiquidityPool"; + isMut: true; + isSigner: false; + }, + { + name: "proposalCreator"; + isMut: false; + isSigner: true; + }, + { + name: "proposal"; + isMut: true; + isSigner: false; + }, + { + name: "slPoolBaseVault"; + isMut: true; + isSigner: false; + }, + { + name: "slPoolQuoteVault"; + isMut: true; + isSigner: false; + }, + { + name: "slPoolSpotLpVault"; + isMut: true; + isSigner: false; + }, + { + name: "baseMint"; + isMut: false; + isSigner: false; + }, + { + name: "quoteMint"; + isMut: false; + isSigner: false; + }, + { + name: "raydium"; + accounts: [ + { + name: "spotPool"; + isMut: true; + isSigner: false; + }, + { + name: "spotPoolBaseVault"; + isMut: true; + isSigner: false; + }, + { + name: "spotPoolQuoteVault"; + isMut: true; + isSigner: false; + }, + { + name: "lpMint"; + isMut: true; + isSigner: false; + }, + { + name: "raydiumAuthority"; + isMut: false; + isSigner: false; + }, + { + name: "tokenProgram"; + isMut: false; + isSigner: false; + }, + { + name: "tokenProgram2022"; + isMut: false; + isSigner: false; + }, + { + name: "cpSwapProgram"; + isMut: false; + isSigner: false; + }, + { + name: "memoProgram"; + isMut: false; + isSigner: false; + } + ]; + }, + { + name: "conditionalVault"; + accounts: [ + { + name: "question"; + isMut: true; + isSigner: false; + }, + { + name: "baseVault"; + isMut: true; + isSigner: false; + }, + { + name: "quoteVault"; + isMut: true; + isSigner: false; + }, + { + name: "baseVaultUnderlyingTokenAccount"; + isMut: true; + isSigner: false; + }, + { + name: "quoteVaultUnderlyingTokenAccount"; + isMut: true; + isSigner: false; + }, + { + name: "conditionalVaultProgram"; + isMut: false; + isSigner: false; + }, + { + name: "passBaseMint"; + isMut: true; + isSigner: false; + }, + { + name: "failBaseMint"; + isMut: true; + isSigner: false; + }, + { + name: "passQuoteMint"; + isMut: true; + isSigner: false; + }, + { + name: "failQuoteMint"; + isMut: true; + isSigner: false; + }, + { + name: "slPoolPassBaseVault"; + isMut: true; + isSigner: true; + }, + { + name: "slPoolFailBaseVault"; + isMut: true; + isSigner: true; + }, + { + name: "slPoolPassQuoteVault"; + isMut: true; + isSigner: true; + }, + { + name: "slPoolFailQuoteVault"; + isMut: true; + isSigner: true; + }, + { + name: "vaultEventAuthority"; + isMut: false; + isSigner: false; + }, + { + name: "payer"; + isMut: true; + isSigner: true; + }, + { + name: "tokenProgram"; + isMut: false; + isSigner: false; + }, + { + name: "systemProgram"; + isMut: false; + isSigner: false; + }, + { + name: "slPoolSigner"; + isMut: true; + isSigner: false; + } + ]; + }, + { + name: "amm"; + accounts: [ + { + name: "passAmm"; + isMut: true; + isSigner: false; + }, + { + name: "failAmm"; + isMut: true; + isSigner: false; + }, + { + name: "passLpMint"; + isMut: true; + isSigner: false; + }, + { + name: "failLpMint"; + isMut: true; + isSigner: false; + }, + { + name: "slPoolPassLpAccount"; + isMut: true; + isSigner: false; + }, + { + name: "slPoolFailLpAccount"; + isMut: true; + isSigner: false; + }, + { + name: "passAmmVaultAtaBase"; + isMut: true; + isSigner: false; + }, + { + name: "passAmmVaultAtaQuote"; + isMut: true; + isSigner: false; + }, + { + name: "failAmmVaultAtaBase"; + isMut: true; + isSigner: false; + }, + { + name: "failAmmVaultAtaQuote"; + isMut: true; + isSigner: false; + }, + { + name: "proposalPassLpVault"; + isMut: true; + isSigner: false; + }, + { + name: "proposalFailLpVault"; + isMut: true; + isSigner: false; + }, + { + name: "ammProgram"; + isMut: false; + isSigner: false; + }, + { + name: "eventAuthority"; + isMut: false; + isSigner: false; + }, + { + name: "payer"; + isMut: true; + isSigner: true; + }, + { + name: "systemProgram"; + isMut: false; + isSigner: false; + }, + { + name: "tokenProgram"; + isMut: false; + isSigner: false; + }, + { + name: "associatedTokenProgram"; + isMut: false; + isSigner: false; + }, + { + name: "slPoolSigner"; + isMut: false; + isSigner: false; + } + ]; + }, + { + name: "draftProposal"; + isMut: true; + isSigner: false; + }, + { + name: "dao"; + isMut: true; + isSigner: false; + }, + { + name: "autocratProgram"; + isMut: false; + isSigner: false; + }, + { + name: "systemProgram"; + isMut: false; + isSigner: false; + }, + { + name: "autocratEventAuthority"; + isMut: false; + isSigner: false; + }, + { + name: "eventAuthority"; + isMut: false; + isSigner: false; + }, + { + name: "program"; + isMut: false; + isSigner: false; + } + ]; + args: [ + { + name: "params"; + type: { + defined: "InitializeProposalWithLiquidityParams"; + }; + } + ]; + }, + { + name: "removeProposalLiquidity"; + accounts: [ + { + name: "slPool"; + isMut: true; + isSigner: false; + }, + { + name: "proposal"; + isMut: false; + isSigner: false; + }, + { + name: "slPoolBaseVault"; + isMut: true; + isSigner: false; + }, + { + name: "slPoolQuoteVault"; + isMut: true; + isSigner: false; + }, + { + name: "slPoolSpotLpVault"; + isMut: true; + isSigner: false; + }, + { + name: "baseMint"; + isMut: false; + isSigner: false; + }, + { + name: "quoteMint"; + isMut: false; + isSigner: false; + }, + { + name: "raydiumInitPoolStatic"; + accounts: [ + { + name: "raydiumAuthority"; + isMut: false; + isSigner: false; + }, + { + name: "createPoolFee"; + isMut: true; + isSigner: false; + }, + { + name: "ammConfig"; + isMut: true; + isSigner: false; + }, + { + name: "cpSwapProgram"; + isMut: false; + isSigner: false; + }, + { + name: "rent"; + isMut: false; + isSigner: false; + }, + { + name: "associatedTokenProgram"; + isMut: false; + isSigner: false; + }, + { + name: "tokenProgram"; + isMut: false; + isSigner: false; + } + ]; + }, + { + name: "raydium"; + accounts: [ + { + name: "activeSpotPool"; + isMut: true; + isSigner: false; + }, + { + name: "activeSpotPoolLpMint"; + isMut: true; + isSigner: false; + }, + { + name: "activeSpotPoolBaseVault"; + isMut: true; + isSigner: false; + }, + { + name: "activeSpotPoolQuoteVault"; + isMut: true; + isSigner: false; + }, + { + name: "tokenProgram2022"; + isMut: false; + isSigner: false; + }, + { + name: "cpSwapProgram"; + isMut: false; + isSigner: false; + }, + { + name: "memoProgram"; + isMut: false; + isSigner: false; + }, + { + name: "nextSpotPool"; + isMut: true; + isSigner: false; + }, + { + name: "nextSpotPoolLpMint"; + isMut: true; + isSigner: false; + }, + { + name: "nextSpotPoolBaseVault"; + isMut: true; + isSigner: false; + }, + { + name: "nextSpotPoolQuoteVault"; + isMut: true; + isSigner: false; + }, + { + name: "nextSpotPoolObservationState"; + isMut: true; + isSigner: false; + }, + { + name: "slPoolNextSpotLpVault"; + isMut: true; + isSigner: false; + }, + { + name: "slPool"; + isMut: false; + isSigner: false; + }, + { + name: "slPoolSigner"; + isMut: false; + isSigner: false; + }, + { + name: "baseMint"; + isMut: false; + isSigner: false; + }, + { + name: "quoteMint"; + isMut: false; + isSigner: false; + } + ]; + }, + { + name: "conditionalVault"; + accounts: [ + { + name: "question"; + isMut: true; + isSigner: false; + }, + { + name: "baseVault"; + isMut: true; + isSigner: false; + }, + { + name: "quoteVault"; + isMut: true; + isSigner: false; + }, + { + name: "baseVaultUnderlyingTokenAccount"; + isMut: true; + isSigner: false; + }, + { + name: "quoteVaultUnderlyingTokenAccount"; + isMut: true; + isSigner: false; + }, + { + name: "conditionalVaultProgram"; + isMut: false; + isSigner: false; + }, + { + name: "passBaseMint"; + isMut: true; + isSigner: false; + }, + { + name: "failBaseMint"; + isMut: true; + isSigner: false; + }, + { + name: "passQuoteMint"; + isMut: true; + isSigner: false; + }, + { + name: "failQuoteMint"; + isMut: true; + isSigner: false; + }, + { + name: "slPoolPassBaseVault"; + isMut: true; + isSigner: false; + }, + { + name: "slPoolFailBaseVault"; + isMut: true; + isSigner: false; + }, + { + name: "slPoolPassQuoteVault"; + isMut: true; + isSigner: false; + }, + { + name: "slPoolFailQuoteVault"; + isMut: true; + isSigner: false; + }, + { + name: "vaultEventAuthority"; + isMut: false; + isSigner: false; + }, + { + name: "tokenProgram"; + isMut: false; + isSigner: false; + }, + { + name: "slPoolSigner"; + isMut: true; + isSigner: false; + } + ]; + }, + { + name: "ammm"; + accounts: [ + { + name: "passAmm"; + isMut: true; + isSigner: false; + }, + { + name: "failAmm"; + isMut: true; + isSigner: false; + }, + { + name: "passLpMint"; + isMut: true; + isSigner: false; + }, + { + name: "failLpMint"; + isMut: true; + isSigner: false; + }, + { + name: "slPoolPassLpAccount"; + isMut: true; + isSigner: false; + }, + { + name: "slPoolFailLpAccount"; + isMut: true; + isSigner: false; + }, + { + name: "passAmmVaultAtaBase"; + isMut: true; + isSigner: false; + }, + { + name: "passAmmVaultAtaQuote"; + isMut: true; + isSigner: false; + }, + { + name: "failAmmVaultAtaBase"; + isMut: true; + isSigner: false; + }, + { + name: "failAmmVaultAtaQuote"; + isMut: true; + isSigner: false; + }, + { + name: "ammProgram"; + isMut: false; + isSigner: false; + }, + { + name: "eventAuthority"; + isMut: false; + isSigner: false; + }, + { + name: "slPoolSigner"; + isMut: false; + isSigner: false; + } + ]; + }, + { + name: "systemProgram"; + isMut: false; + isSigner: false; + }, + { + name: "tokenProgram"; + isMut: false; + isSigner: false; + }, + { + name: "payer"; + isMut: true; + isSigner: true; + }, + { + name: "eventAuthority"; + isMut: false; + isSigner: false; + }, + { + name: "program"; + isMut: false; + isSigner: false; + } + ]; + args: []; + } + ]; + accounts: [ + { + name: "draftProposal"; + type: { + kind: "struct"; + fields: [ + { + name: "sharedLiquidityPool"; + type: "publicKey"; + }, + { + name: "baseMint"; + type: "publicKey"; + }, + { + name: "instruction"; + type: { + defined: "ProposalInstruction"; + }; + }, + { + name: "status"; + type: { + defined: "DraftProposalStatus"; + }; + }, + { + name: "stakedTokenAmount"; + docs: [ + "The amount of tokens that have been staked on this draft proposal" + ]; + type: "u64"; + }, + { + name: "stakedTokenVault"; + docs: ["The vault that holds the staked tokens"]; + type: "publicKey"; + }, + { + name: "nonce"; + docs: ["The nonce used to create this draft proposal PDA"]; + type: "u64"; + }, + { + name: "pdaBump"; + type: "u8"; + } + ]; + }; + }, + { + name: "liquidityPosition"; + type: { + kind: "struct"; + fields: [ + { + name: "owner"; + docs: ["The owner of this position"]; + type: "publicKey"; + }, + { + name: "pool"; + docs: ["The shared liquidity pool this position belongs to"]; + type: "publicKey"; + }, + { + name: "underlyingSpotLpShares"; + docs: [ + "The amount of underlying spot LP shares this position represents" + ]; + type: "u64"; + }, + { + name: "bump"; + docs: ["The PDA bump"]; + type: "u8"; + } + ]; + }; + }, + { + name: "sharedLiquidityPool"; + type: { + kind: "struct"; + fields: [ + { + name: "pdaBump"; + docs: ["The PDA bump."]; + type: "u8"; + }, + { + name: "dao"; + docs: ["The DAO."]; + type: "publicKey"; + }, + { + name: "baseMint"; + docs: ["The base mint."]; + type: "publicKey"; + }, + { + name: "quoteMint"; + docs: ["The quote mint."]; + type: "publicKey"; + }, + { + name: "slPoolSigner"; + docs: [ + "The signer of this pool, used because Raydium pools need a SOL payer and this PDA can't hold SOL." + ]; + type: "publicKey"; + }, + { + name: "slPoolSignerBump"; + docs: ["The pda bump of the signer."]; + type: "u8"; + }, + { + name: "slPoolBaseVault"; + docs: [ + "Holds the base tokens for the shared liquidity pool when it's moving liquidity around." + ]; + type: "publicKey"; + }, + { + name: "slPoolQuoteVault"; + docs: [ + "Holds the quote tokens for the shared liquidity pool when it's moving liquidity around." + ]; + type: "publicKey"; + }, + { + name: "slPoolSpotLpVault"; + docs: ["Holds the LP tokens for the shared liquidity pool."]; + type: "publicKey"; + }, + { + name: "activeProposal"; + docs: ["The proposal that's using liquidity from this pool."]; + type: { + option: "publicKey"; + }; + }, + { + name: "proposalStakeRateThresholdBps"; + docs: [ + "The percentage of a token's supply, in basis points, that needs to be", + "staked to a draft proposal before it can be initialized." + ]; + type: "u16"; + }, + { + name: "seqNum"; + docs: [ + "The sequence number of this shared liquidity pool. Useful for sorting events." + ]; + type: "u64"; + }, + { + name: "activeSpotPool"; + docs: [ + "The current Raydium spot pool. Changes when a proposal is removed." + ]; + type: "publicKey"; + }, + { + name: "activeSpotPoolIndex"; + docs: [ + "The index of the current Raydium spot pool. Starts at 0 and increments by 1 for each new spot pool." + ]; + type: "u32"; + }, + { + name: "isBaseToken0"; + docs: [ + "Whether the base token is token0 in the current Raydium spot pool (otherwise it's token1)." + ]; + type: "bool"; + } + ]; + }; + }, + { + name: "stakeRecord"; + type: { + kind: "struct"; + fields: [ + { + name: "staker"; + type: "publicKey"; + }, + { + name: "amount"; + type: "u64"; + } + ]; + }; + } + ]; + types: [ + { + name: "DepositSharedLiquidityParams"; + type: { + kind: "struct"; + fields: [ + { + name: "lpTokenAmount"; + docs: ["The amount of LP tokens to mint"]; + type: "u64"; + }, + { + name: "maxQuoteTokenAmount"; + docs: ["The maximum amount of quote tokens to deposit"]; + type: "u64"; + }, + { + name: "maxBaseTokenAmount"; + docs: ["The maximum amount of base tokens to deposit"]; + type: "u64"; + } + ]; + }; + }, + { + name: "InitializeDraftProposalParams"; + type: { + kind: "struct"; + fields: [ + { + name: "instruction"; + type: { + defined: "ProposalInstruction"; + }; + }, + { + name: "draftProposalNonce"; + docs: [ + "The nonce for the draft proposal, not used for anything aside from the PDA" + ]; + type: "u64"; + } + ]; + }; + }, + { + name: "InitializeProposalWithLiquidityParams"; + type: { + kind: "struct"; + fields: [ + { + name: "nonce"; + type: "u64"; + } + ]; + }; + }, + { + name: "InitializeSharedLiquidityPoolParams"; + type: { + kind: "struct"; + fields: [ + { + name: "baseAmount"; + type: "u64"; + }, + { + name: "quoteAmount"; + type: "u64"; + }, + { + name: "proposalStakeRateThresholdBps"; + type: "u16"; + } + ]; + }; + }, + { + name: "StakeToDraftProposalParams"; + type: { + kind: "struct"; + fields: [ + { + name: "amount"; + type: "u64"; + } + ]; + }; + }, + { + name: "UnstakeFromDraftProposalParams"; + type: { + kind: "struct"; + fields: [ + { + name: "amount"; + type: "u64"; + } + ]; + }; + }, + { + name: "WithdrawSharedLiquidityParams"; + type: { + kind: "struct"; + fields: [ + { + name: "lpTokenAmount"; + docs: ["The amount of LP tokens to withdraw"]; + type: "u64"; + }, + { + name: "minimumToken0Amount"; + docs: ["The minimum amount of token0 to receive"]; + type: "u64"; + }, + { + name: "minimumToken1Amount"; + docs: ["The minimum amount of token1 to receive"]; + type: "u64"; + } + ]; + }; + }, + { + name: "ProposalAccount"; + type: { + kind: "struct"; + fields: [ + { + name: "pubkey"; + type: "publicKey"; + }, + { + name: "isSigner"; + type: "bool"; + }, + { + name: "isWritable"; + type: "bool"; + } + ]; + }; + }, + { + name: "ProposalInstruction"; + type: { + kind: "struct"; + fields: [ + { + name: "programId"; + type: "publicKey"; + }, + { + name: "accounts"; + type: { + vec: { + defined: "ProposalAccount"; + }; + }; + }, + { + name: "data"; + type: "bytes"; + } + ]; + }; + }, + { + name: "DraftProposalStatus"; + type: { + kind: "enum"; + variants: [ + { + name: "Draft"; + }, + { + name: "Initialized"; + } + ]; + }; + } + ]; + errors: [ + { + code: 6000; + name: "InsufficientStake"; + msg: "Insufficient stake amount"; + }, + { + code: 6001; + name: "ProposalNotFinalized"; + msg: "Proposal is not finalized"; + }, + { + code: 6002; + name: "NoLpTokensToRemove"; + msg: "No LP tokens to remove from AMM"; + }, + { + code: 6003; + name: "NoTokensFromAmm"; + msg: "No tokens received from AMM removal"; + }, + { + code: 6004; + name: "InsufficientReservesReturned"; + msg: "Insufficient reserves returned to spot AMM (less than 99.5%)"; + }, + { + code: 6005; + name: "PoolInUse"; + msg: "Pool is currently being used by an active proposal"; + }, + { + code: 6006; + name: "InsufficientLpShares"; + msg: "User does not have enough LP shares to withdraw"; + }, + { + code: 6007; + name: "SlippageExceeded"; + msg: "Slippage exceeded minimum token amounts"; + }, + { + code: 6008; + name: "NoLpTokensInPool"; + msg: "No LP tokens in pool's LP token account"; + }, + { + code: 6009; + name: "NotEnoughLpTokens"; + msg: "Not enough LP tokens to provide liquidity to proposal"; + }, + { + code: 6010; + name: "InsufficientFunds"; + msg: "Insufficient funds"; + }, + { + code: 6011; + name: "NoActiveProposal"; + msg: "No active proposal"; + }, + { + code: 6012; + name: "ProposalNotInDraftStatus"; + msg: "Proposal is not in draft status"; + }, + { + code: 6013; + name: "ProposalAlreadyActive"; + msg: "Proposal already active"; + }, + { + code: 6014; + name: "AmmAlreadyHasLiquidity"; + msg: "AMM already has liquidity"; + }, + { + code: 6015; + name: "QuestionAlreadyResolved"; + msg: "Question already resolved"; + } + ]; +}; + +export const IDL: SharedLiquidityManager = { + version: "0.1.0", + name: "shared_liquidity_manager", + docs: ["TODO:", "- add unstake", "- add unit tests"], + instructions: [ + { + name: "initializeSharedLiquidityPool", + accounts: [ + { + name: "slPool", + isMut: true, + isSigner: false, + }, + { + name: "dao", + isMut: false, + isSigner: false, + }, + { + name: "creator", + isMut: true, + isSigner: true, + }, + { + name: "creatorSlPoolPosition", + isMut: true, + isSigner: false, + }, + { + name: "baseMint", + isMut: false, + isSigner: false, + }, + { + name: "quoteMint", + isMut: false, + isSigner: false, + }, + { + name: "slPoolSpotLpVault", + isMut: true, + isSigner: false, + }, + { + name: "creatorQuoteTokenAccount", + isMut: true, + isSigner: false, + }, + { + name: "creatorBaseTokenAccount", + isMut: true, + isSigner: false, + }, + { + name: "creatorLpAccount", + isMut: true, + isSigner: false, + docs: ["so Raydium will create it"], + }, + { + name: "raydiumInitPoolStatic", + accounts: [ + { + name: "raydiumAuthority", + isMut: false, + isSigner: false, + }, + { + name: "createPoolFee", + isMut: true, + isSigner: false, + }, + { + name: "ammConfig", + isMut: true, + isSigner: false, + }, + { + name: "cpSwapProgram", + isMut: false, + isSigner: false, + }, + { + name: "rent", + isMut: false, + isSigner: false, + }, + { + name: "associatedTokenProgram", + isMut: false, + isSigner: false, + }, + { + name: "tokenProgram", + isMut: false, + isSigner: false, + }, + ], + }, + { + name: "spotPool", + isMut: true, + isSigner: false, + }, + { + name: "spotPoolLpMint", + isMut: true, + isSigner: false, + }, + { + name: "spotPoolBaseVault", + isMut: true, + isSigner: false, + }, + { + name: "spotPoolQuoteVault", + isMut: true, + isSigner: false, + }, + { + name: "spotPoolObservationState", + isMut: true, + isSigner: false, + }, + { + name: "slPoolSigner", + isMut: false, + isSigner: false, + }, + { + name: "slPoolBaseVault", + isMut: false, + isSigner: false, + }, + { + name: "slPoolQuoteVault", + isMut: false, + isSigner: false, + }, + { + name: "systemProgram", + isMut: false, + isSigner: false, + }, + { + name: "cpSwapProgram", + isMut: false, + isSigner: false, + }, + { + name: "eventAuthority", + isMut: false, + isSigner: false, + }, + { + name: "program", + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: "params", + type: { + defined: "InitializeSharedLiquidityPoolParams", + }, + }, + ], + }, + { + name: "initializeDraftProposal", + accounts: [ + { + name: "draftProposal", + isMut: true, + isSigner: false, + }, + { + name: "sharedLiquidityPool", + isMut: false, + isSigner: false, + }, + { + name: "baseMint", + isMut: false, + isSigner: false, + }, + { + name: "stakedTokenVault", + isMut: true, + isSigner: false, + }, + { + name: "payer", + isMut: true, + isSigner: true, + }, + { + name: "tokenProgram", + isMut: false, + isSigner: false, + }, + { + name: "associatedTokenProgram", + isMut: false, + isSigner: false, + }, + { + name: "systemProgram", + isMut: false, + isSigner: false, + }, + { + name: "eventAuthority", + isMut: false, + isSigner: false, + }, + { + name: "program", + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: "params", + type: { + defined: "InitializeDraftProposalParams", + }, + }, + ], + }, + { + name: "stakeToDraftProposal", + accounts: [ + { + name: "draftProposal", + isMut: true, + isSigner: false, + }, + { + name: "staker", + isMut: false, + isSigner: true, + }, + { + name: "stakerTokenAccount", + isMut: true, + isSigner: false, + }, + { + name: "stakedTokenVault", + isMut: true, + isSigner: false, + }, + { + name: "payer", + isMut: true, + isSigner: true, + }, + { + name: "stakeRecord", + isMut: true, + isSigner: false, + }, + { + name: "tokenProgram", + isMut: false, + isSigner: false, + }, + { + name: "systemProgram", + isMut: false, + isSigner: false, + }, + { + name: "eventAuthority", + isMut: false, + isSigner: false, + }, + { + name: "program", + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: "params", + type: { + defined: "StakeToDraftProposalParams", + }, + }, + ], + }, + { + name: "unstakeFromDraftProposal", + accounts: [ + { + name: "draftProposal", + isMut: true, + isSigner: false, + }, + { + name: "staker", + isMut: false, + isSigner: true, + }, + { + name: "stakerTokenAccount", + isMut: true, + isSigner: false, + }, + { + name: "stakedTokenVault", + isMut: true, + isSigner: false, + }, + { + name: "stakeRecord", + isMut: true, + isSigner: false, + }, + { + name: "tokenProgram", + isMut: false, + isSigner: false, + }, + { + name: "eventAuthority", + isMut: false, + isSigner: false, + }, + { + name: "program", + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: "params", + type: { + defined: "UnstakeFromDraftProposalParams", + }, + }, + ], + }, + { + name: "depositSharedLiquidity", + accounts: [ + { + name: "slPool", + isMut: true, + isSigner: false, + }, + { + name: "activeSpotPool", + isMut: true, + isSigner: false, + }, + { + name: "slPoolSpotLpVault", + isMut: true, + isSigner: false, + }, + { + name: "userQuoteTokenAccount", + isMut: true, + isSigner: false, + }, + { + name: "userBaseTokenAccount", + isMut: true, + isSigner: false, + }, + { + name: "spotPoolBaseVault", + isMut: true, + isSigner: false, + }, + { + name: "spotPoolQuoteVault", + isMut: true, + isSigner: false, + }, + { + name: "baseMint", + isMut: false, + isSigner: false, + }, + { + name: "quoteMint", + isMut: false, + isSigner: false, + }, + { + name: "spotPoolLpMint", + isMut: true, + isSigner: false, + }, + { + name: "userLpTokenAccount", + isMut: true, + isSigner: false, + }, + { + name: "userSlPoolPosition", + isMut: true, + isSigner: false, + }, + { + name: "user", + isMut: false, + isSigner: true, + }, + { + name: "payer", + isMut: true, + isSigner: true, + }, + { + name: "raydiumAuthority", + isMut: false, + isSigner: false, + }, + { + name: "associatedTokenProgram", + isMut: false, + isSigner: false, + }, + { + name: "tokenProgram", + isMut: false, + isSigner: false, + }, + { + name: "tokenProgram2022", + isMut: false, + isSigner: false, + }, + { + name: "cpSwapProgram", + isMut: false, + isSigner: false, + }, + { + name: "systemProgram", + isMut: false, + isSigner: false, + }, + { + name: "eventAuthority", + isMut: false, + isSigner: false, + }, + { + name: "program", + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: "params", + type: { + defined: "DepositSharedLiquidityParams", + }, + }, + ], + }, + { + name: "withdrawSharedLiquidity", + accounts: [ + { + name: "slPool", + isMut: true, + isSigner: false, + }, + { + name: "slPoolSigner", + isMut: false, + isSigner: false, + }, + { + name: "activeSpotPool", + isMut: true, + isSigner: false, + }, + { + name: "slPoolSpotLpVault", + isMut: true, + isSigner: false, + }, + { + name: "userQuoteTokenAccount", + isMut: true, + isSigner: false, + }, + { + name: "userBaseTokenAccount", + isMut: true, + isSigner: false, + }, + { + name: "spotPoolBaseVault", + isMut: true, + isSigner: false, + }, + { + name: "spotPoolQuoteVault", + isMut: true, + isSigner: false, + }, + { + name: "baseMint", + isMut: false, + isSigner: false, + }, + { + name: "quoteMint", + isMut: false, + isSigner: false, + }, + { + name: "spotPoolLpMint", + isMut: true, + isSigner: false, + }, + { + name: "userLpTokenAccount", + isMut: true, + isSigner: false, + }, + { + name: "userSlPoolPosition", + isMut: true, + isSigner: false, + }, + { + name: "user", + isMut: true, + isSigner: true, + }, + { + name: "feeReceiver", + isMut: false, + isSigner: false, + }, + { + name: "raydiumAuthority", + isMut: false, + isSigner: false, + }, + { + name: "tokenProgram", + isMut: false, + isSigner: false, + }, + { + name: "tokenProgram2022", + isMut: false, + isSigner: false, + }, + { + name: "cpSwapProgram", + isMut: false, + isSigner: false, + }, + { + name: "memoProgram", + isMut: false, + isSigner: false, + }, + { + name: "eventAuthority", + isMut: false, + isSigner: false, + }, + { + name: "program", + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: "params", + type: { + defined: "WithdrawSharedLiquidityParams", + }, + }, + ], + }, + { + name: "initializeProposalWithLiquidity", + accounts: [ + { + name: "sharedLiquidityPool", + isMut: true, + isSigner: false, + }, + { + name: "proposalCreator", + isMut: false, + isSigner: true, + }, + { + name: "proposal", + isMut: true, + isSigner: false, + }, + { + name: "slPoolBaseVault", + isMut: true, + isSigner: false, + }, + { + name: "slPoolQuoteVault", + isMut: true, + isSigner: false, + }, + { + name: "slPoolSpotLpVault", + isMut: true, + isSigner: false, + }, + { + name: "baseMint", + isMut: false, + isSigner: false, + }, + { + name: "quoteMint", + isMut: false, + isSigner: false, + }, + { + name: "raydium", + accounts: [ + { + name: "spotPool", + isMut: true, + isSigner: false, + }, + { + name: "spotPoolBaseVault", + isMut: true, + isSigner: false, + }, + { + name: "spotPoolQuoteVault", + isMut: true, + isSigner: false, + }, + { + name: "lpMint", + isMut: true, + isSigner: false, + }, + { + name: "raydiumAuthority", + isMut: false, + isSigner: false, + }, + { + name: "tokenProgram", + isMut: false, + isSigner: false, + }, + { + name: "tokenProgram2022", + isMut: false, + isSigner: false, + }, + { + name: "cpSwapProgram", + isMut: false, + isSigner: false, + }, + { + name: "memoProgram", + isMut: false, + isSigner: false, + }, + ], + }, + { + name: "conditionalVault", + accounts: [ + { + name: "question", + isMut: true, + isSigner: false, + }, + { + name: "baseVault", + isMut: true, + isSigner: false, + }, + { + name: "quoteVault", + isMut: true, + isSigner: false, + }, + { + name: "baseVaultUnderlyingTokenAccount", + isMut: true, + isSigner: false, + }, + { + name: "quoteVaultUnderlyingTokenAccount", + isMut: true, + isSigner: false, + }, + { + name: "conditionalVaultProgram", + isMut: false, + isSigner: false, + }, + { + name: "passBaseMint", + isMut: true, + isSigner: false, + }, + { + name: "failBaseMint", + isMut: true, + isSigner: false, + }, + { + name: "passQuoteMint", + isMut: true, + isSigner: false, + }, + { + name: "failQuoteMint", + isMut: true, + isSigner: false, + }, + { + name: "slPoolPassBaseVault", + isMut: true, + isSigner: true, + }, + { + name: "slPoolFailBaseVault", + isMut: true, + isSigner: true, + }, + { + name: "slPoolPassQuoteVault", + isMut: true, + isSigner: true, + }, + { + name: "slPoolFailQuoteVault", + isMut: true, + isSigner: true, + }, + { + name: "vaultEventAuthority", + isMut: false, + isSigner: false, + }, + { + name: "payer", + isMut: true, + isSigner: true, + }, + { + name: "tokenProgram", + isMut: false, + isSigner: false, + }, + { + name: "systemProgram", + isMut: false, + isSigner: false, + }, + { + name: "slPoolSigner", + isMut: true, + isSigner: false, + }, + ], + }, + { + name: "amm", + accounts: [ + { + name: "passAmm", + isMut: true, + isSigner: false, + }, + { + name: "failAmm", + isMut: true, + isSigner: false, + }, + { + name: "passLpMint", + isMut: true, + isSigner: false, + }, + { + name: "failLpMint", + isMut: true, + isSigner: false, + }, + { + name: "slPoolPassLpAccount", + isMut: true, + isSigner: false, + }, + { + name: "slPoolFailLpAccount", + isMut: true, + isSigner: false, + }, + { + name: "passAmmVaultAtaBase", + isMut: true, + isSigner: false, + }, + { + name: "passAmmVaultAtaQuote", + isMut: true, + isSigner: false, + }, + { + name: "failAmmVaultAtaBase", + isMut: true, + isSigner: false, + }, + { + name: "failAmmVaultAtaQuote", + isMut: true, + isSigner: false, + }, + { + name: "proposalPassLpVault", + isMut: true, + isSigner: false, + }, + { + name: "proposalFailLpVault", + isMut: true, + isSigner: false, + }, + { + name: "ammProgram", + isMut: false, + isSigner: false, + }, + { + name: "eventAuthority", + isMut: false, + isSigner: false, + }, + { + name: "payer", + isMut: true, + isSigner: true, + }, + { + name: "systemProgram", + isMut: false, + isSigner: false, + }, + { + name: "tokenProgram", + isMut: false, + isSigner: false, + }, + { + name: "associatedTokenProgram", + isMut: false, + isSigner: false, + }, + { + name: "slPoolSigner", + isMut: false, + isSigner: false, + }, + ], + }, + { + name: "draftProposal", + isMut: true, + isSigner: false, + }, + { + name: "dao", + isMut: true, + isSigner: false, + }, + { + name: "autocratProgram", + isMut: false, + isSigner: false, + }, + { + name: "systemProgram", + isMut: false, + isSigner: false, + }, + { + name: "autocratEventAuthority", + isMut: false, + isSigner: false, + }, + { + name: "eventAuthority", + isMut: false, + isSigner: false, + }, + { + name: "program", + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: "params", + type: { + defined: "InitializeProposalWithLiquidityParams", + }, + }, + ], + }, + { + name: "removeProposalLiquidity", + accounts: [ + { + name: "slPool", + isMut: true, + isSigner: false, + }, + { + name: "proposal", + isMut: false, + isSigner: false, + }, + { + name: "slPoolBaseVault", + isMut: true, + isSigner: false, + }, + { + name: "slPoolQuoteVault", + isMut: true, + isSigner: false, + }, + { + name: "slPoolSpotLpVault", + isMut: true, + isSigner: false, + }, + { + name: "baseMint", + isMut: false, + isSigner: false, + }, + { + name: "quoteMint", + isMut: false, + isSigner: false, + }, + { + name: "raydiumInitPoolStatic", + accounts: [ + { + name: "raydiumAuthority", + isMut: false, + isSigner: false, + }, + { + name: "createPoolFee", + isMut: true, + isSigner: false, + }, + { + name: "ammConfig", + isMut: true, + isSigner: false, + }, + { + name: "cpSwapProgram", + isMut: false, + isSigner: false, + }, + { + name: "rent", + isMut: false, + isSigner: false, + }, + { + name: "associatedTokenProgram", + isMut: false, + isSigner: false, + }, + { + name: "tokenProgram", + isMut: false, + isSigner: false, + }, + ], + }, + { + name: "raydium", + accounts: [ + { + name: "activeSpotPool", + isMut: true, + isSigner: false, + }, + { + name: "activeSpotPoolLpMint", + isMut: true, + isSigner: false, + }, + { + name: "activeSpotPoolBaseVault", + isMut: true, + isSigner: false, + }, + { + name: "activeSpotPoolQuoteVault", + isMut: true, + isSigner: false, + }, + { + name: "tokenProgram2022", + isMut: false, + isSigner: false, + }, + { + name: "cpSwapProgram", + isMut: false, + isSigner: false, + }, + { + name: "memoProgram", + isMut: false, + isSigner: false, + }, + { + name: "nextSpotPool", + isMut: true, + isSigner: false, + }, + { + name: "nextSpotPoolLpMint", + isMut: true, + isSigner: false, + }, + { + name: "nextSpotPoolBaseVault", + isMut: true, + isSigner: false, + }, + { + name: "nextSpotPoolQuoteVault", + isMut: true, + isSigner: false, + }, + { + name: "nextSpotPoolObservationState", + isMut: true, + isSigner: false, + }, + { + name: "slPoolNextSpotLpVault", + isMut: true, + isSigner: false, + }, + { + name: "slPool", + isMut: false, + isSigner: false, + }, + { + name: "slPoolSigner", + isMut: false, + isSigner: false, + }, + { + name: "baseMint", + isMut: false, + isSigner: false, + }, + { + name: "quoteMint", + isMut: false, + isSigner: false, + }, + ], + }, + { + name: "conditionalVault", + accounts: [ + { + name: "question", + isMut: true, + isSigner: false, + }, + { + name: "baseVault", + isMut: true, + isSigner: false, + }, + { + name: "quoteVault", + isMut: true, + isSigner: false, + }, + { + name: "baseVaultUnderlyingTokenAccount", + isMut: true, + isSigner: false, + }, + { + name: "quoteVaultUnderlyingTokenAccount", + isMut: true, + isSigner: false, + }, + { + name: "conditionalVaultProgram", + isMut: false, + isSigner: false, + }, + { + name: "passBaseMint", + isMut: true, + isSigner: false, + }, + { + name: "failBaseMint", + isMut: true, + isSigner: false, + }, + { + name: "passQuoteMint", + isMut: true, + isSigner: false, + }, + { + name: "failQuoteMint", + isMut: true, + isSigner: false, + }, + { + name: "slPoolPassBaseVault", + isMut: true, + isSigner: false, + }, + { + name: "slPoolFailBaseVault", + isMut: true, + isSigner: false, + }, + { + name: "slPoolPassQuoteVault", + isMut: true, + isSigner: false, + }, + { + name: "slPoolFailQuoteVault", + isMut: true, + isSigner: false, + }, + { + name: "vaultEventAuthority", + isMut: false, + isSigner: false, + }, + { + name: "tokenProgram", + isMut: false, + isSigner: false, + }, + { + name: "slPoolSigner", + isMut: true, + isSigner: false, + }, + ], + }, + { + name: "ammm", + accounts: [ + { + name: "passAmm", + isMut: true, + isSigner: false, + }, + { + name: "failAmm", + isMut: true, + isSigner: false, + }, + { + name: "passLpMint", + isMut: true, + isSigner: false, + }, + { + name: "failLpMint", + isMut: true, + isSigner: false, + }, + { + name: "slPoolPassLpAccount", + isMut: true, + isSigner: false, + }, + { + name: "slPoolFailLpAccount", + isMut: true, + isSigner: false, + }, + { + name: "passAmmVaultAtaBase", + isMut: true, + isSigner: false, + }, + { + name: "passAmmVaultAtaQuote", + isMut: true, + isSigner: false, + }, + { + name: "failAmmVaultAtaBase", + isMut: true, + isSigner: false, + }, + { + name: "failAmmVaultAtaQuote", + isMut: true, + isSigner: false, + }, + { + name: "ammProgram", + isMut: false, + isSigner: false, + }, + { + name: "eventAuthority", + isMut: false, + isSigner: false, + }, + { + name: "slPoolSigner", + isMut: false, + isSigner: false, + }, + ], + }, + { + name: "systemProgram", + isMut: false, + isSigner: false, + }, + { + name: "tokenProgram", + isMut: false, + isSigner: false, + }, + { + name: "payer", + isMut: true, + isSigner: true, + }, + { + name: "eventAuthority", + isMut: false, + isSigner: false, + }, + { + name: "program", + isMut: false, + isSigner: false, + }, + ], + args: [], + }, + ], + accounts: [ + { + name: "draftProposal", + type: { + kind: "struct", + fields: [ + { + name: "sharedLiquidityPool", + type: "publicKey", + }, + { + name: "baseMint", + type: "publicKey", + }, + { + name: "instruction", + type: { + defined: "ProposalInstruction", + }, + }, + { + name: "status", + type: { + defined: "DraftProposalStatus", + }, + }, + { + name: "stakedTokenAmount", + docs: [ + "The amount of tokens that have been staked on this draft proposal", + ], + type: "u64", + }, + { + name: "stakedTokenVault", + docs: ["The vault that holds the staked tokens"], + type: "publicKey", + }, + { + name: "nonce", + docs: ["The nonce used to create this draft proposal PDA"], + type: "u64", + }, + { + name: "pdaBump", + type: "u8", + }, + ], + }, + }, + { + name: "liquidityPosition", + type: { + kind: "struct", + fields: [ + { + name: "owner", + docs: ["The owner of this position"], + type: "publicKey", + }, + { + name: "pool", + docs: ["The shared liquidity pool this position belongs to"], + type: "publicKey", + }, + { + name: "underlyingSpotLpShares", + docs: [ + "The amount of underlying spot LP shares this position represents", + ], + type: "u64", + }, + { + name: "bump", + docs: ["The PDA bump"], + type: "u8", + }, + ], + }, + }, + { + name: "sharedLiquidityPool", + type: { + kind: "struct", + fields: [ + { + name: "pdaBump", + docs: ["The PDA bump."], + type: "u8", + }, + { + name: "dao", + docs: ["The DAO."], + type: "publicKey", + }, + { + name: "baseMint", + docs: ["The base mint."], + type: "publicKey", + }, + { + name: "quoteMint", + docs: ["The quote mint."], + type: "publicKey", + }, + { + name: "slPoolSigner", + docs: [ + "The signer of this pool, used because Raydium pools need a SOL payer and this PDA can't hold SOL.", + ], + type: "publicKey", + }, + { + name: "slPoolSignerBump", + docs: ["The pda bump of the signer."], + type: "u8", + }, + { + name: "slPoolBaseVault", + docs: [ + "Holds the base tokens for the shared liquidity pool when it's moving liquidity around.", + ], + type: "publicKey", + }, + { + name: "slPoolQuoteVault", + docs: [ + "Holds the quote tokens for the shared liquidity pool when it's moving liquidity around.", + ], + type: "publicKey", + }, + { + name: "slPoolSpotLpVault", + docs: ["Holds the LP tokens for the shared liquidity pool."], + type: "publicKey", + }, + { + name: "activeProposal", + docs: ["The proposal that's using liquidity from this pool."], + type: { + option: "publicKey", + }, + }, + { + name: "proposalStakeRateThresholdBps", + docs: [ + "The percentage of a token's supply, in basis points, that needs to be", + "staked to a draft proposal before it can be initialized.", + ], + type: "u16", + }, + { + name: "seqNum", + docs: [ + "The sequence number of this shared liquidity pool. Useful for sorting events.", + ], + type: "u64", + }, + { + name: "activeSpotPool", + docs: [ + "The current Raydium spot pool. Changes when a proposal is removed.", + ], + type: "publicKey", + }, + { + name: "activeSpotPoolIndex", + docs: [ + "The index of the current Raydium spot pool. Starts at 0 and increments by 1 for each new spot pool.", + ], + type: "u32", + }, + { + name: "isBaseToken0", + docs: [ + "Whether the base token is token0 in the current Raydium spot pool (otherwise it's token1).", + ], + type: "bool", + }, + ], + }, + }, + { + name: "stakeRecord", + type: { + kind: "struct", + fields: [ + { + name: "staker", + type: "publicKey", + }, + { + name: "amount", + type: "u64", + }, + ], + }, + }, + ], + types: [ + { + name: "DepositSharedLiquidityParams", + type: { + kind: "struct", + fields: [ + { + name: "lpTokenAmount", + docs: ["The amount of LP tokens to mint"], + type: "u64", + }, + { + name: "maxQuoteTokenAmount", + docs: ["The maximum amount of quote tokens to deposit"], + type: "u64", + }, + { + name: "maxBaseTokenAmount", + docs: ["The maximum amount of base tokens to deposit"], + type: "u64", + }, + ], + }, + }, + { + name: "InitializeDraftProposalParams", + type: { + kind: "struct", + fields: [ + { + name: "instruction", + type: { + defined: "ProposalInstruction", + }, + }, + { + name: "draftProposalNonce", + docs: [ + "The nonce for the draft proposal, not used for anything aside from the PDA", + ], + type: "u64", + }, + ], + }, + }, + { + name: "InitializeProposalWithLiquidityParams", + type: { + kind: "struct", + fields: [ + { + name: "nonce", + type: "u64", + }, + ], + }, + }, + { + name: "InitializeSharedLiquidityPoolParams", + type: { + kind: "struct", + fields: [ + { + name: "baseAmount", + type: "u64", + }, + { + name: "quoteAmount", + type: "u64", + }, + { + name: "proposalStakeRateThresholdBps", + type: "u16", + }, + ], + }, + }, + { + name: "StakeToDraftProposalParams", + type: { + kind: "struct", + fields: [ + { + name: "amount", + type: "u64", + }, + ], + }, + }, + { + name: "UnstakeFromDraftProposalParams", + type: { + kind: "struct", + fields: [ + { + name: "amount", + type: "u64", + }, + ], + }, + }, + { + name: "WithdrawSharedLiquidityParams", + type: { + kind: "struct", + fields: [ + { + name: "lpTokenAmount", + docs: ["The amount of LP tokens to withdraw"], + type: "u64", + }, + { + name: "minimumToken0Amount", + docs: ["The minimum amount of token0 to receive"], + type: "u64", + }, + { + name: "minimumToken1Amount", + docs: ["The minimum amount of token1 to receive"], + type: "u64", + }, + ], + }, + }, + { + name: "ProposalAccount", + type: { + kind: "struct", + fields: [ + { + name: "pubkey", + type: "publicKey", + }, + { + name: "isSigner", + type: "bool", + }, + { + name: "isWritable", + type: "bool", + }, + ], + }, + }, + { + name: "ProposalInstruction", + type: { + kind: "struct", + fields: [ + { + name: "programId", + type: "publicKey", + }, + { + name: "accounts", + type: { + vec: { + defined: "ProposalAccount", + }, + }, + }, + { + name: "data", + type: "bytes", + }, + ], + }, + }, + { + name: "DraftProposalStatus", + type: { + kind: "enum", + variants: [ + { + name: "Draft", + }, + { + name: "Initialized", + }, + ], + }, + }, + ], + errors: [ + { + code: 6000, + name: "InsufficientStake", + msg: "Insufficient stake amount", + }, + { + code: 6001, + name: "ProposalNotFinalized", + msg: "Proposal is not finalized", + }, + { + code: 6002, + name: "NoLpTokensToRemove", + msg: "No LP tokens to remove from AMM", + }, + { + code: 6003, + name: "NoTokensFromAmm", + msg: "No tokens received from AMM removal", + }, + { + code: 6004, + name: "InsufficientReservesReturned", + msg: "Insufficient reserves returned to spot AMM (less than 99.5%)", + }, + { + code: 6005, + name: "PoolInUse", + msg: "Pool is currently being used by an active proposal", + }, + { + code: 6006, + name: "InsufficientLpShares", + msg: "User does not have enough LP shares to withdraw", + }, + { + code: 6007, + name: "SlippageExceeded", + msg: "Slippage exceeded minimum token amounts", + }, + { + code: 6008, + name: "NoLpTokensInPool", + msg: "No LP tokens in pool's LP token account", + }, + { + code: 6009, + name: "NotEnoughLpTokens", + msg: "Not enough LP tokens to provide liquidity to proposal", + }, + { + code: 6010, + name: "InsufficientFunds", + msg: "Insufficient funds", + }, + { + code: 6011, + name: "NoActiveProposal", + msg: "No active proposal", + }, + { + code: 6012, + name: "ProposalNotInDraftStatus", + msg: "Proposal is not in draft status", + }, + { + code: 6013, + name: "ProposalAlreadyActive", + msg: "Proposal already active", + }, + { + code: 6014, + name: "AmmAlreadyHasLiquidity", + msg: "AMM already has liquidity", + }, + { + code: 6015, + name: "QuestionAlreadyResolved", + msg: "Question already resolved", + }, + ], +}; diff --git a/sdk/src/v0.4/utils/pda.ts b/sdk/src/v0.4/utils/pda.ts index fc31ec2f8..1df1c1dc1 100644 --- a/sdk/src/v0.4/utils/pda.ts +++ b/sdk/src/v0.4/utils/pda.ts @@ -13,6 +13,7 @@ import { DEVNET_RAYDIUM_CP_SWAP_PROGRAM_ID, MPL_TOKEN_METADATA_PROGRAM_ID, RAYDIUM_CP_SWAP_PROGRAM_ID, + SHARED_LIQUIDITY_MANAGER_PROGRAM_ID, } from "../constants.js"; import { LAUNCHPAD_PROGRAM_ID } from "../constants.js"; @@ -203,6 +204,34 @@ export const getLiquidityPoolAddr = ( ); }; +export const getSharedLiquidityPoolAddr = ( + programId: PublicKey = SHARED_LIQUIDITY_MANAGER_PROGRAM_ID, + dao: PublicKey, + creator: PublicKey, + proposalStakeRateThresholdBps: number +): [PublicKey, number] => { + return PublicKey.findProgramAddressSync( + [ + Buffer.from("sl_pool"), + dao.toBuffer(), + creator.toBuffer(), + new BN(proposalStakeRateThresholdBps).toArrayLike(Buffer, "le", 2), + ], + programId + ); +}; + +export const getSlPoolPositionAddr = ( + programId: PublicKey = SHARED_LIQUIDITY_MANAGER_PROGRAM_ID, + slPool: PublicKey, + user: PublicKey +): [PublicKey, number] => { + return PublicKey.findProgramAddressSync( + [Buffer.from("sl_pool_position"), slPool.toBuffer(), user.toBuffer()], + programId + ); +}; + export const getRaydiumCpmmLpMintAddr = ( poolState: PublicKey, isDevnet: boolean @@ -215,3 +244,80 @@ export const getRaydiumCpmmLpMintAddr = ( programId ); }; + +export const getRaydiumCpmmPoolVaultAddr = ( + poolState: PublicKey, + token: PublicKey, + isDevnet: boolean +): [PublicKey, number] => { + const programId = isDevnet + ? DEVNET_RAYDIUM_CP_SWAP_PROGRAM_ID + : RAYDIUM_CP_SWAP_PROGRAM_ID; + return PublicKey.findProgramAddressSync( + [ + utils.bytes.utf8.encode("pool_vault"), + poolState.toBuffer(), + token.toBuffer(), + ], + programId + ); +}; + +export const getRaydiumCpmmObservationStateAddr = ( + poolState: PublicKey, + isDevnet: boolean +): [PublicKey, number] => { + const programId = isDevnet + ? DEVNET_RAYDIUM_CP_SWAP_PROGRAM_ID + : RAYDIUM_CP_SWAP_PROGRAM_ID; + return PublicKey.findProgramAddressSync( + [utils.bytes.utf8.encode("observation"), poolState.toBuffer()], + programId + ); +}; + +export const getSharedLiquidityPoolSignerAddr = ( + programId: PublicKey = SHARED_LIQUIDITY_MANAGER_PROGRAM_ID, + slPool: PublicKey +): [PublicKey, number] => { + return PublicKey.findProgramAddressSync( + [Buffer.from("sl_pool_signer"), slPool.toBuffer()], + programId + ); +}; + +export const getSpotPoolAddr = ( + programId: PublicKey = SHARED_LIQUIDITY_MANAGER_PROGRAM_ID, + slPool: PublicKey, + index: number +): [PublicKey, number] => { + return PublicKey.findProgramAddressSync( + [ + utils.bytes.utf8.encode("spot_pool"), + slPool.toBuffer(), + new BN(index).toArrayLike(Buffer, "le", 4), + ], + programId + ); +}; + +export const getDraftProposalAddr = ( + programId: PublicKey = SHARED_LIQUIDITY_MANAGER_PROGRAM_ID, + nonce: BN +): [PublicKey, number] => { + return PublicKey.findProgramAddressSync( + [Buffer.from("draft_proposal"), nonce.toArrayLike(Buffer, "le", 8)], + programId + ); +}; + +export const getStakeRecordAddr = ( + programId: PublicKey = SHARED_LIQUIDITY_MANAGER_PROGRAM_ID, + draftProposal: PublicKey, + staker: PublicKey +): [PublicKey, number] => { + return PublicKey.findProgramAddressSync( + [Buffer.from("stake_record"), draftProposal.toBuffer(), staker.toBuffer()], + programId + ); +}; diff --git a/sdk/yarn.lock b/sdk/yarn.lock index 67ef13297..38b070f7f 100644 --- a/sdk/yarn.lock +++ b/sdk/yarn.lock @@ -751,6 +751,13 @@ "@solana/codecs-core" "2.0.0-experimental.8618508" "@solana/codecs-numbers" "2.0.0-experimental.8618508" +"@solana/spl-memo@^0.2.5": + version "0.2.5" + resolved "https://registry.yarnpkg.com/@solana/spl-memo/-/spl-memo-0.2.5.tgz#a7828cdd1e810ff77c7c015ac97dfa166d0651fe" + integrity sha512-0Zx5t3gAdcHlRTt2O3RgGlni1x7vV7Xq7j4z9q8kKOMgU03PyoTbFQ/BSYCcICHzkaqD7ZxAiaJ6dlXolg01oA== + dependencies: + buffer "^6.0.3" + "@solana/spl-token-metadata@^0.1.2": version "0.1.2" resolved "https://registry.yarnpkg.com/@solana/spl-token-metadata/-/spl-token-metadata-0.1.2.tgz#876e13432bd2960bd3cac16b9b0af63e69e37719" diff --git a/tests/amm/integration/ammLifecycle.test.ts b/tests/amm/integration/ammLifecycle.test.ts index 68e56cc74..2fa3ca2dd 100644 --- a/tests/amm/integration/ammLifecycle.test.ts +++ b/tests/amm/integration/ammLifecycle.test.ts @@ -46,7 +46,13 @@ export default async function () { await this.mintTo(USDC, this.payer.publicKey, this.payer, 10_000 * 10 ** 6); let proposal = Keypair.generate().publicKey; - amm = await ammClient.createAmm(proposal, META, USDC, toBN(DAY_IN_SLOTS), 500); + amm = await ammClient.createAmm( + proposal, + META, + USDC, + toBN(DAY_IN_SLOTS), + 500 + ); // 1. Initialize AMM const initialAmm = await ammClient.getAmm(amm); diff --git a/tests/amm/unit/addLiquidity.test.ts b/tests/amm/unit/addLiquidity.test.ts index 0b857dc96..e9af8f8d5 100644 --- a/tests/amm/unit/addLiquidity.test.ts +++ b/tests/amm/unit/addLiquidity.test.ts @@ -40,7 +40,13 @@ export default function suite() { ); let proposal = Keypair.generate().publicKey; - amm = await ammClient.createAmm(proposal, META, USDC, toBN(DAY_IN_SLOTS), 500); + amm = await ammClient.createAmm( + proposal, + META, + USDC, + toBN(DAY_IN_SLOTS), + 500 + ); await this.createTokenAccount(META, this.payer.publicKey); await this.createTokenAccount(USDC, this.payer.publicKey); diff --git a/tests/amm/unit/crankThatTwap.test.ts b/tests/amm/unit/crankThatTwap.test.ts index d6206924d..0ee60fdf9 100644 --- a/tests/amm/unit/crankThatTwap.test.ts +++ b/tests/amm/unit/crankThatTwap.test.ts @@ -3,7 +3,12 @@ import { ComputeBudgetProgram, Keypair, PublicKey } from "@solana/web3.js"; import { assert } from "chai"; import { createMint, mintTo } from "spl-token-bankrun"; import * as anchor from "@coral-xyz/anchor"; -import { advanceBySlots, DAY_IN_SLOTS, ONE_MINUTE_IN_SLOTS, toBN } from "../../utils.js"; +import { + advanceBySlots, + DAY_IN_SLOTS, + ONE_MINUTE_IN_SLOTS, + toBN, +} from "../../utils.js"; import { BN } from "bn.js"; export default function suite() { @@ -39,7 +44,14 @@ export default function suite() { let proposal = Keypair.generate().publicKey; // $500 initial price, $10 change per update - amm = await ammClient.createAmm(proposal, META, USDC, toBN(twapStartDelaySlots), 500, 10); + amm = await ammClient.createAmm( + proposal, + META, + USDC, + toBN(twapStartDelaySlots), + 500, + 10 + ); // $1000 where liquidity is, await ammClient @@ -58,94 +70,126 @@ export default function suite() { const initialAmm = await ammClient.getAmm(amm); const initialLastUpdatedSlot = initialAmm.oracle.lastUpdatedSlot; - await advanceBySlots(this.context, ONE_MINUTE_IN_SLOTS -1n); + await advanceBySlots(this.context, ONE_MINUTE_IN_SLOTS - 1n); await ammClient - .crankThatTwapIx(amm) - .preInstructions([ - ComputeBudgetProgram.setComputeUnitPrice({ - microLamports: 1 - }), - ]) - .rpc(); + .crankThatTwapIx(amm) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitPrice({ + microLamports: 1, + }), + ]) + .rpc(); let updatedAmm = await ammClient.getAmm(amm); - assert.isTrue(updatedAmm.oracle.lastUpdatedSlot.eq(initialLastUpdatedSlot), "Oracle should not be updated if insufficient slots have passed"); - assert.isTrue(updatedAmm.oracle.lastObservation.eq(PriceMath.getAmmPrice(500, 9, 6))); + assert.isTrue( + updatedAmm.oracle.lastUpdatedSlot.eq(initialLastUpdatedSlot), + "Oracle should not be updated if insufficient slots have passed" + ); + assert.isTrue( + updatedAmm.oracle.lastObservation.eq(PriceMath.getAmmPrice(500, 9, 6)) + ); await advanceBySlots(this.context, 1n); await ammClient - .crankThatTwapIx(amm) - .preInstructions([ - ComputeBudgetProgram.setComputeUnitPrice({ - microLamports: 2 - }), - ]) - .rpc(); + .crankThatTwapIx(amm) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitPrice({ + microLamports: 2, + }), + ]) + .rpc(); updatedAmm = await ammClient.getAmm(amm); // observation should be updated but not aggregator - assert.isTrue(updatedAmm.oracle.lastUpdatedSlot.eq(initialLastUpdatedSlot.addn(Number(ONE_MINUTE_IN_SLOTS)))) - assert.isTrue(updatedAmm.oracle.lastObservation.eq(PriceMath.getAmmPrice(510, 9, 6))); + assert.isTrue( + updatedAmm.oracle.lastUpdatedSlot.eq( + initialLastUpdatedSlot.addn(Number(ONE_MINUTE_IN_SLOTS)) + ) + ); + assert.isTrue( + updatedAmm.oracle.lastObservation.eq(PriceMath.getAmmPrice(510, 9, 6)) + ); assert.isTrue(updatedAmm.oracle.aggregator.eqn(0)); await advanceBySlots(this.context, DAY_IN_SLOTS / 2n); await ammClient - .crankThatTwapIx(amm) - .preInstructions([ - ComputeBudgetProgram.setComputeUnitPrice({ - microLamports: 3 - }), - ]) - .rpc(); + .crankThatTwapIx(amm) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitPrice({ + microLamports: 3, + }), + ]) + .rpc(); updatedAmm = await ammClient.getAmm(amm); - assert.isTrue(updatedAmm.oracle.lastObservation.eq(PriceMath.getAmmPrice(520, 9, 6))); + assert.isTrue( + updatedAmm.oracle.lastObservation.eq(PriceMath.getAmmPrice(520, 9, 6)) + ); assert.isTrue(updatedAmm.oracle.aggregator.eqn(0)); - await advanceBySlots(this.context, DAY_IN_SLOTS / 2n + 1n - ONE_MINUTE_IN_SLOTS); + await advanceBySlots( + this.context, + DAY_IN_SLOTS / 2n + 1n - ONE_MINUTE_IN_SLOTS + ); await ammClient - .crankThatTwapIx(amm) - .preInstructions([ - ComputeBudgetProgram.setComputeUnitPrice({ - microLamports: 4 - }), - ]) - .rpc(); + .crankThatTwapIx(amm) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitPrice({ + microLamports: 4, + }), + ]) + .rpc(); updatedAmm = await ammClient.getAmm(amm); - assert.isTrue(updatedAmm.oracle.lastObservation.eq(PriceMath.getAmmPrice(530, 9, 6))); + assert.isTrue( + updatedAmm.oracle.lastObservation.eq(PriceMath.getAmmPrice(530, 9, 6)) + ); // only 1 slot has passed, so aggregator should be 0 - assert.isTrue(updatedAmm.oracle.aggregator.eq(PriceMath.getAmmPrice(530, 9, 6))); + assert.isTrue( + updatedAmm.oracle.aggregator.eq(PriceMath.getAmmPrice(530, 9, 6)) + ); - const twapStartSlot = initialAmm.createdAtSlot.addn(Number(twapStartDelaySlots)); - const twapSlotsPassed = updatedAmm.oracle.lastUpdatedSlot.sub(twapStartSlot); + const twapStartSlot = initialAmm.createdAtSlot.addn( + Number(twapStartDelaySlots) + ); + const twapSlotsPassed = + updatedAmm.oracle.lastUpdatedSlot.sub(twapStartSlot); assert.isTrue(twapSlotsPassed.eqn(1)); // if this is true, then `get_twap()` will return 530 (530 / 1) await advanceBySlots(this.context, ONE_MINUTE_IN_SLOTS * 2n); await ammClient - .crankThatTwapIx(amm) - .preInstructions([ - ComputeBudgetProgram.setComputeUnitPrice({ - microLamports: 5 - }), - ]) - .rpc(); + .crankThatTwapIx(amm) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitPrice({ + microLamports: 5, + }), + ]) + .rpc(); updatedAmm = await ammClient.getAmm(amm); - assert.isTrue(updatedAmm.oracle.lastObservation.eq(PriceMath.getAmmPrice(540, 9, 6))); + assert.isTrue( + updatedAmm.oracle.lastObservation.eq(PriceMath.getAmmPrice(540, 9, 6)) + ); // 2 minutes have passed, so aggregator should be 2 * 540 + 530 - assert.isTrue(updatedAmm.oracle.aggregator.eq(PriceMath.getAmmPrice(540, 9, 6).mul(new BN(ONE_MINUTE_IN_SLOTS.toString())).muln(2).add(PriceMath.getAmmPrice(530, 9, 6)))); + assert.isTrue( + updatedAmm.oracle.aggregator.eq( + PriceMath.getAmmPrice(540, 9, 6) + .mul(new BN(ONE_MINUTE_IN_SLOTS.toString())) + .muln(2) + .add(PriceMath.getAmmPrice(530, 9, 6)) + ) + ); }); it("updates oracle and sequence number when crankThatTwap is called", async function () { diff --git a/tests/amm/unit/initializeAmm.test.ts b/tests/amm/unit/initializeAmm.test.ts index e11fd8956..48636b1ec 100644 --- a/tests/amm/unit/initializeAmm.test.ts +++ b/tests/amm/unit/initializeAmm.test.ts @@ -43,7 +43,13 @@ export default function suite() { let amm: PublicKey; [amm, bump] = getAmmAddr(ammClient.program.programId, META, USDC); - await ammClient.createAmm(Keypair.generate().publicKey, META, USDC, twapStartDelaySlots, 500); + await ammClient.createAmm( + Keypair.generate().publicKey, + META, + USDC, + twapStartDelaySlots, + 500 + ); const ammAcc = await ammClient.getAmm(amm); @@ -66,11 +72,7 @@ export default function suite() { ammAcc.oracle.initialObservation.eq(expectedInitialObservation) ); assert.equal(ammAcc.seqNum.toString(), "0"); - assert.isTrue( - ammAcc.oracle.startDelaySlots.eq( - twapStartDelaySlots - ) - ); + assert.isTrue(ammAcc.oracle.startDelaySlots.eq(twapStartDelaySlots)); }); it("fails to create an amm with two identical mints", async function () { diff --git a/tests/amm/unit/removeLiquidity.test.ts b/tests/amm/unit/removeLiquidity.test.ts index 4da2eb487..e5c880942 100644 --- a/tests/amm/unit/removeLiquidity.test.ts +++ b/tests/amm/unit/removeLiquidity.test.ts @@ -48,7 +48,13 @@ export default function suite() { await this.mintTo(USDC, this.payer.publicKey, this.payer, 10_000 * 10 ** 6); let proposal = Keypair.generate().publicKey; - amm = await ammClient.createAmm(proposal, META, USDC, toBN(DAY_IN_SLOTS), 500); + amm = await ammClient.createAmm( + proposal, + META, + USDC, + toBN(DAY_IN_SLOTS), + 500 + ); await ammClient.addLiquidity(amm, 1000, 2); }); diff --git a/tests/amm/unit/swap.test.ts b/tests/amm/unit/swap.test.ts index 18259e905..f308d3493 100644 --- a/tests/amm/unit/swap.test.ts +++ b/tests/amm/unit/swap.test.ts @@ -49,7 +49,13 @@ export default function suite() { await this.mintTo(USDC, this.payer.publicKey, this.payer, 10_000 * 10 ** 6); let proposal = Keypair.generate().publicKey; - amm = await ammClient.createAmm(proposal, META, USDC, toBN(DAY_IN_SLOTS), 500); + amm = await ammClient.createAmm( + proposal, + META, + USDC, + toBN(DAY_IN_SLOTS), + 500 + ); await ammClient .addLiquidityIx( diff --git a/tests/autocrat/autocrat.ts b/tests/autocrat/autocrat.ts index d849414d6..2eb3e73e2 100644 --- a/tests/autocrat/autocrat.ts +++ b/tests/autocrat/autocrat.ts @@ -177,7 +177,15 @@ export default function suite() { describe("#initialize_dao", async function () { it("initializes the DAO", async function () { - dao = await autocratClient.initializeDao(META, 400, 5, 5000, USDC, undefined, new BN(DAY_IN_SLOTS.toString())); + dao = await autocratClient.initializeDao( + META, + 400, + 5, + 5000, + USDC, + undefined, + new BN(DAY_IN_SLOTS.toString()) + ); let treasuryPdaBump; [daoTreasury, treasuryPdaBump] = PublicKey.findProgramAddressSync( diff --git a/tests/conditionalVault/unit/mergeTokens.test.ts b/tests/conditionalVault/unit/mergeTokens.test.ts index 0f450976a..30f294590 100644 --- a/tests/conditionalVault/unit/mergeTokens.test.ts +++ b/tests/conditionalVault/unit/mergeTokens.test.ts @@ -1,6 +1,6 @@ import { sha256 } from "@metadaoproject/futarchy"; import { ConditionalVaultClient } from "@metadaoproject/futarchy/v0.4"; -import { Keypair, PublicKey } from "@solana/web3.js"; +import { ComputeBudgetProgram, Keypair, PublicKey } from "@solana/web3.js"; import { assert } from "chai"; import { createAssociatedTokenAccount, @@ -135,6 +135,9 @@ export default function suite() { ).then((acc) => acc.amount); await vaultClient .mergeTokensIx(question, vault, underlyingTokenMint, new BN(500), 2) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 1 }), + ]) .rpc(); balanceAfter = await getAccount( this.banksClient, diff --git a/tests/conditionalVault/unit/redeemTokens.test.ts b/tests/conditionalVault/unit/redeemTokens.test.ts index 2fbfea43f..10e29dad9 100644 --- a/tests/conditionalVault/unit/redeemTokens.test.ts +++ b/tests/conditionalVault/unit/redeemTokens.test.ts @@ -1,6 +1,6 @@ import { sha256 } from "@metadaoproject/futarchy"; import { ConditionalVaultClient } from "@metadaoproject/futarchy/v0.4"; -import { Keypair, PublicKey } from "@solana/web3.js"; +import { ComputeBudgetProgram, Keypair, PublicKey } from "@solana/web3.js"; import { assert } from "chai"; import { createAssociatedTokenAccount, @@ -125,6 +125,10 @@ export default function suite() { await vaultClient .redeemTokensIx(question, vault, underlyingTokenMint, 2) + .preInstructions([ + // To prevent the test from failing due to thinking it has already processed the instruction + ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 1 }), + ]) .rpc(); let balanceAfter = await getAccount( diff --git a/tests/conditionalVault/unit/splitTokens.test.ts b/tests/conditionalVault/unit/splitTokens.test.ts index dec3ecd43..4951b0063 100644 --- a/tests/conditionalVault/unit/splitTokens.test.ts +++ b/tests/conditionalVault/unit/splitTokens.test.ts @@ -1,6 +1,6 @@ import { sha256 } from "@metadaoproject/futarchy"; import { ConditionalVaultClient } from "@metadaoproject/futarchy/v0.4"; -import { Keypair, PublicKey } from "@solana/web3.js"; +import { ComputeBudgetProgram, Keypair, PublicKey } from "@solana/web3.js"; import { assert } from "chai"; import { createAssociatedTokenAccount, @@ -226,6 +226,10 @@ export default function suite() { await vaultClient .splitTokensIx(question, vault, underlyingTokenMint, new BN(1000), 2) + .preInstructions([ + // To prevent the test from failing due to thinking it has already processed the instruction + ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 1 }), + ]) .rpc(); storedVault = await vaultClient.fetchVault(vault); diff --git a/tests/fixtures/raydium_cpmm.ts b/tests/fixtures/raydium_cpmm.ts new file mode 100644 index 000000000..1887ac448 --- /dev/null +++ b/tests/fixtures/raydium_cpmm.ts @@ -0,0 +1,1759 @@ +export type RaydiumCpmm = { + version: "0.1.0"; + name: "raydium_cpmm"; + instructions: [ + { + name: "initialize"; + docs: [ + "Creates a pool for the given token pair and the initial price", + "", + "# Arguments", + "", + "* `ctx`- The context of accounts", + "* `init_amount_0` - the initial amount_0 to deposit", + "* `init_amount_1` - the initial amount_1 to deposit", + "* `open_time` - the timestamp allowed for swap", + "" + ]; + accounts: [ + { + name: "creator"; + isMut: true; + isSigner: true; + docs: ["Address paying to create the pool. Can be anyone"]; + }, + { + name: "ammConfig"; + isMut: false; + isSigner: false; + docs: ["Which config the pool belongs to."]; + }, + { + name: "authority"; + isMut: false; + isSigner: false; + }, + { + name: "poolState"; + isMut: true; + isSigner: true; + docs: [ + "PDA account:", + "seeds = [", + "POOL_SEED.as_bytes(),", + "amm_config.key().as_ref(),", + "token_0_mint.key().as_ref(),", + "token_1_mint.key().as_ref(),", + "],", + "", + "Or random account: must be signed by cli" + ]; + }, + { + name: "token0Mint"; + isMut: false; + isSigner: false; + docs: ["Token_0 mint, the key must smaller then token_1 mint."]; + }, + { + name: "token1Mint"; + isMut: false; + isSigner: false; + docs: ["Token_1 mint, the key must grater then token_0 mint."]; + }, + { + name: "lpMint"; + isMut: true; + isSigner: false; + docs: ["pool lp mint"]; + }, + { + name: "creatorToken0"; + isMut: true; + isSigner: false; + docs: ["payer token0 account"]; + }, + { + name: "creatorToken1"; + isMut: true; + isSigner: false; + docs: ["creator token1 account"]; + }, + { + name: "creatorLpToken"; + isMut: true; + isSigner: false; + docs: ["creator lp token account"]; + }, + { + name: "token0Vault"; + isMut: true; + isSigner: false; + }, + { + name: "token1Vault"; + isMut: true; + isSigner: false; + }, + { + name: "createPoolFee"; + isMut: true; + isSigner: false; + docs: ["create pool fee account"]; + }, + { + name: "observationState"; + isMut: true; + isSigner: false; + docs: ["an account to store oracle observations"]; + }, + { + name: "tokenProgram"; + isMut: false; + isSigner: false; + docs: ["Program to create mint account and mint tokens"]; + }, + { + name: "token0Program"; + isMut: false; + isSigner: false; + docs: ["Spl token program or token program 2022"]; + }, + { + name: "token1Program"; + isMut: false; + isSigner: false; + docs: ["Spl token program or token program 2022"]; + }, + { + name: "associatedTokenProgram"; + isMut: false; + isSigner: false; + docs: ["Program to create an ATA for receiving position NFT"]; + }, + { + name: "systemProgram"; + isMut: false; + isSigner: false; + docs: ["To create a new program account"]; + }, + { + name: "rent"; + isMut: false; + isSigner: false; + docs: ["Sysvar for program account"]; + } + ]; + args: [ + { + name: "initAmount0"; + type: "u64"; + }, + { + name: "initAmount1"; + type: "u64"; + }, + { + name: "openTime"; + type: "u64"; + } + ]; + }, + { + name: "deposit"; + docs: [ + "Creates a pool for the given token pair and the initial price", + "", + "# Arguments", + "", + "* `ctx`- The context of accounts", + "* `lp_token_amount` - Pool token amount to transfer. token_a and token_b amount are set by the current exchange rate and size of the pool", + "* `maximum_token_0_amount` - Maximum token 0 amount to deposit, prevents excessive slippage", + "* `maximum_token_1_amount` - Maximum token 1 amount to deposit, prevents excessive slippage", + "" + ]; + accounts: [ + { + name: "owner"; + isMut: false; + isSigner: true; + docs: ["Pays to mint the position"]; + }, + { + name: "authority"; + isMut: false; + isSigner: false; + }, + { + name: "poolState"; + isMut: true; + isSigner: false; + }, + { + name: "ownerLpToken"; + isMut: true; + isSigner: false; + docs: ["Owner lp tokan account"]; + }, + { + name: "token0Account"; + isMut: true; + isSigner: false; + docs: ["The payer's token account for token_0"]; + }, + { + name: "token1Account"; + isMut: true; + isSigner: false; + docs: ["The payer's token account for token_1"]; + }, + { + name: "token0Vault"; + isMut: true; + isSigner: false; + docs: ["The address that holds pool tokens for token_0"]; + }, + { + name: "token1Vault"; + isMut: true; + isSigner: false; + docs: ["The address that holds pool tokens for token_1"]; + }, + { + name: "tokenProgram"; + isMut: false; + isSigner: false; + docs: ["token Program"]; + }, + { + name: "tokenProgram2022"; + isMut: false; + isSigner: false; + docs: ["Token program 2022"]; + }, + { + name: "vault0Mint"; + isMut: false; + isSigner: false; + docs: ["The mint of token_0 vault"]; + }, + { + name: "vault1Mint"; + isMut: false; + isSigner: false; + docs: ["The mint of token_1 vault"]; + }, + { + name: "lpMint"; + isMut: true; + isSigner: false; + docs: ["Lp token mint"]; + } + ]; + args: [ + { + name: "lpTokenAmount"; + type: "u64"; + }, + { + name: "maximumToken0Amount"; + type: "u64"; + }, + { + name: "maximumToken1Amount"; + type: "u64"; + } + ]; + }, + { + name: "withdraw"; + docs: [ + "Withdraw lp for token0 ande token1", + "", + "# Arguments", + "", + "* `ctx`- The context of accounts", + "* `lp_token_amount` - Amount of pool tokens to burn. User receives an output of token a and b based on the percentage of the pool tokens that are returned.", + "* `minimum_token_0_amount` - Minimum amount of token 0 to receive, prevents excessive slippage", + "* `minimum_token_1_amount` - Minimum amount of token 1 to receive, prevents excessive slippage", + "" + ]; + accounts: [ + { + name: "owner"; + isMut: false; + isSigner: true; + docs: ["Pays to mint the position"]; + }, + { + name: "authority"; + isMut: false; + isSigner: false; + }, + { + name: "poolState"; + isMut: true; + isSigner: false; + docs: ["Pool state account"]; + }, + { + name: "ownerLpToken"; + isMut: true; + isSigner: false; + docs: ["Owner lp token account"]; + }, + { + name: "token0Account"; + isMut: true; + isSigner: false; + docs: ["The token account for receive token_0,"]; + }, + { + name: "token1Account"; + isMut: true; + isSigner: false; + docs: ["The token account for receive token_1"]; + }, + { + name: "token0Vault"; + isMut: true; + isSigner: false; + docs: ["The address that holds pool tokens for token_0"]; + }, + { + name: "token1Vault"; + isMut: true; + isSigner: false; + docs: ["The address that holds pool tokens for token_1"]; + }, + { + name: "tokenProgram"; + isMut: false; + isSigner: false; + docs: ["token Program"]; + }, + { + name: "tokenProgram2022"; + isMut: false; + isSigner: false; + docs: ["Token program 2022"]; + }, + { + name: "vault0Mint"; + isMut: false; + isSigner: false; + docs: ["The mint of token_0 vault"]; + }, + { + name: "vault1Mint"; + isMut: false; + isSigner: false; + docs: ["The mint of token_1 vault"]; + }, + { + name: "lpMint"; + isMut: true; + isSigner: false; + docs: ["Pool lp token mint"]; + }, + { + name: "memoProgram"; + isMut: false; + isSigner: false; + docs: ["memo program"]; + } + ]; + args: [ + { + name: "lpTokenAmount"; + type: "u64"; + }, + { + name: "minimumToken0Amount"; + type: "u64"; + }, + { + name: "minimumToken1Amount"; + type: "u64"; + } + ]; + }, + { + name: "swapBaseInput"; + docs: [ + "Swap the tokens in the pool base input amount", + "", + "# Arguments", + "", + "* `ctx`- The context of accounts", + "* `amount_in` - input amount to transfer, output to DESTINATION is based on the exchange rate", + "* `minimum_amount_out` - Minimum amount of output token, prevents excessive slippage", + "" + ]; + accounts: [ + { + name: "payer"; + isMut: false; + isSigner: true; + docs: ["The user performing the swap"]; + }, + { + name: "authority"; + isMut: false; + isSigner: false; + }, + { + name: "ammConfig"; + isMut: false; + isSigner: false; + docs: ["The factory state to read protocol fees"]; + }, + { + name: "poolState"; + isMut: true; + isSigner: false; + docs: [ + "The program account of the pool in which the swap will be performed" + ]; + }, + { + name: "inputTokenAccount"; + isMut: true; + isSigner: false; + docs: ["The user token account for input token"]; + }, + { + name: "outputTokenAccount"; + isMut: true; + isSigner: false; + docs: ["The user token account for output token"]; + }, + { + name: "inputVault"; + isMut: true; + isSigner: false; + docs: ["The vault token account for input token"]; + }, + { + name: "outputVault"; + isMut: true; + isSigner: false; + docs: ["The vault token account for output token"]; + }, + { + name: "inputTokenProgram"; + isMut: false; + isSigner: false; + docs: ["SPL program for input token transfers"]; + }, + { + name: "outputTokenProgram"; + isMut: false; + isSigner: false; + docs: ["SPL program for output token transfers"]; + }, + { + name: "inputTokenMint"; + isMut: false; + isSigner: false; + docs: ["The mint of input token"]; + }, + { + name: "outputTokenMint"; + isMut: false; + isSigner: false; + docs: ["The mint of output token"]; + }, + { + name: "observationState"; + isMut: true; + isSigner: false; + docs: ["The program account for the most recent oracle observation"]; + } + ]; + args: [ + { + name: "amountIn"; + type: "u64"; + }, + { + name: "minimumAmountOut"; + type: "u64"; + } + ]; + }, + { + name: "swapBaseOutput"; + docs: [ + "Swap the tokens in the pool base output amount", + "", + "# Arguments", + "", + "* `ctx`- The context of accounts", + "* `max_amount_in` - input amount prevents excessive slippage", + "* `amount_out` - amount of output token", + "" + ]; + accounts: [ + { + name: "payer"; + isMut: false; + isSigner: true; + docs: ["The user performing the swap"]; + }, + { + name: "authority"; + isMut: false; + isSigner: false; + }, + { + name: "ammConfig"; + isMut: false; + isSigner: false; + docs: ["The factory state to read protocol fees"]; + }, + { + name: "poolState"; + isMut: true; + isSigner: false; + docs: [ + "The program account of the pool in which the swap will be performed" + ]; + }, + { + name: "inputTokenAccount"; + isMut: true; + isSigner: false; + docs: ["The user token account for input token"]; + }, + { + name: "outputTokenAccount"; + isMut: true; + isSigner: false; + docs: ["The user token account for output token"]; + }, + { + name: "inputVault"; + isMut: true; + isSigner: false; + docs: ["The vault token account for input token"]; + }, + { + name: "outputVault"; + isMut: true; + isSigner: false; + docs: ["The vault token account for output token"]; + }, + { + name: "inputTokenProgram"; + isMut: false; + isSigner: false; + docs: ["SPL program for input token transfers"]; + }, + { + name: "outputTokenProgram"; + isMut: false; + isSigner: false; + docs: ["SPL program for output token transfers"]; + }, + { + name: "inputTokenMint"; + isMut: false; + isSigner: false; + docs: ["The mint of input token"]; + }, + { + name: "outputTokenMint"; + isMut: false; + isSigner: false; + docs: ["The mint of output token"]; + }, + { + name: "observationState"; + isMut: true; + isSigner: false; + docs: ["The program account for the most recent oracle observation"]; + } + ]; + args: [ + { + name: "maxAmountIn"; + type: "u64"; + }, + { + name: "amountOut"; + type: "u64"; + } + ]; + } + ]; + accounts: [ + { + name: "ammConfig"; + docs: ["Holds the current owner of the factory"]; + type: { + kind: "struct"; + fields: [ + { + name: "bump"; + docs: ["Bump to identify PDA"]; + type: "u8"; + }, + { + name: "disableCreatePool"; + docs: ["Status to control if new pool can be create"]; + type: "bool"; + }, + { + name: "index"; + docs: ["Config index"]; + type: "u16"; + }, + { + name: "tradeFeeRate"; + docs: ["The trade fee, denominated in hundredths of a bip (10^-6)"]; + type: "u64"; + }, + { + name: "protocolFeeRate"; + docs: ["The protocol fee"]; + type: "u64"; + }, + { + name: "fundFeeRate"; + docs: ["The fund fee, denominated in hundredths of a bip (10^-6)"]; + type: "u64"; + }, + { + name: "createPoolFee"; + docs: ["Fee for create a new pool"]; + type: "u64"; + }, + { + name: "protocolOwner"; + docs: ["Address of the protocol fee owner"]; + type: "publicKey"; + }, + { + name: "fundOwner"; + docs: ["Address of the fund fee owner"]; + type: "publicKey"; + }, + { + name: "padding"; + docs: ["padding"]; + type: { + array: ["u64", 16]; + }; + } + ]; + }; + }, + { + name: "poolState"; + type: { + kind: "struct"; + fields: [ + { + name: "ammConfig"; + docs: ["Which config the pool belongs"]; + type: "publicKey"; + }, + { + name: "poolCreator"; + docs: ["pool creator"]; + type: "publicKey"; + }, + { + name: "token0Vault"; + docs: ["Token A"]; + type: "publicKey"; + }, + { + name: "token1Vault"; + docs: ["Token B"]; + type: "publicKey"; + }, + { + name: "lpMint"; + docs: [ + "Pool tokens are issued when A or B tokens are deposited.", + "Pool tokens can be withdrawn back to the original A or B token." + ]; + type: "publicKey"; + }, + { + name: "token0Mint"; + docs: ["Mint information for token A"]; + type: "publicKey"; + }, + { + name: "token1Mint"; + docs: ["Mint information for token B"]; + type: "publicKey"; + }, + { + name: "token0Program"; + docs: ["token_0 program"]; + type: "publicKey"; + }, + { + name: "token1Program"; + docs: ["token_1 program"]; + type: "publicKey"; + }, + { + name: "observationKey"; + docs: ["observation account to store oracle data"]; + type: "publicKey"; + }, + { + name: "authBump"; + type: "u8"; + }, + { + name: "status"; + docs: [ + "Bitwise representation of the state of the pool", + "bit0, 1: disable deposit(vaule is 1), 0: normal", + "bit1, 1: disable withdraw(vaule is 2), 0: normal", + "bit2, 1: disable swap(vaule is 4), 0: normal" + ]; + type: "u8"; + }, + { + name: "lpMintDecimals"; + type: "u8"; + }, + { + name: "mint0Decimals"; + docs: ["mint0 and mint1 decimals"]; + type: "u8"; + }, + { + name: "mint1Decimals"; + type: "u8"; + }, + { + name: "lpSupply"; + docs: ["lp mint supply"]; + type: "u64"; + }, + { + name: "protocolFeesToken0"; + docs: [ + "The amounts of token_0 and token_1 that are owed to the liquidity provider." + ]; + type: "u64"; + }, + { + name: "protocolFeesToken1"; + type: "u64"; + }, + { + name: "fundFeesToken0"; + type: "u64"; + }, + { + name: "fundFeesToken1"; + type: "u64"; + }, + { + name: "openTime"; + docs: ["The timestamp allowed for swap in the pool."]; + type: "u64"; + }, + { + name: "padding"; + docs: ["padding for future updates"]; + type: { + array: ["u64", 32]; + }; + } + ]; + }; + }, + { + name: "observationState"; + type: { + kind: "struct"; + fields: [ + { + name: "initialized"; + docs: ["Whether the ObservationState is initialized"]; + type: "bool"; + }, + { + name: "observationIndex"; + docs: ["the most-recently updated index of the observations array"]; + type: "u16"; + }, + { + name: "poolId"; + type: "publicKey"; + }, + { + name: "observations"; + docs: ["observation array"]; + type: { + array: [ + { + defined: "Observation"; + }, + 100 + ]; + }; + }, + { + name: "padding"; + docs: ["padding for feature update"]; + type: { + array: ["u64", 4]; + }; + } + ]; + }; + } + ]; + types: [ + { + name: "Observation"; + docs: ["The element of observations in ObservationState"]; + type: { + kind: "struct"; + fields: [ + { + name: "blockTimestamp"; + docs: ["The block timestamp of the observation"]; + type: "u64"; + }, + { + name: "cumulativeToken0PriceX32"; + docs: [ + "the cumulative of token0 price during the duration time, Q32.32, the remaining 64 bit for overflow" + ]; + type: "u128"; + }, + { + name: "cumulativeToken1PriceX32"; + docs: [ + "the cumulative of token1 price during the duration time, Q32.32, the remaining 64 bit for overflow" + ]; + type: "u128"; + } + ]; + }; + }, + { + name: "PoolStatusBitIndex"; + type: { + kind: "enum"; + variants: [ + { + name: "Deposit"; + }, + { + name: "Withdraw"; + }, + { + name: "Swap"; + } + ]; + }; + }, + { + name: "PoolStatusBitFlag"; + type: { + kind: "enum"; + variants: [ + { + name: "Enable"; + }, + { + name: "Disable"; + } + ]; + }; + } + ]; +}; + +export const IDL: RaydiumCpmm = { + version: "0.1.0", + name: "raydium_cpmm", + instructions: [ + { + name: "initialize", + docs: [ + "Creates a pool for the given token pair and the initial price", + "", + "# Arguments", + "", + "* `ctx`- The context of accounts", + "* `init_amount_0` - the initial amount_0 to deposit", + "* `init_amount_1` - the initial amount_1 to deposit", + "* `open_time` - the timestamp allowed for swap", + "", + ], + accounts: [ + { + name: "creator", + isMut: true, + isSigner: true, + docs: ["Address paying to create the pool. Can be anyone"], + }, + { + name: "ammConfig", + isMut: false, + isSigner: false, + docs: ["Which config the pool belongs to."], + }, + { + name: "authority", + isMut: false, + isSigner: false, + }, + { + name: "poolState", + isMut: true, + isSigner: true, + docs: [ + "PDA account:", + "seeds = [", + "POOL_SEED.as_bytes(),", + "amm_config.key().as_ref(),", + "token_0_mint.key().as_ref(),", + "token_1_mint.key().as_ref(),", + "],", + "", + "Or random account: must be signed by cli", + ], + }, + { + name: "token0Mint", + isMut: false, + isSigner: false, + docs: ["Token_0 mint, the key must smaller then token_1 mint."], + }, + { + name: "token1Mint", + isMut: false, + isSigner: false, + docs: ["Token_1 mint, the key must grater then token_0 mint."], + }, + { + name: "lpMint", + isMut: true, + isSigner: false, + docs: ["pool lp mint"], + }, + { + name: "creatorToken0", + isMut: true, + isSigner: false, + docs: ["payer token0 account"], + }, + { + name: "creatorToken1", + isMut: true, + isSigner: false, + docs: ["creator token1 account"], + }, + { + name: "creatorLpToken", + isMut: true, + isSigner: false, + docs: ["creator lp token account"], + }, + { + name: "token0Vault", + isMut: true, + isSigner: false, + }, + { + name: "token1Vault", + isMut: true, + isSigner: false, + }, + { + name: "createPoolFee", + isMut: true, + isSigner: false, + docs: ["create pool fee account"], + }, + { + name: "observationState", + isMut: true, + isSigner: false, + docs: ["an account to store oracle observations"], + }, + { + name: "tokenProgram", + isMut: false, + isSigner: false, + docs: ["Program to create mint account and mint tokens"], + }, + { + name: "token0Program", + isMut: false, + isSigner: false, + docs: ["Spl token program or token program 2022"], + }, + { + name: "token1Program", + isMut: false, + isSigner: false, + docs: ["Spl token program or token program 2022"], + }, + { + name: "associatedTokenProgram", + isMut: false, + isSigner: false, + docs: ["Program to create an ATA for receiving position NFT"], + }, + { + name: "systemProgram", + isMut: false, + isSigner: false, + docs: ["To create a new program account"], + }, + { + name: "rent", + isMut: false, + isSigner: false, + docs: ["Sysvar for program account"], + }, + ], + args: [ + { + name: "initAmount0", + type: "u64", + }, + { + name: "initAmount1", + type: "u64", + }, + { + name: "openTime", + type: "u64", + }, + ], + }, + { + name: "deposit", + docs: [ + "Creates a pool for the given token pair and the initial price", + "", + "# Arguments", + "", + "* `ctx`- The context of accounts", + "* `lp_token_amount` - Pool token amount to transfer. token_a and token_b amount are set by the current exchange rate and size of the pool", + "* `maximum_token_0_amount` - Maximum token 0 amount to deposit, prevents excessive slippage", + "* `maximum_token_1_amount` - Maximum token 1 amount to deposit, prevents excessive slippage", + "", + ], + accounts: [ + { + name: "owner", + isMut: false, + isSigner: true, + docs: ["Pays to mint the position"], + }, + { + name: "authority", + isMut: false, + isSigner: false, + }, + { + name: "poolState", + isMut: true, + isSigner: false, + }, + { + name: "ownerLpToken", + isMut: true, + isSigner: false, + docs: ["Owner lp tokan account"], + }, + { + name: "token0Account", + isMut: true, + isSigner: false, + docs: ["The payer's token account for token_0"], + }, + { + name: "token1Account", + isMut: true, + isSigner: false, + docs: ["The payer's token account for token_1"], + }, + { + name: "token0Vault", + isMut: true, + isSigner: false, + docs: ["The address that holds pool tokens for token_0"], + }, + { + name: "token1Vault", + isMut: true, + isSigner: false, + docs: ["The address that holds pool tokens for token_1"], + }, + { + name: "tokenProgram", + isMut: false, + isSigner: false, + docs: ["token Program"], + }, + { + name: "tokenProgram2022", + isMut: false, + isSigner: false, + docs: ["Token program 2022"], + }, + { + name: "vault0Mint", + isMut: false, + isSigner: false, + docs: ["The mint of token_0 vault"], + }, + { + name: "vault1Mint", + isMut: false, + isSigner: false, + docs: ["The mint of token_1 vault"], + }, + { + name: "lpMint", + isMut: true, + isSigner: false, + docs: ["Lp token mint"], + }, + ], + args: [ + { + name: "lpTokenAmount", + type: "u64", + }, + { + name: "maximumToken0Amount", + type: "u64", + }, + { + name: "maximumToken1Amount", + type: "u64", + }, + ], + }, + { + name: "withdraw", + docs: [ + "Withdraw lp for token0 ande token1", + "", + "# Arguments", + "", + "* `ctx`- The context of accounts", + "* `lp_token_amount` - Amount of pool tokens to burn. User receives an output of token a and b based on the percentage of the pool tokens that are returned.", + "* `minimum_token_0_amount` - Minimum amount of token 0 to receive, prevents excessive slippage", + "* `minimum_token_1_amount` - Minimum amount of token 1 to receive, prevents excessive slippage", + "", + ], + accounts: [ + { + name: "owner", + isMut: false, + isSigner: true, + docs: ["Pays to mint the position"], + }, + { + name: "authority", + isMut: false, + isSigner: false, + }, + { + name: "poolState", + isMut: true, + isSigner: false, + docs: ["Pool state account"], + }, + { + name: "ownerLpToken", + isMut: true, + isSigner: false, + docs: ["Owner lp token account"], + }, + { + name: "token0Account", + isMut: true, + isSigner: false, + docs: ["The token account for receive token_0,"], + }, + { + name: "token1Account", + isMut: true, + isSigner: false, + docs: ["The token account for receive token_1"], + }, + { + name: "token0Vault", + isMut: true, + isSigner: false, + docs: ["The address that holds pool tokens for token_0"], + }, + { + name: "token1Vault", + isMut: true, + isSigner: false, + docs: ["The address that holds pool tokens for token_1"], + }, + { + name: "tokenProgram", + isMut: false, + isSigner: false, + docs: ["token Program"], + }, + { + name: "tokenProgram2022", + isMut: false, + isSigner: false, + docs: ["Token program 2022"], + }, + { + name: "vault0Mint", + isMut: false, + isSigner: false, + docs: ["The mint of token_0 vault"], + }, + { + name: "vault1Mint", + isMut: false, + isSigner: false, + docs: ["The mint of token_1 vault"], + }, + { + name: "lpMint", + isMut: true, + isSigner: false, + docs: ["Pool lp token mint"], + }, + { + name: "memoProgram", + isMut: false, + isSigner: false, + docs: ["memo program"], + }, + ], + args: [ + { + name: "lpTokenAmount", + type: "u64", + }, + { + name: "minimumToken0Amount", + type: "u64", + }, + { + name: "minimumToken1Amount", + type: "u64", + }, + ], + }, + { + name: "swapBaseInput", + docs: [ + "Swap the tokens in the pool base input amount", + "", + "# Arguments", + "", + "* `ctx`- The context of accounts", + "* `amount_in` - input amount to transfer, output to DESTINATION is based on the exchange rate", + "* `minimum_amount_out` - Minimum amount of output token, prevents excessive slippage", + "", + ], + accounts: [ + { + name: "payer", + isMut: false, + isSigner: true, + docs: ["The user performing the swap"], + }, + { + name: "authority", + isMut: false, + isSigner: false, + }, + { + name: "ammConfig", + isMut: false, + isSigner: false, + docs: ["The factory state to read protocol fees"], + }, + { + name: "poolState", + isMut: true, + isSigner: false, + docs: [ + "The program account of the pool in which the swap will be performed", + ], + }, + { + name: "inputTokenAccount", + isMut: true, + isSigner: false, + docs: ["The user token account for input token"], + }, + { + name: "outputTokenAccount", + isMut: true, + isSigner: false, + docs: ["The user token account for output token"], + }, + { + name: "inputVault", + isMut: true, + isSigner: false, + docs: ["The vault token account for input token"], + }, + { + name: "outputVault", + isMut: true, + isSigner: false, + docs: ["The vault token account for output token"], + }, + { + name: "inputTokenProgram", + isMut: false, + isSigner: false, + docs: ["SPL program for input token transfers"], + }, + { + name: "outputTokenProgram", + isMut: false, + isSigner: false, + docs: ["SPL program for output token transfers"], + }, + { + name: "inputTokenMint", + isMut: false, + isSigner: false, + docs: ["The mint of input token"], + }, + { + name: "outputTokenMint", + isMut: false, + isSigner: false, + docs: ["The mint of output token"], + }, + { + name: "observationState", + isMut: true, + isSigner: false, + docs: ["The program account for the most recent oracle observation"], + }, + ], + args: [ + { + name: "amountIn", + type: "u64", + }, + { + name: "minimumAmountOut", + type: "u64", + }, + ], + }, + { + name: "swapBaseOutput", + docs: [ + "Swap the tokens in the pool base output amount", + "", + "# Arguments", + "", + "* `ctx`- The context of accounts", + "* `max_amount_in` - input amount prevents excessive slippage", + "* `amount_out` - amount of output token", + "", + ], + accounts: [ + { + name: "payer", + isMut: false, + isSigner: true, + docs: ["The user performing the swap"], + }, + { + name: "authority", + isMut: false, + isSigner: false, + }, + { + name: "ammConfig", + isMut: false, + isSigner: false, + docs: ["The factory state to read protocol fees"], + }, + { + name: "poolState", + isMut: true, + isSigner: false, + docs: [ + "The program account of the pool in which the swap will be performed", + ], + }, + { + name: "inputTokenAccount", + isMut: true, + isSigner: false, + docs: ["The user token account for input token"], + }, + { + name: "outputTokenAccount", + isMut: true, + isSigner: false, + docs: ["The user token account for output token"], + }, + { + name: "inputVault", + isMut: true, + isSigner: false, + docs: ["The vault token account for input token"], + }, + { + name: "outputVault", + isMut: true, + isSigner: false, + docs: ["The vault token account for output token"], + }, + { + name: "inputTokenProgram", + isMut: false, + isSigner: false, + docs: ["SPL program for input token transfers"], + }, + { + name: "outputTokenProgram", + isMut: false, + isSigner: false, + docs: ["SPL program for output token transfers"], + }, + { + name: "inputTokenMint", + isMut: false, + isSigner: false, + docs: ["The mint of input token"], + }, + { + name: "outputTokenMint", + isMut: false, + isSigner: false, + docs: ["The mint of output token"], + }, + { + name: "observationState", + isMut: true, + isSigner: false, + docs: ["The program account for the most recent oracle observation"], + }, + ], + args: [ + { + name: "maxAmountIn", + type: "u64", + }, + { + name: "amountOut", + type: "u64", + }, + ], + }, + ], + accounts: [ + { + name: "ammConfig", + docs: ["Holds the current owner of the factory"], + type: { + kind: "struct", + fields: [ + { + name: "bump", + docs: ["Bump to identify PDA"], + type: "u8", + }, + { + name: "disableCreatePool", + docs: ["Status to control if new pool can be create"], + type: "bool", + }, + { + name: "index", + docs: ["Config index"], + type: "u16", + }, + { + name: "tradeFeeRate", + docs: ["The trade fee, denominated in hundredths of a bip (10^-6)"], + type: "u64", + }, + { + name: "protocolFeeRate", + docs: ["The protocol fee"], + type: "u64", + }, + { + name: "fundFeeRate", + docs: ["The fund fee, denominated in hundredths of a bip (10^-6)"], + type: "u64", + }, + { + name: "createPoolFee", + docs: ["Fee for create a new pool"], + type: "u64", + }, + { + name: "protocolOwner", + docs: ["Address of the protocol fee owner"], + type: "publicKey", + }, + { + name: "fundOwner", + docs: ["Address of the fund fee owner"], + type: "publicKey", + }, + { + name: "padding", + docs: ["padding"], + type: { + array: ["u64", 16], + }, + }, + ], + }, + }, + { + name: "poolState", + type: { + kind: "struct", + fields: [ + { + name: "ammConfig", + docs: ["Which config the pool belongs"], + type: "publicKey", + }, + { + name: "poolCreator", + docs: ["pool creator"], + type: "publicKey", + }, + { + name: "token0Vault", + docs: ["Token A"], + type: "publicKey", + }, + { + name: "token1Vault", + docs: ["Token B"], + type: "publicKey", + }, + { + name: "lpMint", + docs: [ + "Pool tokens are issued when A or B tokens are deposited.", + "Pool tokens can be withdrawn back to the original A or B token.", + ], + type: "publicKey", + }, + { + name: "token0Mint", + docs: ["Mint information for token A"], + type: "publicKey", + }, + { + name: "token1Mint", + docs: ["Mint information for token B"], + type: "publicKey", + }, + { + name: "token0Program", + docs: ["token_0 program"], + type: "publicKey", + }, + { + name: "token1Program", + docs: ["token_1 program"], + type: "publicKey", + }, + { + name: "observationKey", + docs: ["observation account to store oracle data"], + type: "publicKey", + }, + { + name: "authBump", + type: "u8", + }, + { + name: "status", + docs: [ + "Bitwise representation of the state of the pool", + "bit0, 1: disable deposit(vaule is 1), 0: normal", + "bit1, 1: disable withdraw(vaule is 2), 0: normal", + "bit2, 1: disable swap(vaule is 4), 0: normal", + ], + type: "u8", + }, + { + name: "lpMintDecimals", + type: "u8", + }, + { + name: "mint0Decimals", + docs: ["mint0 and mint1 decimals"], + type: "u8", + }, + { + name: "mint1Decimals", + type: "u8", + }, + { + name: "lpSupply", + docs: ["lp mint supply"], + type: "u64", + }, + { + name: "protocolFeesToken0", + docs: [ + "The amounts of token_0 and token_1 that are owed to the liquidity provider.", + ], + type: "u64", + }, + { + name: "protocolFeesToken1", + type: "u64", + }, + { + name: "fundFeesToken0", + type: "u64", + }, + { + name: "fundFeesToken1", + type: "u64", + }, + { + name: "openTime", + docs: ["The timestamp allowed for swap in the pool."], + type: "u64", + }, + { + name: "padding", + docs: ["padding for future updates"], + type: { + array: ["u64", 32], + }, + }, + ], + }, + }, + { + name: "observationState", + type: { + kind: "struct", + fields: [ + { + name: "initialized", + docs: ["Whether the ObservationState is initialized"], + type: "bool", + }, + { + name: "observationIndex", + docs: ["the most-recently updated index of the observations array"], + type: "u16", + }, + { + name: "poolId", + type: "publicKey", + }, + { + name: "observations", + docs: ["observation array"], + type: { + array: [ + { + defined: "Observation", + }, + 100, + ], + }, + }, + { + name: "padding", + docs: ["padding for feature update"], + type: { + array: ["u64", 4], + }, + }, + ], + }, + }, + ], + types: [ + { + name: "Observation", + docs: ["The element of observations in ObservationState"], + type: { + kind: "struct", + fields: [ + { + name: "blockTimestamp", + docs: ["The block timestamp of the observation"], + type: "u64", + }, + { + name: "cumulativeToken0PriceX32", + docs: [ + "the cumulative of token0 price during the duration time, Q32.32, the remaining 64 bit for overflow", + ], + type: "u128", + }, + { + name: "cumulativeToken1PriceX32", + docs: [ + "the cumulative of token1 price during the duration time, Q32.32, the remaining 64 bit for overflow", + ], + type: "u128", + }, + ], + }, + }, + { + name: "PoolStatusBitIndex", + type: { + kind: "enum", + variants: [ + { + name: "Deposit", + }, + { + name: "Withdraw", + }, + { + name: "Swap", + }, + ], + }, + }, + { + name: "PoolStatusBitFlag", + type: { + kind: "enum", + variants: [ + { + name: "Enable", + }, + { + name: "Disable", + }, + ], + }, + }, + ], +}; diff --git a/tests/integration/fullLaunch.test.ts b/tests/integration/fullLaunch.test.ts index 93e91ddc3..942d61636 100644 --- a/tests/integration/fullLaunch.test.ts +++ b/tests/integration/fullLaunch.test.ts @@ -1,232 +1,225 @@ -import { Keypair, PublicKey, ComputeBudgetProgram, Transaction } from "@solana/web3.js"; +import { + Keypair, + PublicKey, + ComputeBudgetProgram, + Transaction, +} from "@solana/web3.js"; import { assert } from "chai"; import { - AutocratClient, - LaunchpadClient, - getFundingRecordAddr, - getLaunchAddr, - getLaunchSignerAddr, - MAINNET_USDC, - PriceMath, + AutocratClient, + LaunchpadClient, + getFundingRecordAddr, + getLaunchAddr, + getLaunchSignerAddr, + MAINNET_USDC, + PriceMath, } from "@metadaoproject/futarchy/v0.4"; import { BN } from "bn.js"; import { - getAssociatedTokenAddressSync, - createAssociatedTokenAccount, - getAccount, + getAssociatedTokenAddressSync, + createAssociatedTokenAccount, + getAccount, } from "@solana/spl-token"; import { initializeMintWithSeeds } from "../launchpad/utils.js"; import * as token from "@solana/spl-token"; export default async function suite() { - // Create multiple funders - const funder1 = Keypair.generate(); - const funder2 = Keypair.generate(); - const funder3 = Keypair.generate(); - - let META: PublicKey; - let launch: PublicKey; - let launchSigner: PublicKey; - let dao: PublicKey; - let daoTreasury: PublicKey; - - const minRaise = new BN(1000_000000); // 1000 USDC - const launchPeriod = 60 * 60 * 24 * 2; // 2 days - - // Initialize the launch - const result = await initializeMintWithSeeds( - this.banksClient, - this.launchpadClient, - this.payer - ); - - META = result.tokenMint; - launch = result.launch; - launchSigner = result.launchSigner; - - // Setup token accounts for funders - await this.createTokenAccount(MAINNET_USDC, funder1.publicKey); - await this.createTokenAccount(MAINNET_USDC, funder2.publicKey); - await this.createTokenAccount(MAINNET_USDC, funder3.publicKey); - - - // Mint USDC to funders - await this.transfer(MAINNET_USDC, this.payer, funder1.publicKey, 5000_000000); - await this.transfer(MAINNET_USDC, this.payer, funder2.publicKey, 3000_000000); - await this.transfer(MAINNET_USDC, this.payer, funder3.publicKey, 4000_000000); - - // Initialize launch - await this.launchpadClient - .initializeLaunchIx( - "META", - "META", - "https://example.com", - minRaise, - launchPeriod, - META - ) - .rpc(); - - // Start launch - await this.launchpadClient.startLaunchIx(launch).rpc(); - - // Fund from multiple sources - await this.launchpadClient - .fundIx(launch, new BN(5000_000000), funder1.publicKey) - .signers([funder1]) - .rpc(); - - await this.launchpadClient - .fundIx(launch, new BN(1500_000000)) - .rpc(); - - await this.launchpadClient - .fundIx(launch, new BN(3500_000000), funder3.publicKey) - .signers([funder3]) - .rpc(); - - // Advance time and complete launch - await this.advanceBySeconds(launchPeriod + 3600); - - await this.launchpadClient - .completeLaunchIx(launch, META) - .preInstructions([ - ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }), - ]) - .rpc(); - - // Verify launch completion and DAO creation - const launchAccount = await this.launchpadClient.fetchLaunch(launch); - assert.exists(launchAccount.state.complete); - assert.exists(launchAccount.dao); - dao = launchAccount.dao; - daoTreasury = launchAccount.daoTreasury; - - - // Claim tokens for all funders - await this.launchpadClient - .claimIx(launch, META, funder1.publicKey) - .rpc(); - - await this.launchpadClient - .claimIx(launch, META) - .rpc(); - - await this.launchpadClient - .claimIx(launch, META, funder3.publicKey) - .rpc(); - - // Verify token distributions - const funder1Balance = await this.getTokenBalance(META, funder1.publicKey); - const payerBalance = await this.getTokenBalance(META, this.payer.publicKey); - const funder3Balance = await this.getTokenBalance(META, funder3.publicKey); - - assert.equal(funder1Balance.toString(), "5000000000000"); // 5M tokens - assert.equal(payerBalance.toString(), "1500000000000"); // 1.5M tokens - assert.equal(funder3Balance.toString(), "3500000000000"); // 3.5M tokens - - - - - // Create proposal to mint tokens - const mintAmount = new BN(1_000_000_000000); // 1M tokens - const receiver = Keypair.generate(); - const receiverAccount = await this.createTokenAccount(META, receiver.publicKey); - - const mintToIx = token.createMintToInstruction( - META, - receiverAccount, - daoTreasury, - mintAmount.toNumber() - ); - - const instruction = { - programId: mintToIx.programId, - data: mintToIx.data, - accounts: mintToIx.keys, - }; - - // Needs to be 1% of supply - // and 1% of USDC raised - - const proposal = await this.autocratClient.initializeProposal( - dao, - "Mint 1M tokens to receiver", - instruction, - PriceMath.getChainAmount(100_000, 6), // 100k tokens - PriceMath.getChainAmount(100, 6) // 100 USDC - ); - - let { - passAmm, - failAmm, - passBaseMint, - passQuoteMint, - failBaseMint, - failQuoteMint, - baseVault, - quoteVault, - passLp, - failLp, - question, - } = this.autocratClient.getProposalPdas(proposal, META, MAINNET_USDC, dao); - - await this.vaultClient - .splitTokensIx(question, baseVault, META, new BN(10 * 10 ** 9), 2) - .rpc(); - await this.vaultClient - .splitTokensIx( - question, - quoteVault, - MAINNET_USDC, - new BN(10_000 * 1_000_000), - 2 - ) - .rpc(); - - // swap $500 in the pass market, make it pass - await this.ammClient - .swapIx( - passAmm, - passBaseMint, - passQuoteMint, - { buy: {} }, - new BN(500).muln(1_000_000), - new BN(0) - ) - .rpc(); - + // Create multiple funders + const funder1 = Keypair.generate(); + const funder2 = Keypair.generate(); + const funder3 = Keypair.generate(); + + let META: PublicKey; + let launch: PublicKey; + let launchSigner: PublicKey; + let dao: PublicKey; + let daoTreasury: PublicKey; + + const minRaise = new BN(1000_000000); // 1000 USDC + const launchPeriod = 60 * 60 * 24 * 2; // 2 days + + // Initialize the launch + const result = await initializeMintWithSeeds( + this.banksClient, + this.launchpadClient, + this.payer + ); + + META = result.tokenMint; + launch = result.launch; + launchSigner = result.launchSigner; + + // Setup token accounts for funders + await this.createTokenAccount(MAINNET_USDC, funder1.publicKey); + await this.createTokenAccount(MAINNET_USDC, funder2.publicKey); + await this.createTokenAccount(MAINNET_USDC, funder3.publicKey); + + // Mint USDC to funders + await this.transfer(MAINNET_USDC, this.payer, funder1.publicKey, 5000_000000); + await this.transfer(MAINNET_USDC, this.payer, funder2.publicKey, 3000_000000); + await this.transfer(MAINNET_USDC, this.payer, funder3.publicKey, 4000_000000); + + // Initialize launch + await this.launchpadClient + .initializeLaunchIx( + "META", + "META", + "https://example.com", + minRaise, + launchPeriod, + META + ) + .rpc(); + + // Start launch + await this.launchpadClient.startLaunchIx(launch).rpc(); + + // Fund from multiple sources + await this.launchpadClient + .fundIx(launch, new BN(5000_000000), funder1.publicKey) + .signers([funder1]) + .rpc(); + + await this.launchpadClient.fundIx(launch, new BN(1500_000000)).rpc(); + + await this.launchpadClient + .fundIx(launch, new BN(3500_000000), funder3.publicKey) + .signers([funder3]) + .rpc(); + + // Advance time and complete launch + await this.advanceBySeconds(launchPeriod + 3600); + + await this.launchpadClient + .completeLaunchIx(launch, META) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }), + ]) + .rpc(); + + // Verify launch completion and DAO creation + const launchAccount = await this.launchpadClient.fetchLaunch(launch); + assert.exists(launchAccount.state.complete); + assert.exists(launchAccount.dao); + dao = launchAccount.dao; + daoTreasury = launchAccount.daoTreasury; + + // Claim tokens for all funders + await this.launchpadClient.claimIx(launch, META, funder1.publicKey).rpc(); + + await this.launchpadClient.claimIx(launch, META).rpc(); + + await this.launchpadClient.claimIx(launch, META, funder3.publicKey).rpc(); + + // Verify token distributions + const funder1Balance = await this.getTokenBalance(META, funder1.publicKey); + const payerBalance = await this.getTokenBalance(META, this.payer.publicKey); + const funder3Balance = await this.getTokenBalance(META, funder3.publicKey); + + assert.equal(funder1Balance.toString(), "5000000000000"); // 5M tokens + assert.equal(payerBalance.toString(), "1500000000000"); // 1.5M tokens + assert.equal(funder3Balance.toString(), "3500000000000"); // 3.5M tokens + + // Create proposal to mint tokens + const mintAmount = new BN(1_000_000_000000); // 1M tokens + const receiver = Keypair.generate(); + const receiverAccount = await this.createTokenAccount( + META, + receiver.publicKey + ); + + const mintToIx = token.createMintToInstruction( + META, + receiverAccount, + daoTreasury, + mintAmount.toNumber() + ); + + const instruction = { + programId: mintToIx.programId, + data: mintToIx.data, + accounts: mintToIx.keys, + }; + + // Needs to be 1% of supply + // and 1% of USDC raised + + const proposal = await this.autocratClient.initializeProposal( + dao, + "Mint 1M tokens to receiver", + instruction, + PriceMath.getChainAmount(100_000, 6), // 100k tokens + PriceMath.getChainAmount(100, 6) // 100 USDC + ); + + let { + passAmm, + failAmm, + passBaseMint, + passQuoteMint, + failBaseMint, + failQuoteMint, + baseVault, + quoteVault, + passLp, + failLp, + question, + } = this.autocratClient.getProposalPdas(proposal, META, MAINNET_USDC, dao); + + await this.vaultClient + .splitTokensIx(question, baseVault, META, new BN(10 * 10 ** 9), 2) + .rpc(); + await this.vaultClient + .splitTokensIx( + question, + quoteVault, + MAINNET_USDC, + new BN(10_000 * 1_000_000), + 2 + ) + .rpc(); + + // swap $500 in the pass market, make it pass + await this.ammClient + .swapIx( + passAmm, + passBaseMint, + passQuoteMint, + { buy: {} }, + new BN(500).muln(1_000_000), + new BN(0) + ) + .rpc(); + + for (let i = 0; i < 100; i++) { + await this.advanceBySlots(10_000n); - for (let i = 0; i < 100; i++) { - await this.advanceBySlots(10_000n); - - await this.ammClient - .crankThatTwapIx(passAmm) - .postInstructions([ - // this is to get around bankrun thinking we've processed the same transaction multiple times - ComputeBudgetProgram.setComputeUnitPrice({ - microLamports: i, - }), - await this.ammClient.crankThatTwapIx(failAmm).instruction(), - ]) - .signers([this.payer]) - .rpc({ skipPreflight: true }); - - } + await this.ammClient + .crankThatTwapIx(passAmm) + .postInstructions([ + // this is to get around bankrun thinking we've processed the same transaction multiple times + ComputeBudgetProgram.setComputeUnitPrice({ + microLamports: i, + }), + await this.ammClient.crankThatTwapIx(failAmm).instruction(), + ]) + .signers([this.payer]) + .rpc({ skipPreflight: true }); + } - await this.autocratClient.finalizeProposal(proposal); + await this.autocratClient.finalizeProposal(proposal); - const storedProposal = await this.autocratClient.getProposal(proposal); + const storedProposal = await this.autocratClient.getProposal(proposal); - assert.exists(storedProposal.state.passed); + assert.exists(storedProposal.state.passed); - await this.autocratClient.executeProposal(proposal); + await this.autocratClient.executeProposal(proposal); - const storedMeta = await this.getMint(META); + const storedMeta = await this.getMint(META); - assert.equal(storedMeta.supply, 12_000_000 * 10 ** 6); + assert.equal(storedMeta.supply, 12_000_000 * 10 ** 6); - const receiverBalance = await this.getTokenBalance(META, receiver.publicKey); + const receiverBalance = await this.getTokenBalance(META, receiver.publicKey); - assert.equal(receiverBalance.toString(), "1000000000000"); + assert.equal(receiverBalance.toString(), "1000000000000"); } diff --git a/tests/integration/mintAndSwap.test.ts b/tests/integration/mintAndSwap.test.ts index a250f3578..42610f9fb 100644 --- a/tests/integration/mintAndSwap.test.ts +++ b/tests/integration/mintAndSwap.test.ts @@ -42,13 +42,7 @@ export default async function test() { // Initialize AMM await ammClient - .initializeAmmIx( - YES, - NO, - new BN(0), - new BN(100), - new BN(1000) - ) + .initializeAmmIx(YES, NO, new BN(0), new BN(100), new BN(1000)) .rpc(); const amm = getAmmAddr(ammClient.getProgramId(), YES, NO)[0]; diff --git a/tests/integration/scalarMarkets.test.ts b/tests/integration/scalarMarkets.test.ts index 2cd38aa86..8f13cb87c 100644 --- a/tests/integration/scalarMarkets.test.ts +++ b/tests/integration/scalarMarkets.test.ts @@ -10,9 +10,7 @@ import { Keypair, PublicKey, Transaction } from "@solana/web3.js"; import BN from "bn.js"; import { assert } from "chai"; import * as token from "@solana/spl-token"; -import { - getAccount, -} from "spl-token-bankrun"; +import { getAccount } from "spl-token-bankrun"; export default async function test() { let vaultClient: ConditionalVaultClient = this.vaultClient; @@ -88,7 +86,9 @@ export default async function test() { const NO = storedVault.conditionalTokenMints[1]; // Initialize AMM - await ammClient.initializeAmmIx(YES, NO, new BN(0), new BN(100), new BN(1000)).rpc(); + await ammClient + .initializeAmmIx(YES, NO, new BN(0), new BN(100), new BN(1000)) + .rpc(); const amm = getAmmAddr(ammClient.getProgramId(), YES, NO)[0]; // Create token accounts for Alice, Bob, Carol, and Dan diff --git a/tests/integration/twap.test.ts b/tests/integration/twap.test.ts index 4aeb07671..3bb1b6992 100644 --- a/tests/integration/twap.test.ts +++ b/tests/integration/twap.test.ts @@ -4,110 +4,112 @@ import { advanceBySlots, DAY_IN_SLOTS, toBN } from "../utils.js"; import { AmmMath } from "@metadaoproject/futarchy/v0.4"; export default async function test() { - // Create META and USDC mints - const META = await this.createMint(this.payer.publicKey, 9); - const USDC = await this.createMint(this.payer.publicKey, 6); - - // Create token accounts and mint tokens - await this.createTokenAccount(META, this.payer.publicKey); - await this.createTokenAccount(USDC, this.payer.publicKey); - - await this.mintTo(META, this.payer.publicKey, this.payer, 100 * 10 ** 9); - await this.mintTo(USDC, this.payer.publicKey, this.payer, 10_000 * 10 ** 6); - - // Create AMM with TWAP parameters - const twapStartDelaySlots = DAY_IN_SLOTS; - const twapInitialObservation = 500; - const proposal = Keypair.generate().publicKey; - const amm = await this.ammClient.createAmm( - proposal, - META, - USDC, - toBN(twapStartDelaySlots), - twapInitialObservation - ); - - // Add initial liquidity - await this.ammClient.addLiquidity(amm, 500, 1); - - // Check initial AMM state - const initialAmm = await this.ammClient.getAmm(amm); - const initialLastUpdatedSlot = initialAmm.oracle.lastUpdatedSlot; - - // Try to crank before delay - should remain at initial state - await this.ammClient - .crankThatTwapIx(amm) - .preInstructions([ - ComputeBudgetProgram.setComputeUnitLimit({ - units: 100_000, - }), - ]) - .rpc(); - - const ammBeforeDelay = await this.ammClient.getAmm(amm); - assert.isTrue( - ammBeforeDelay.oracle.lastUpdatedSlot.eq(initialLastUpdatedSlot), - "Should not update lastUpdatedSlot before delay" - ); - - // Advance slots to get past the delay period - await advanceBySlots(this.context, twapStartDelaySlots); - - // Crank the TWAP - should update now - await this.ammClient - .crankThatTwapIx(amm) - .preInstructions([ - ComputeBudgetProgram.setComputeUnitLimit({ - units: 100_001, - }), - ]) - .rpc(); - - // Verify TWAP updated - const ammAfterDelay = await this.ammClient.getAmm(amm); - assert.isTrue( - ammAfterDelay.oracle.lastUpdatedSlot.gt(initialLastUpdatedSlot), - "TWAP should be updated after delay slots" - ); - - // Get initial TWAP value - const initialTwap = AmmMath.getTwap(ammAfterDelay); - - // Perform some swaps - await this.ammClient.swap(amm, { buy: {} }, 200, 0.2); - await this.ammClient.swap(amm, { sell: {} }, 0.2, 100); - - // Advance slots and crank again - await advanceBySlots(this.context, 150n); - await this.ammClient - .crankThatTwapIx(amm) - .preInstructions([ - ComputeBudgetProgram.setComputeUnitLimit({ - units: 100_002, - }), - ]) - .rpc(); - - // Verify TWAP updated - const ammAfterSwap = await this.ammClient.getAmm(amm); - assert.isTrue( - ammAfterSwap.oracle.lastUpdatedSlot.gt(ammAfterDelay.oracle.lastUpdatedSlot), - "TWAP should update after swap and crank" - ); - - // Verify TWAP value changed after swaps - const finalTwap = AmmMath.getTwap(ammAfterSwap); - assert.isTrue( - !finalTwap.eq(initialTwap), - "TWAP value should change after swaps and crank" - ); - - // Verify that the TWAP is calculated correctly - const expectedTwap = ammAfterSwap.oracle.aggregator.div( - ammAfterSwap.oracle.lastUpdatedSlot.sub(ammAfterSwap.createdAtSlot) - ); - assert.isTrue( - finalTwap.eq(expectedTwap), - "Calculated TWAP should match the expected value" - ); -} \ No newline at end of file + // Create META and USDC mints + const META = await this.createMint(this.payer.publicKey, 9); + const USDC = await this.createMint(this.payer.publicKey, 6); + + // Create token accounts and mint tokens + await this.createTokenAccount(META, this.payer.publicKey); + await this.createTokenAccount(USDC, this.payer.publicKey); + + await this.mintTo(META, this.payer.publicKey, this.payer, 100 * 10 ** 9); + await this.mintTo(USDC, this.payer.publicKey, this.payer, 10_000 * 10 ** 6); + + // Create AMM with TWAP parameters + const twapStartDelaySlots = DAY_IN_SLOTS; + const twapInitialObservation = 500; + const proposal = Keypair.generate().publicKey; + const amm = await this.ammClient.createAmm( + proposal, + META, + USDC, + toBN(twapStartDelaySlots), + twapInitialObservation + ); + + // Add initial liquidity + await this.ammClient.addLiquidity(amm, 500, 1); + + // Check initial AMM state + const initialAmm = await this.ammClient.getAmm(amm); + const initialLastUpdatedSlot = initialAmm.oracle.lastUpdatedSlot; + + // Try to crank before delay - should remain at initial state + await this.ammClient + .crankThatTwapIx(amm) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ + units: 100_000, + }), + ]) + .rpc(); + + const ammBeforeDelay = await this.ammClient.getAmm(amm); + assert.isTrue( + ammBeforeDelay.oracle.lastUpdatedSlot.eq(initialLastUpdatedSlot), + "Should not update lastUpdatedSlot before delay" + ); + + // Advance slots to get past the delay period + await advanceBySlots(this.context, twapStartDelaySlots); + + // Crank the TWAP - should update now + await this.ammClient + .crankThatTwapIx(amm) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ + units: 100_001, + }), + ]) + .rpc(); + + // Verify TWAP updated + const ammAfterDelay = await this.ammClient.getAmm(amm); + assert.isTrue( + ammAfterDelay.oracle.lastUpdatedSlot.gt(initialLastUpdatedSlot), + "TWAP should be updated after delay slots" + ); + + // Get initial TWAP value + const initialTwap = AmmMath.getTwap(ammAfterDelay); + + // Perform some swaps + await this.ammClient.swap(amm, { buy: {} }, 200, 0.2); + await this.ammClient.swap(amm, { sell: {} }, 0.2, 100); + + // Advance slots and crank again + await advanceBySlots(this.context, 150n); + await this.ammClient + .crankThatTwapIx(amm) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ + units: 100_002, + }), + ]) + .rpc(); + + // Verify TWAP updated + const ammAfterSwap = await this.ammClient.getAmm(amm); + assert.isTrue( + ammAfterSwap.oracle.lastUpdatedSlot.gt( + ammAfterDelay.oracle.lastUpdatedSlot + ), + "TWAP should update after swap and crank" + ); + + // Verify TWAP value changed after swaps + const finalTwap = AmmMath.getTwap(ammAfterSwap); + assert.isTrue( + !finalTwap.eq(initialTwap), + "TWAP value should change after swaps and crank" + ); + + // Verify that the TWAP is calculated correctly + const expectedTwap = ammAfterSwap.oracle.aggregator.div( + ammAfterSwap.oracle.lastUpdatedSlot.sub(ammAfterSwap.createdAtSlot) + ); + assert.isTrue( + finalTwap.eq(expectedTwap), + "Calculated TWAP should match the expected value" + ); +} diff --git a/tests/launchpad/unit/claim.test.ts b/tests/launchpad/unit/claim.test.ts index 72890b39a..dca149b18 100644 --- a/tests/launchpad/unit/claim.test.ts +++ b/tests/launchpad/unit/claim.test.ts @@ -43,7 +43,10 @@ export default function suite() { launch = result.launch; launchSigner = result.launchSigner; usdcVault = getAssociatedTokenAddressSync(MAINNET_USDC, launchSigner, true); - funderUsdcAccount = getAssociatedTokenAddressSync(MAINNET_USDC, this.payer.publicKey); + funderUsdcAccount = getAssociatedTokenAddressSync( + MAINNET_USDC, + this.payer.publicKey + ); // Initialize launch await launchpadClient diff --git a/tests/launchpad/unit/fund.test.ts b/tests/launchpad/unit/fund.test.ts index a53a7af6c..20f40cea5 100644 --- a/tests/launchpad/unit/fund.test.ts +++ b/tests/launchpad/unit/fund.test.ts @@ -49,8 +49,14 @@ export default function suite() { tokenVault = getAssociatedTokenAddressSync(META, launchSigner, true); usdcVault = getAssociatedTokenAddressSync(MAINNET_USDC, launchSigner, true); - funderTokenAccount = getAssociatedTokenAddressSync(META, this.payer.publicKey); - funderUsdcAccount = getAssociatedTokenAddressSync(MAINNET_USDC, this.payer.publicKey); + funderTokenAccount = getAssociatedTokenAddressSync( + META, + this.payer.publicKey + ); + funderUsdcAccount = getAssociatedTokenAddressSync( + MAINNET_USDC, + this.payer.publicKey + ); // Initialize launch await launchpadClient diff --git a/tests/launchpad/unit/initializeLaunch.test.ts b/tests/launchpad/unit/initializeLaunch.test.ts index ebf6243fb..610499c5f 100644 --- a/tests/launchpad/unit/initializeLaunch.test.ts +++ b/tests/launchpad/unit/initializeLaunch.test.ts @@ -1,4 +1,9 @@ -import { PublicKey, Keypair, SystemProgram, Transaction } from "@solana/web3.js"; +import { + PublicKey, + Keypair, + SystemProgram, + Transaction, +} from "@solana/web3.js"; import { assert } from "chai"; import { AutocratClient, @@ -39,7 +44,7 @@ export default function suite() { this.launchpadClient, this.payer ); - + META = result.tokenMint; launch = result.launch; launchSigner = result.launchSigner; @@ -120,7 +125,12 @@ export default function suite() { space: token.MINT_SIZE, programId: token.TOKEN_PROGRAM_ID, }), - token.createInitializeMint2Instruction(META, 6, fakeLaunchSigner.publicKey, null) + token.createInitializeMint2Instruction( + META, + 6, + fakeLaunchSigner.publicKey, + null + ) ); tx.recentBlockhash = (await this.banksClient.getLatestBlockhash())[0]; tx.feePayer = this.payer.publicKey; diff --git a/tests/launchpad/unit/refund.test.ts b/tests/launchpad/unit/refund.test.ts index e52bc1142..16cf47ca8 100644 --- a/tests/launchpad/unit/refund.test.ts +++ b/tests/launchpad/unit/refund.test.ts @@ -44,7 +44,10 @@ export default function suite() { launch = result.launch; launchSigner = result.launchSigner; usdcVault = getAssociatedTokenAddressSync(MAINNET_USDC, launchSigner, true); - funderUsdcAccount = getAssociatedTokenAddressSync(MAINNET_USDC, this.payer.publicKey); + funderUsdcAccount = getAssociatedTokenAddressSync( + MAINNET_USDC, + this.payer.publicKey + ); // Initialize launch await launchpadClient diff --git a/tests/launchpad/utils.ts b/tests/launchpad/utils.ts index 4bb5ba4ac..cf4f25a2d 100644 --- a/tests/launchpad/utils.ts +++ b/tests/launchpad/utils.ts @@ -1,14 +1,21 @@ -import { PublicKey, Signer, SystemProgram, Transaction } from '@solana/web3.js'; -import * as token from '@solana/spl-token'; -import { BanksClient } from 'solana-bankrun'; -import { LaunchpadClient } from '@metadaoproject/futarchy/v0.4'; -import { getLaunchAddr, getLaunchSignerAddr } from '@metadaoproject/futarchy/v0.4'; +import { PublicKey, Signer, SystemProgram, Transaction } from "@solana/web3.js"; +import * as token from "@solana/spl-token"; +import { BanksClient } from "solana-bankrun"; +import { LaunchpadClient } from "@metadaoproject/futarchy/v0.4"; +import { + getLaunchAddr, + getLaunchSignerAddr, +} from "@metadaoproject/futarchy/v0.4"; export async function initializeMintWithSeeds( banksClient: BanksClient, launchpadClient: LaunchpadClient, payer: Signer -): Promise<{ tokenMint: PublicKey, launch: PublicKey, launchSigner: PublicKey }> { +): Promise<{ + tokenMint: PublicKey; + launch: PublicKey; + launchSigner: PublicKey; +}> { const seed = Math.random().toString(36).substring(2, 15); const tokenMint = await PublicKey.createWithSeed( payer.publicKey, @@ -46,6 +53,6 @@ export async function initializeMintWithSeeds( return { tokenMint, launch, - launchSigner + launchSigner, }; } diff --git a/tests/main.test.ts b/tests/main.test.ts index ea9f8ebdd..989eeaf99 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -2,8 +2,9 @@ import conditionalVault from "./conditionalVault/main.test.js"; import amm from "./amm/main.test.js"; import autocrat from "./autocrat/autocrat.js"; import launchpad from "./launchpad/main.test.js"; +import sharedLiquidityManager from "./sharedLiquidityManager/main.test.js"; -import { Clock, startAnchor } from "solana-bankrun"; +import { BanksClient, Clock, startAnchor } from "solana-bankrun"; import { BankrunProvider } from "anchor-bankrun"; import * as anchor from "@coral-xyz/anchor"; import { @@ -11,6 +12,7 @@ import { AutocratClient, ConditionalVaultClient, LaunchpadClient, + SharedLiquidityManagerClient, MAINNET_USDC, RAYDIUM_CREATE_POOL_FEE_RECEIVE, } from "@metadaoproject/futarchy/v0.4"; @@ -50,6 +52,49 @@ import scalarMarkets from "./integration/scalarMarkets.test.js"; import twap from "./integration/twap.test.js"; import fullLaunch from "./integration/fullLaunch.test.js"; +// Extend the Mocha context to include our test properties +declare module "mocha" { + interface Context { + context: any; + banksClient: BanksClient; + vaultClient: ConditionalVaultClient; + autocratClient: AutocratClient; + launchpadClient: LaunchpadClient; + ammClient: AmmClient; + sharedLiquidityManagerClient: SharedLiquidityManagerClient; + payer: Keypair; + createTokenAccount: ( + mint: PublicKey, + owner: PublicKey + ) => Promise; + createMint: ( + mintAuthority: PublicKey, + decimals: number + ) => Promise; + mintTo: ( + mint: PublicKey, + to: PublicKey, + mintAuthority: Keypair, + amount: number + ) => Promise; + getTokenBalance: (mint: PublicKey, owner: PublicKey) => Promise; + getMint: (mint: PublicKey) => Promise; + assertBalance: ( + mint: PublicKey, + owner: PublicKey, + amount: number + ) => Promise; + transfer: ( + mint: PublicKey, + from: Keypair, + to: PublicKey, + amount: number + ) => Promise; + advanceBySlots: (slots: bigint) => Promise; + advanceBySeconds: (seconds: number) => Promise; + } +} + before(async function () { // const version: VersionKey = "0.4"; // const { AmmClient, AutocratClient, ConditionalVaultClient } = getVersion(version); @@ -115,7 +160,11 @@ before(async function () { this.launchpadClient = LaunchpadClient.createClient({ provider: provider as any, }); + this.provider = provider; this.ammClient = AmmClient.createClient({ provider: provider as any }); + this.sharedLiquidityManagerClient = SharedLiquidityManagerClient.createClient( + { provider: provider as any } + ); this.payer = provider.wallet.payer; this.createTokenAccount = async (mint: PublicKey, owner: PublicKey) => { @@ -230,9 +279,13 @@ describe("conditional_vault", conditionalVault); describe("amm", amm); describe("autocrat", autocrat); describe("launchpad", launchpad); +describe("shared_liquidity_manager", sharedLiquidityManager); describe("project-wide integration tests", function () { it("mint and swap in a single transaction", mintAndSwap); - it("tests scalar markets (mint, split, swap, redeem) with some fuzzing", scalarMarkets); + it( + "tests scalar markets (mint, split, swap, redeem) with some fuzzing", + scalarMarkets + ); it("tests twap functionality (crankThatTwap, twapStartDelaySlots)", twap); it("full launch", fullLaunch); }); diff --git a/tests/sharedLiquidityManager/integration/sharedLiquidityManagerLifecycle.test.ts b/tests/sharedLiquidityManager/integration/sharedLiquidityManagerLifecycle.test.ts new file mode 100644 index 000000000..0cafd8fa6 --- /dev/null +++ b/tests/sharedLiquidityManager/integration/sharedLiquidityManagerLifecycle.test.ts @@ -0,0 +1,559 @@ +import { + AmmClient, + AutocratClient, + getAmmAddr, + getAmmLpMintAddr, + getLiquidityPoolAddr, + getRaydiumCpmmLpMintAddr, + getRaydiumCpmmObservationStateAddr, + getRaydiumCpmmPoolVaultAddr, + LOW_FEE_RAYDIUM_CONFIG, + RAYDIUM_AUTHORITY, + RAYDIUM_CP_SWAP_PROGRAM_ID, + RAYDIUM_CREATE_POOL_FEE_RECEIVE, + SharedLiquidityManagerClient, + getSharedLiquidityPoolAddr, + CONDITIONAL_VAULT_PROGRAM_ID, + AMM_PROGRAM_ID, + AUTOCRAT_PROGRAM_ID, + getProposalAddr, + ConditionalVaultClient, + InstructionUtils, + getDaoTreasuryAddr, + getEventAuthorityAddr, + getSharedLiquidityPoolSignerAddr, + getDraftProposalAddr, + getStakeRecordAddr, + getSpotPoolAddr, +} from "@metadaoproject/futarchy/v0.4"; +import { + AddressLookupTableAccount, + AddressLookupTableProgram, + ComputeBudgetProgram, + Keypair, + PublicKey, + Transaction, + TransactionMessage, + VersionedTransaction, +} from "@solana/web3.js"; +import { assert } from "chai"; +import { + createMint, + createAssociatedTokenAccount, + mintTo, + getAccount, + getMint, +} from "spl-token-bankrun"; +import * as anchor from "@coral-xyz/anchor"; +import * as token from "@solana/spl-token"; +import { DAY_IN_SLOTS, expectError, toBN } from "../../utils.js"; +import { BN } from "bn.js"; +import { IDL as RaydiumCpmmIdl } from "../../fixtures/raydium_cpmm.js"; +import { sha256 } from "@metadaoproject/futarchy"; +import { BanksClient } from "solana-bankrun"; + +export default async function () { + const ammClient: AmmClient = this.ammClient; + const autocratClient: AutocratClient = this.autocratClient; + const vaultClient: ConditionalVaultClient = this.vaultClient; + const sharedLiquidityManagerClient: SharedLiquidityManagerClient = + this.sharedLiquidityManagerClient; + const cpSwap = new anchor.Program( + RaydiumCpmmIdl, + new PublicKey(RAYDIUM_CP_SWAP_PROGRAM_ID), + this.provider + ); + + // First, set up tokens and a DAO + + const META = await createMint( + this.banksClient, + this.payer, + this.payer.publicKey, + this.payer.publicKey, + 9 + ); + const USDC = await createMint( + this.banksClient, + this.payer, + this.payer.publicKey, + this.payer.publicKey, + 6 + ); + + await this.createTokenAccount(META, this.payer.publicKey); + await this.createTokenAccount(USDC, this.payer.publicKey); + + await this.mintTo(META, this.payer.publicKey, this.payer, 100 * 10 ** 9); + await this.mintTo(USDC, this.payer.publicKey, this.payer, 100_000 * 10 ** 6); + + const dao = await autocratClient.initializeDao( + META, + 1000, + 10, + 10_000, + USDC, + undefined, + new BN(DAY_IN_SLOTS.toString()) + ); + + // Second, set up a shared liquidity pool + + await sharedLiquidityManagerClient + .initializeSharedLiquidityPoolIx( + dao, + META, + USDC, + new BN(25 * 10 ** 9), + new BN(25_000 * 10 ** 6) + ) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 }), + ComputeBudgetProgram.requestHeapFrame({ bytes: 256 * 1024 }), + ]) + .rpc(); + + const [slPool] = getSharedLiquidityPoolAddr( + sharedLiquidityManagerClient.getProgramId(), + dao, + this.payer.publicKey, + 100 + ); + + const [slPoolSigner] = getSharedLiquidityPoolSignerAddr( + sharedLiquidityManagerClient.getProgramId(), + slPool + ); + + let storedSlPool = + await sharedLiquidityManagerClient.program.account.sharedLiquidityPool.fetch( + slPool + ); + + // Third, initialize a draft proposal + + await sharedLiquidityManagerClient + .initializeDraftProposalIx( + slPool, + META, + { + programId: META, + accounts: [], + data: Buffer.from([]), + }, + new BN(1338) + ) + .rpc(); + + const [draftProposal] = getDraftProposalAddr( + sharedLiquidityManagerClient.getProgramId(), + new BN(1338) + ); + + let storedDraftProposal = + await sharedLiquidityManagerClient.program.account.draftProposal.fetch( + draftProposal + ); + assert.equal(storedDraftProposal.stakedTokenAmount.toString(), "0"); + + // Fourth, stake to the draft proposal + + await sharedLiquidityManagerClient + .stakeToDraftProposalIx(draftProposal, META, new BN(1_000_000_000)) + .rpc(); + + storedDraftProposal = + await sharedLiquidityManagerClient.program.account.draftProposal.fetch( + draftProposal + ); + + const [stakeRecord] = getStakeRecordAddr( + sharedLiquidityManagerClient.getProgramId(), + draftProposal, + this.payer.publicKey + ); + const storedStakeRecord = + await sharedLiquidityManagerClient.program.account.stakeRecord.fetch( + stakeRecord + ); + + assert.equal( + storedStakeRecord.staker.toString(), + this.payer.publicKey.toString() + ); + assert.equal(storedStakeRecord.amount.toString(), 1_000_000_000n.toString()); + assert.equal( + storedDraftProposal.stakedTokenAmount.toString(), + 1_000_000_000n.toString() + ); + + // Fifth, initialize a proposal with liquidity + + const nonce = new BN(12329); + + let [proposal] = getProposalAddr(AUTOCRAT_PROGRAM_ID, slPoolSigner, nonce); + + await vaultClient.initializeQuestion( + sha256(`Will ${proposal} pass?/FAIL/PASS`), + proposal, + 2 + ); + + const { + passAmm, + failAmm, + passBaseMint, + passQuoteMint, + failBaseMint, + failQuoteMint, + passLp, + failLp, + question, + } = autocratClient.getProposalPdas(proposal, META, USDC, dao); + + const storedDao = await autocratClient.fetchDao(dao); + + await vaultClient + .initializeVaultIx(question, META, 2) + .postInstructions( + await InstructionUtils.getInstructions( + vaultClient.initializeVaultIx(question, USDC, 2), + ammClient.initializeAmmIx( + passBaseMint, + passQuoteMint, + storedDao.twapStartDelaySlots, + storedDao.twapInitialObservation, + storedDao.twapMaxObservationChangePerUpdate + ), + ammClient.initializeAmmIx( + failBaseMint, + failQuoteMint, + storedDao.twapStartDelaySlots, + storedDao.twapInitialObservation, + storedDao.twapMaxObservationChangePerUpdate + ) + ) + ) + .rpc(); + + let initProposalWithLiquidityTx: Transaction = + await sharedLiquidityManagerClient + .initializeProposalWithLiquidityIx(dao, META, USDC, nonce, draftProposal) + .transaction(); + + const slot = await this.banksClient.getSlot(); + const [createTableIx, lookupTableAddress] = + AddressLookupTableProgram.createLookupTable({ + authority: this.payer.publicKey, + payer: this.payer.publicKey, + recentSlot: slot - 1n, + }); + + const accountsToAdd = initProposalWithLiquidityTx.instructions.map( + (instruction) => instruction.keys.map((key) => key.pubkey) + ); + const uniqueAccounts = [...new Set(accountsToAdd.flat())] as PublicKey[]; + + // Create the lookup table first + let createLutTx = new Transaction().add(createTableIx); + createLutTx.recentBlockhash = ( + await this.banksClient.getLatestBlockhash() + )[0]; + createLutTx.feePayer = this.payer.publicKey; + createLutTx.sign(this.payer); + + await this.banksClient.processTransaction(createLutTx); + + await this.advanceBySlots(1n); + + // Extend the lookup table with all unique accounts + // Raydium allows up to 20 addresses per extend instruction + const addressesPerExtend = 20; + for (let i = 0; i < uniqueAccounts.length; i += addressesPerExtend) { + const batch = uniqueAccounts.slice(i, i + addressesPerExtend); + + const extendTableIx = AddressLookupTableProgram.extendLookupTable({ + authority: this.payer.publicKey, + payer: this.payer.publicKey, + lookupTable: lookupTableAddress, + addresses: batch, + }); + + let extendLutTx = new Transaction().add(extendTableIx); + extendLutTx.recentBlockhash = ( + await this.banksClient.getLatestBlockhash() + )[0]; + extendLutTx.feePayer = this.payer.publicKey; + extendLutTx.sign(this.payer); + + await this.banksClient.processTransaction(extendLutTx); + await this.advanceBySlots(1n); + } + + console.log("UNIQUE ACCOUNTS", uniqueAccounts.length); + + // Create and process second extension transaction + const extendTableIx2 = AddressLookupTableProgram.extendLookupTable({ + authority: this.payer.publicKey, + payer: this.payer.publicKey, + lookupTable: lookupTableAddress, + addresses: [ComputeBudgetProgram.programId], + }); + + let lutTx2 = new Transaction().add(extendTableIx2); + lutTx2.recentBlockhash = (await this.banksClient.getLatestBlockhash())[0]; + lutTx2.feePayer = this.payer.publicKey; + lutTx2.sign(this.payer); + + await this.banksClient.processTransaction(lutTx2); + + await this.advanceBySlots(1n); + + let rawStoredLookupTable = await this.banksClient.getAccount( + lookupTableAddress + ); + + let storedLookupTable = new AddressLookupTableAccount({ + key: lookupTableAddress, + state: AddressLookupTableAccount.deserialize(rawStoredLookupTable.data), + }); + + const messageV0 = new TransactionMessage({ + payerKey: this.payer.publicKey, + recentBlockhash: (await this.banksClient.getLatestBlockhash())[0], + instructions: [ + ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }), + ComputeBudgetProgram.requestHeapFrame({ bytes: 256 * 1024 }), + ].concat(initProposalWithLiquidityTx.instructions), + }).compileToV0Message([storedLookupTable]); + + console.log("messageV0", messageV0); + + let tx = new VersionedTransaction(messageV0); + tx.sign([this.payer]); + + const [daoTreasury] = getDaoTreasuryAddr(AUTOCRAT_PROGRAM_ID, dao); + + await this.createTokenAccount(passLp, daoTreasury, true); + await this.createTokenAccount(failLp, daoTreasury, true); + + console.log("tx size", tx.serialize().length); + + await this.banksClient.processTransaction(tx); + + await this.advanceBySlots(DAY_IN_SLOTS); + + // Crank TWAPs multiple times to ensure markets are mature enough + // The markets need to have been updated for at least proposal.duration_in_slots + for (let i = 0; i < 50; i++) { + await this.advanceBySlots(20_000n); + + await ammClient + .crankThatTwapIx(passAmm) + .preInstructions([ + // Add compute unit price to avoid bankrun thinking we've processed the same transaction multiple times + ComputeBudgetProgram.setComputeUnitPrice({ + microLamports: i, + }), + await ammClient.crankThatTwapIx(failAmm).instruction(), + ]) + .rpc(); + } + + // Finalize the proposal with a pass outcome + await autocratClient.finalizeProposal(proposal); + + // Test unstaking from the draft proposal + const initialStakerBalance = ( + await getAccount( + this.banksClient, + token.getAssociatedTokenAddressSync(META, this.payer.publicKey) + ) + ).amount; + + await sharedLiquidityManagerClient + .unstakeFromDraftProposalIx(draftProposal, META, new BN(500_000_000)) + .rpc(); + + const updatedStakeRecord = + await sharedLiquidityManagerClient.program.account.stakeRecord.fetch( + stakeRecord + ); + const updatedDraftProposal = + await sharedLiquidityManagerClient.program.account.draftProposal.fetch( + draftProposal + ); + const finalStakerBalance = ( + await getAccount( + this.banksClient, + token.getAssociatedTokenAddressSync(META, this.payer.publicKey) + ) + ).amount; + + assert.equal(updatedStakeRecord.amount.toString(), 500_000_000n.toString()); + assert.equal( + updatedDraftProposal.stakedTokenAmount.toString(), + 500_000_000n.toString() + ); + assert.equal(finalStakerBalance, initialStakerBalance + 500_000_000n); + + // Remove proposal liquidity + let removeProposalLiquidityTx = await sharedLiquidityManagerClient + .removeProposalLiquidityIx( + dao, + storedSlPool.activeSpotPool, + META, + USDC, + nonce + ) + .transaction(); + + // Create a new lookup table for the remove liquidity transaction + const slot2 = await this.banksClient.getSlot(); + const [createTableIx2, lookupTableAddress2] = + AddressLookupTableProgram.createLookupTable({ + authority: this.payer.publicKey, + payer: this.payer.publicKey, + recentSlot: slot2 - 1n, + }); + + const removeAccountsToAdd = removeProposalLiquidityTx.instructions.map( + (instruction) => instruction.keys.map((key) => key.pubkey) + ); + const removeUniqueAccounts = [ + ...new Set(removeAccountsToAdd.flat()), + ] as PublicKey[]; + + // Create the lookup table first + let createLutTx2 = new Transaction().add(createTableIx2); + createLutTx2.recentBlockhash = ( + await this.banksClient.getLatestBlockhash() + )[0]; + createLutTx2.feePayer = this.payer.publicKey; + createLutTx2.sign(this.payer); + + await this.banksClient.processTransaction(createLutTx2); + + await this.advanceBySlots(1n); + + // Extend the lookup table with all unique accounts + // Raydium allows up to 20 addresses per extend instruction + const addressesPerExtend2 = 20; + for (let i = 0; i < removeUniqueAccounts.length; i += addressesPerExtend2) { + const batch = removeUniqueAccounts.slice(i, i + addressesPerExtend2); + + const extendTableIx = AddressLookupTableProgram.extendLookupTable({ + authority: this.payer.publicKey, + payer: this.payer.publicKey, + lookupTable: lookupTableAddress2, + addresses: batch, + }); + + let extendLutTx = new Transaction().add(extendTableIx); + extendLutTx.recentBlockhash = ( + await this.banksClient.getLatestBlockhash() + )[0]; + extendLutTx.feePayer = this.payer.publicKey; + extendLutTx.sign(this.payer); + + await this.banksClient.processTransaction(extendLutTx); + await this.advanceBySlots(1n); + } + + console.log("REMOVE UNIQUE ACCOUNTS", removeUniqueAccounts.length); + + // Create and process second extension transaction for ComputeBudgetProgram + const extendTableIx3 = AddressLookupTableProgram.extendLookupTable({ + authority: this.payer.publicKey, + payer: this.payer.publicKey, + lookupTable: lookupTableAddress2, + addresses: [ComputeBudgetProgram.programId], + }); + + let lutTx3 = new Transaction().add(extendTableIx3); + lutTx3.recentBlockhash = (await this.banksClient.getLatestBlockhash())[0]; + lutTx3.feePayer = this.payer.publicKey; + lutTx3.sign(this.payer); + + await this.banksClient.processTransaction(lutTx3); + + await this.advanceBySlots(1n); + + let rawStoredLookupTable2 = await this.banksClient.getAccount( + lookupTableAddress2 + ); + + let storedLookupTable2 = new AddressLookupTableAccount({ + key: lookupTableAddress2, + state: AddressLookupTableAccount.deserialize(rawStoredLookupTable2.data), + }); + + const messageV0Remove = new TransactionMessage({ + payerKey: this.payer.publicKey, + recentBlockhash: (await this.banksClient.getLatestBlockhash())[0], + instructions: [ + ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }), + ComputeBudgetProgram.requestHeapFrame({ bytes: 256 * 1024 }), + ].concat(removeProposalLiquidityTx.instructions), + }).compileToV0Message([storedLookupTable2]); + + let removeTx = new VersionedTransaction(messageV0Remove); + removeTx.sign([this.payer]); + console.log("removeTx size", removeTx.serialize().length); + await this.banksClient.processTransaction(removeTx); + + storedSlPool = await sharedLiquidityManagerClient.getSlPool(slPool); + + const activeSpotPool = await cpSwap.account.poolState.fetch( + storedSlPool.activeSpotPool + ); + console.log("activeSpotPool", activeSpotPool); + return; + + let banksClient = this.banksClient as BanksClient; + + // console.log(await banksClient.getAccount(cpSwap.programId)); + + console.log("storedSlPool", storedSlPool); + console.log( + "storedSlPool.activeSpotPool", + await banksClient.getAccount(storedSlPool.activeSpotPool) + ); + const activeSpotPoolRaw = await banksClient.getAccount( + storedSlPool.activeSpotPool + ); + console.log(typeof activeSpotPoolRaw); + // anchor.accounts + // const activeSpotPool = cpSwap.account.poolState.coder.accounts.decode("poolState", activeSpotPoolRaw.data); + console.log("activeSpotPool", activeSpotPool); + return; + await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second + const storedSpotPool1 = await cpSwap.account.poolState.fetch( + storedSlPool.activeSpotPool + ); + return; + console.log(storedSpotPool1); + storedSlPool = await sharedLiquidityManagerClient.getSlPool(slPool); + + const storedSpotPool2 = await cpSwap.account.poolState.fetch( + storedSlPool.activeSpotPool + ); + console.log(storedSpotPool2); + + return; + + console.log("slPool", slPool); + console.log("slPoolSigner", slPoolSigner); + console.log("spotPool", storedSlPool.activeSpotPool); + const spotPool1 = getSpotPoolAddr( + sharedLiquidityManagerClient.getProgramId(), + slPool, + 1 + )[0]; + console.log("spotPool1", spotPool1); + + // const storedSpotPool1 = await cpSwap.account.poolState.fetchNullable(spotPool1); + // console.log(storedSpotPool1); + // console.log(await this.banksClient.getAccount(storedSpotPool1.token0Vault)); + // console.log(await this.banksClient.getAccount(storedSpotPool1.token1Vault)); +} diff --git a/tests/sharedLiquidityManager/main.test.ts b/tests/sharedLiquidityManager/main.test.ts new file mode 100644 index 000000000..16a0a1920 --- /dev/null +++ b/tests/sharedLiquidityManager/main.test.ts @@ -0,0 +1,24 @@ +import initializeSharedLiquidityPool from "./unit/initializeSharedLiquidityPool.test.js"; +import initializeDraftProposal from "./unit/initializeDraftProposal.test.js"; +import stakeToDraftProposal from "./unit/stakeToDraftProposal.test.js"; +import unstakeFromDraftProposal from "./unit/unstakeFromDraftProposal.test.js"; +import depositSharedLiquidity from "./unit/depositSharedLiquidity.test.js"; +import withdrawSharedLiquidity from "./unit/withdrawSharedLiquidity.test.js"; +import initializeProposalWithLiquidity from "./unit/initializeProposalWithLiquidity.test.js"; +import removeProposalLiquidity from "./unit/removeProposalLiquidity.test.js"; +import sharedLiquidityManagerLifecycle from "./integration/sharedLiquidityManagerLifecycle.test.js"; + +export default function suite() { + describe("#initialize_shared_liquidity_pool", initializeSharedLiquidityPool); + describe("#initialize_draft_proposal", initializeDraftProposal); + describe("#stake_to_draft_proposal", stakeToDraftProposal); + describe("#unstake_from_draft_proposal", unstakeFromDraftProposal); + describe("#deposit_shared_liquidity", depositSharedLiquidity); + describe("#withdraw_shared_liquidity", withdrawSharedLiquidity); + describe( + "#initialize_proposal_with_liquidity", + initializeProposalWithLiquidity + ); + describe("#remove_proposal_liquidity", removeProposalLiquidity); + it("shared liquidity manager lifecycle", sharedLiquidityManagerLifecycle); +} diff --git a/tests/sharedLiquidityManager/unit/depositSharedLiquidity.test.ts b/tests/sharedLiquidityManager/unit/depositSharedLiquidity.test.ts new file mode 100644 index 000000000..ddd7b41f8 --- /dev/null +++ b/tests/sharedLiquidityManager/unit/depositSharedLiquidity.test.ts @@ -0,0 +1,296 @@ +import { + SharedLiquidityManagerClient, + AutocratClient, + getSharedLiquidityPoolAddr, + getSpotPoolAddr, + getSlPoolPositionAddr, +} from "@metadaoproject/futarchy/v0.4"; +import { PublicKey, ComputeBudgetProgram, Keypair } from "@solana/web3.js"; +import { assert } from "chai"; +import { createMint, getAccount } from "spl-token-bankrun"; +import { BN } from "bn.js"; +import * as token from "@solana/spl-token"; +import { DAY_IN_SLOTS, expectError } from "../../utils.js"; + +export default function suite() { + let sharedLiquidityManagerClient: SharedLiquidityManagerClient; + let autocratClient: AutocratClient; + let META: PublicKey; + let USDC: PublicKey; + let dao: PublicKey; + let slPool: PublicKey; + let spotPool: PublicKey; + + before(async function () { + sharedLiquidityManagerClient = this.sharedLiquidityManagerClient; + autocratClient = this.autocratClient; + }); + + beforeEach(async function () { + // Create fresh test tokens for each test to avoid address collisions + META = await createMint( + this.banksClient, + this.payer, + this.payer.publicKey, + this.payer.publicKey, + 9 + ); + USDC = await createMint( + this.banksClient, + this.payer, + this.payer.publicKey, + this.payer.publicKey, + 6 + ); + + // Create token accounts and mint tokens + await this.createTokenAccount(META, this.payer.publicKey); + await this.createTokenAccount(USDC, this.payer.publicKey); + await this.mintTo(META, this.payer.publicKey, this.payer, 100 * 10 ** 9); + await this.mintTo( + USDC, + this.payer.publicKey, + this.payer, + 100_000 * 10 ** 6 + ); + + dao = await autocratClient.initializeDao( + META, + 1000, + 10, + 10_000, + USDC, + undefined, + new BN(DAY_IN_SLOTS.toString()) + ); + + // Initialize shared liquidity pool + await sharedLiquidityManagerClient + .initializeSharedLiquidityPoolIx( + dao, + META, + USDC, + new BN(25 * 10 ** 9), + new BN(25_000 * 10 ** 6) + ) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 }), + ]) + .rpc(); + + // Calculate pool addresses + [slPool] = getSharedLiquidityPoolAddr( + sharedLiquidityManagerClient.getProgramId(), + dao, + this.payer.publicKey, + 100 + ); + + [spotPool] = getSpotPoolAddr( + sharedLiquidityManagerClient.getProgramId(), + slPool, + 0 + ); + }); + + it("deposits liquidity to shared pool", async function () { + const user = Keypair.generate(); + await this.createTokenAccount(META, user.publicKey); + await this.createTokenAccount(USDC, user.publicKey); + await this.mintTo(META, user.publicKey, this.payer, 100 * 10 ** 9); + await this.mintTo(USDC, user.publicKey, this.payer, 100_000 * 10 ** 6); + + const lpTokenAmount = new BN(1_000_000); // 1 LP token + const maxBaseAmount = new BN(1 * 10 ** 9); // 1 META max + const maxQuoteAmount = new BN(1_000 * 10 ** 6); // 1,000 USDC max + + const initialBaseBalance = await this.getTokenBalance(META, user.publicKey); + const initialQuoteBalance = await this.getTokenBalance( + USDC, + user.publicKey + ); + + await sharedLiquidityManagerClient + .depositSharedLiquidityIx( + slPool, + spotPool, + META, + USDC, + lpTokenAmount, + maxBaseAmount, + maxQuoteAmount, + user.publicKey + ) + .signers([user]) + .rpc(); + + // Check user position was created/updated + // const storedSlPool = await sharedLiquidityManagerClient.program.account.sharedLiquidityPool.fetch(slPool); + const position = await sharedLiquidityManagerClient.getSlPoolPosition( + getSlPoolPositionAddr( + sharedLiquidityManagerClient.getProgramId(), + slPool, + user.publicKey + )[0] + ); + + assert.equal( + position.underlyingSpotLpShares.toString(), + lpTokenAmount.toString() + ); + + // Verify some tokens were spent (exact amounts depend on pool ratios) + const finalBaseBalance = await this.getTokenBalance(META, user.publicKey); + const finalQuoteBalance = await this.getTokenBalance(USDC, user.publicKey); + + assert.isBelow(Number(finalBaseBalance), Number(initialBaseBalance)); + assert.isBelow(Number(finalQuoteBalance), Number(initialQuoteBalance)); + }); + + it("fails with insufficient base tokens", async function () { + const user = Keypair.generate(); + await this.createTokenAccount(META, user.publicKey); + await this.createTokenAccount(USDC, user.publicKey); + // Give user only 1 META but try to deposit 200 META worth + await this.mintTo(META, user.publicKey, this.payer, 1 * 10 ** 9); + await this.mintTo(USDC, user.publicKey, this.payer, 100_000 * 10 ** 6); + + // Request a large amount of LP tokens that would require more than 1 META + const lpTokenAmount = new BN(500000000_000_000); // 50 LP tokens (much more than user can afford) + const maxBaseAmount = new BN(200 * 10 ** 9); // More than user has + const maxQuoteAmount = new BN(1_000 * 10 ** 6); + + const callbacks = expectError( + "InsufficientFunds", + "Should have thrown error for insufficient base tokens" + ); + + await sharedLiquidityManagerClient + .depositSharedLiquidityIx( + slPool, + spotPool, + META, + USDC, + lpTokenAmount, + maxBaseAmount, + maxQuoteAmount, + user.publicKey + ) + .signers([user]) + .rpc() + .then(callbacks[0], callbacks[1]); + }); + + it("fails with insufficient quote tokens", async function () { + const user = Keypair.generate(); + await this.createTokenAccount(META, user.publicKey); + await this.createTokenAccount(USDC, user.publicKey); + await this.mintTo(META, user.publicKey, this.payer, 100 * 10 ** 9); + // Give user only 1,000 USDC but try to deposit 200,000 USDC worth + await this.mintTo(USDC, user.publicKey, this.payer, 1_000 * 10 ** 6); + + // Request a large amount of LP tokens that would require more than 1,000 USDC + const lpTokenAmount = new BN(50_000_000); // 50 LP tokens (much more than user can afford) + const maxBaseAmount = new BN(1 * 10 ** 9); + const maxQuoteAmount = new BN(200_000 * 10 ** 6); // More than user has + + const callbacks = expectError( + "InsufficientFunds", + "Should have thrown error for insufficient quote tokens" + ); + + await sharedLiquidityManagerClient + .depositSharedLiquidityIx( + slPool, + spotPool, + META, + USDC, + lpTokenAmount, + maxBaseAmount, + maxQuoteAmount, + user.publicKey + ) + .signers([user]) + .rpc() + .then(callbacks[0], callbacks[1]); + }); + + it("allows multiple deposits from same user", async function () { + const lpTokenAmount = new BN(500_000); // 0.5 LP token + const maxBaseAmount = new BN(1 * 10 ** 9); + const maxQuoteAmount = new BN(1_000 * 10 ** 6); + // First deposit + await sharedLiquidityManagerClient + .depositSharedLiquidityIx( + slPool, + spotPool, + META, + USDC, + lpTokenAmount, + maxBaseAmount, + maxQuoteAmount + ) + .rpc(); + + // Second deposit + await sharedLiquidityManagerClient + .depositSharedLiquidityIx( + slPool, + spotPool, + META, + USDC, + lpTokenAmount, + maxBaseAmount, + maxQuoteAmount + ) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitPrice({ + microLamports: 1000, + }), + ]) + .rpc(); + + // Both should succeed + assert.ok(true); + }); + + it("allows deposits from multiple users", async function () { + const secondUser = Keypair.generate(); + await this.createTokenAccount(META, secondUser.publicKey); + await this.createTokenAccount(USDC, secondUser.publicKey); + await this.mintTo(META, secondUser.publicKey, this.payer, 10 * 10 ** 9); + await this.mintTo(USDC, secondUser.publicKey, this.payer, 10_000 * 10 ** 6); + + const lpTokenAmount = new BN(500_000); + const maxBaseAmount = new BN(1 * 10 ** 9); + const maxQuoteAmount = new BN(1_000 * 10 ** 6); + + // First user deposits + await sharedLiquidityManagerClient + .depositSharedLiquidityIx( + slPool, + spotPool, + META, + USDC, + lpTokenAmount, + maxBaseAmount, + maxQuoteAmount + ) + .rpc(); + + // Second user deposits + await sharedLiquidityManagerClient + .depositSharedLiquidityIx( + slPool, + spotPool, + META, + USDC, + lpTokenAmount, + maxBaseAmount, + maxQuoteAmount, + secondUser.publicKey + ) + .signers([secondUser]) + .rpc(); + }); +} diff --git a/tests/sharedLiquidityManager/unit/initializeDraftProposal.test.ts b/tests/sharedLiquidityManager/unit/initializeDraftProposal.test.ts new file mode 100644 index 000000000..a0f09195e --- /dev/null +++ b/tests/sharedLiquidityManager/unit/initializeDraftProposal.test.ts @@ -0,0 +1,188 @@ +import { + SharedLiquidityManagerClient, + AutocratClient, + getDraftProposalAddr, + getSharedLiquidityPoolAddr, +} from "@metadaoproject/futarchy/v0.4"; +import { PublicKey, ComputeBudgetProgram } from "@solana/web3.js"; +import { assert } from "chai"; +import { createMint } from "spl-token-bankrun"; +import { BN } from "bn.js"; +import { DAY_IN_SLOTS } from "../../utils.js"; + +export default function suite() { + let sharedLiquidityManagerClient: SharedLiquidityManagerClient; + let autocratClient: AutocratClient; + let META: PublicKey; + let USDC: PublicKey; + let dao: PublicKey; + let slPool: PublicKey; + + before(async function () { + sharedLiquidityManagerClient = this.sharedLiquidityManagerClient; + autocratClient = this.autocratClient; + }); + + beforeEach(async function () { + // Create fresh test tokens for each test to avoid address collisions + META = await createMint( + this.banksClient, + this.payer, + this.payer.publicKey, + this.payer.publicKey, + 9 + ); + USDC = await createMint( + this.banksClient, + this.payer, + this.payer.publicKey, + this.payer.publicKey, + 6 + ); + + // Create token accounts and mint tokens + await this.createTokenAccount(META, this.payer.publicKey); + await this.createTokenAccount(USDC, this.payer.publicKey); + await this.mintTo(META, this.payer.publicKey, this.payer, 100 * 10 ** 9); + await this.mintTo( + USDC, + this.payer.publicKey, + this.payer, + 100_000 * 10 ** 6 + ); + + // Initialize DAO and shared liquidity pool + dao = await autocratClient.initializeDao( + META, + 1000, + 10, + 10_000, + USDC, + undefined, + new BN(DAY_IN_SLOTS.toString()) + ); + + await sharedLiquidityManagerClient + .initializeSharedLiquidityPoolIx( + dao, + META, + USDC, + new BN(25 * 10 ** 9), + new BN(25_000 * 10 ** 6) + ) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 }), + ]) + .rpc(); + + [slPool] = getSharedLiquidityPoolAddr( + sharedLiquidityManagerClient.getProgramId(), + dao, + this.payer.publicKey, + 100 + ); + }); + + it("initializes draft proposal with simple instruction", async function () { + const nonce = new BN(1337); + await sharedLiquidityManagerClient + .initializeDraftProposalIx( + slPool, + META, + { + programId: META, + accounts: [], + data: Buffer.from([]), + }, + nonce + ) + .rpc(); + + const [draftProposal] = getDraftProposalAddr( + sharedLiquidityManagerClient.getProgramId(), + nonce + ); + + const storedDraftProposal = + await sharedLiquidityManagerClient.program.account.draftProposal.fetch( + draftProposal + ); + + assert.ok(storedDraftProposal.sharedLiquidityPool.equals(slPool)); + assert.ok(storedDraftProposal.baseMint.equals(META)); + assert.equal(storedDraftProposal.stakedTokenAmount.toString(), "0"); + assert.equal(storedDraftProposal.nonce.toString(), nonce.toString()); + assert.exists(storedDraftProposal.status.draft); + }); + + it("initializes draft proposal with complex instruction", async function () { + const complexInstruction = { + programId: META, + accounts: [ + { pubkey: META, isSigner: false, isWritable: true }, + { pubkey: USDC, isSigner: false, isWritable: false }, + ], + data: Buffer.from([1, 2, 3, 4, 5]), + }; + + const nonce = new BN(2468); + await sharedLiquidityManagerClient + .initializeDraftProposalIx(slPool, META, complexInstruction, nonce) + .rpc(); + + const [draftProposal] = getDraftProposalAddr( + sharedLiquidityManagerClient.getProgramId(), + nonce + ); + + const storedDraftProposal = + await sharedLiquidityManagerClient.program.account.draftProposal.fetch( + draftProposal + ); + + assert.ok(storedDraftProposal.instruction.programId.equals(META)); + assert.equal(storedDraftProposal.instruction.accounts.length, 2); + assert.deepEqual( + Array.from(storedDraftProposal.instruction.data), + [1, 2, 3, 4, 5] + ); + }); + + it("fails with duplicate nonce", async function () { + const nonce = new BN(3691); + + // First proposal should succeed + await sharedLiquidityManagerClient + .initializeDraftProposalIx( + slPool, + META, + { + programId: META, + accounts: [], + data: Buffer.from([]), + }, + nonce + ) + .rpc(); + + // Second proposal with same nonce should fail + try { + await sharedLiquidityManagerClient + .initializeDraftProposalIx( + slPool, + META, + { + programId: META, + accounts: [], + data: Buffer.from([1]), + }, + nonce + ) + .rpc(); + assert.fail("Should have thrown error"); + } catch (e) { + // Should fail due to account already existing + assert.exists(e); + } + }); +} diff --git a/tests/sharedLiquidityManager/unit/initializeProposalWithLiquidity.test.ts b/tests/sharedLiquidityManager/unit/initializeProposalWithLiquidity.test.ts new file mode 100644 index 000000000..93157c19d --- /dev/null +++ b/tests/sharedLiquidityManager/unit/initializeProposalWithLiquidity.test.ts @@ -0,0 +1,783 @@ +import { + SharedLiquidityManagerClient, + AutocratClient, + AmmClient, + ConditionalVaultClient, + getSharedLiquidityPoolAddr, + getSpotPoolAddr, + getDraftProposalAddr, + getProposalAddr, + getDaoTreasuryAddr, + getSharedLiquidityPoolSignerAddr, + InstructionUtils, + AMM_PROGRAM_ID, + CONDITIONAL_VAULT_PROGRAM_ID, + AUTOCRAT_PROGRAM_ID, + getSlPoolPositionAddr, +} from "@metadaoproject/futarchy/v0.4"; +import { + PublicKey, + ComputeBudgetProgram, + Keypair, + Transaction, + AddressLookupTableProgram, + TransactionMessage, + VersionedTransaction, + AddressLookupTableAccount, +} from "@solana/web3.js"; +import { assert } from "chai"; +import { createMint, getAccount } from "spl-token-bankrun"; +import { BN } from "bn.js"; +import * as token from "@solana/spl-token"; +import { + createLookupTableForTransaction, + DAY_IN_SLOTS, + expectError, +} from "../../utils.js"; +import { sha256 } from "@metadaoproject/futarchy"; +import * as anchor from "@coral-xyz/anchor"; + +export default function suite() { + let sharedLiquidityManagerClient: SharedLiquidityManagerClient; + let autocratClient: AutocratClient; + let ammClient: AmmClient; + let vaultClient: ConditionalVaultClient; + let META: PublicKey; + let USDC: PublicKey; + let dao: PublicKey; + let slPool: PublicKey; + let spotPool: PublicKey; + let proposalNonce: anchor.BN; + let proposal: PublicKey; + + before(async function () { + sharedLiquidityManagerClient = this.sharedLiquidityManagerClient; + autocratClient = this.autocratClient; + ammClient = this.ammClient; + vaultClient = this.vaultClient; + }); + + beforeEach(async function () { + // Create fresh test tokens for each test to avoid address collisions + META = await createMint( + this.banksClient, + this.payer, + this.payer.publicKey, + this.payer.publicKey, + 9 + ); + USDC = await createMint( + this.banksClient, + this.payer, + this.payer.publicKey, + this.payer.publicKey, + 6 + ); + + // Create token accounts and mint tokens + await this.createTokenAccount(META, this.payer.publicKey); + await this.createTokenAccount(USDC, this.payer.publicKey); + await this.mintTo(META, this.payer.publicKey, this.payer, 100 * 10 ** 9); + await this.mintTo( + USDC, + this.payer.publicKey, + this.payer, + 100_000 * 10 ** 6 + ); + + dao = await autocratClient.initializeDao( + META, + 1000, + 10, + 10_000, + USDC, + undefined, + new BN(DAY_IN_SLOTS.toString()) + ); + + // Initialize shared liquidity pool + await sharedLiquidityManagerClient + .initializeSharedLiquidityPoolIx( + dao, + META, + USDC, + new BN(25 * 10 ** 9), + new BN(25_000 * 10 ** 6) + ) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ units: 500_000 }), + ]) + .rpc(); + + // Calculate pool addresses + [slPool] = getSharedLiquidityPoolAddr( + sharedLiquidityManagerClient.getProgramId(), + dao, + this.payer.publicKey, + 100 + ); + + [spotPool] = getSpotPoolAddr( + sharedLiquidityManagerClient.getProgramId(), + slPool, + 0 + ); + + const [slPoolSigner] = getSharedLiquidityPoolSignerAddr( + sharedLiquidityManagerClient.getProgramId(), + slPool + ); + proposalNonce = new BN(Math.floor(Math.random() * 1000000)); + [proposal] = getProposalAddr( + AUTOCRAT_PROGRAM_ID, + slPoolSigner, + proposalNonce + ); + + // Initialize question + await vaultClient.initializeQuestion( + sha256(`Will ${proposal} pass?/FAIL/PASS`), + proposal, + 2 + ); + + // Get proposal PDAs + const { + passBaseMint, + passQuoteMint, + failBaseMint, + failQuoteMint, + passLp, + failLp, + question, + } = autocratClient.getProposalPdas(proposal, META, USDC, dao); + + const storedDao = await autocratClient.fetchDao(dao); + + // Initialize vaults and AMMs + await vaultClient + .initializeVaultIx(question, META, 2) + .postInstructions( + await InstructionUtils.getInstructions( + vaultClient.initializeVaultIx(question, USDC, 2), + ammClient.initializeAmmIx( + passBaseMint, + passQuoteMint, + storedDao.twapStartDelaySlots, + storedDao.twapInitialObservation, + storedDao.twapMaxObservationChangePerUpdate + ), + ammClient.initializeAmmIx( + failBaseMint, + failQuoteMint, + storedDao.twapStartDelaySlots, + storedDao.twapInitialObservation, + storedDao.twapMaxObservationChangePerUpdate + ) + ) + ) + .rpc(); + }); + + it("initializes proposal with liquidity successfully", async function () { + const proposalCreator = Keypair.generate(); + await this.createTokenAccount(META, proposalCreator.publicKey); + await this.mintTo( + META, + proposalCreator.publicKey, + this.payer, + 100 * 10 ** 9 + ); + + // First create a draft proposal + const draftProposalNonce = new BN(Math.floor(Math.random() * 1000000)); + const [draftProposal] = getDraftProposalAddr( + sharedLiquidityManagerClient.getProgramId(), + draftProposalNonce + ); + + await sharedLiquidityManagerClient + .initializeDraftProposalIx( + slPool, + META, + { + programId: autocratClient.getProgramId(), + accounts: [ + { pubkey: dao, isSigner: false, isWritable: true }, + { + pubkey: Keypair.generate().publicKey, + isSigner: false, + isWritable: true, + }, + { + pubkey: Keypair.generate().publicKey, + isSigner: false, + isWritable: true, + }, + { + pubkey: proposalCreator.publicKey, + isSigner: true, + isWritable: false, + }, + { + pubkey: Keypair.generate().publicKey, + isSigner: false, + isWritable: false, + }, + { + pubkey: new PublicKey("11111111111111111111111111111111"), + isSigner: false, + isWritable: false, + }, + ], + data: Buffer.from([]), + }, + draftProposalNonce + ) + .rpc(); + + // Stake enough tokens to meet threshold + const stakeAmount = new BN(10 * 10 ** 9); // 10 META tokens + await sharedLiquidityManagerClient + .stakeToDraftProposalIx( + draftProposal, + META, + stakeAmount, + proposalCreator.publicKey + ) + .signers([proposalCreator]) + .rpc(); + + // Setup required for initializeProposalWithLiquidity + // Create lookup table for the transaction + let initProposalWithLiquidityTx: Transaction = + await sharedLiquidityManagerClient + .initializeProposalWithLiquidityIx( + dao, + META, + USDC, + proposalNonce, + draftProposal + ) + .transaction(); + + const lookupTable = await createLookupTableForTransaction( + initProposalWithLiquidityTx, + this + ); + console.log("lookupTable", lookupTable); + return; + + // Create and send the versioned transaction + const messageV0 = new TransactionMessage({ + payerKey: this.payer.publicKey, + recentBlockhash: (await this.banksClient.getLatestBlockhash())[0], + instructions: [ + ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }), + ComputeBudgetProgram.requestHeapFrame({ bytes: 256 * 1024 }), + ].concat(initProposalWithLiquidityTx.instructions), + }).compileToV0Message([lookupTable]); + + let tx = new VersionedTransaction(messageV0); + tx.sign([this.payer]); + + await this.banksClient.processTransaction(tx); + + // Check that the draft proposal status was updated + const draftProposalAccount = + await sharedLiquidityManagerClient.program.account.draftProposal.fetch( + draftProposal + ); + assert.exists(draftProposalAccount.status.initialized); + + // Check that the shared liquidity pool was updated + const finalSlPool = await sharedLiquidityManagerClient.getSlPool(slPool); + assert.isNotNull(finalSlPool.activeProposal); + }); + + it("fails when stake threshold not met", async function () { + const proposalCreator = Keypair.generate(); + await this.createTokenAccount(META, proposalCreator.publicKey); + await this.mintTo( + META, + proposalCreator.publicKey, + this.payer, + 100 * 10 ** 9 + ); + + // Create a draft proposal + const draftProposalNonce = new BN(Math.floor(Math.random() * 1000000)); + const [draftProposal] = getDraftProposalAddr( + sharedLiquidityManagerClient.getProgramId(), + draftProposalNonce + ); + + await sharedLiquidityManagerClient + .initializeDraftProposalIx( + slPool, + META, + { + programId: autocratClient.getProgramId(), + accounts: [ + { pubkey: dao, isSigner: false, isWritable: true }, + { + pubkey: Keypair.generate().publicKey, + isSigner: false, + isWritable: true, + }, + { + pubkey: Keypair.generate().publicKey, + isSigner: false, + isWritable: true, + }, + { + pubkey: proposalCreator.publicKey, + isSigner: true, + isWritable: false, + }, + { + pubkey: Keypair.generate().publicKey, + isSigner: false, + isWritable: false, + }, + { + pubkey: new PublicKey("11111111111111111111111111111111"), + isSigner: false, + isWritable: false, + }, + ], + data: Buffer.from([]), + }, + draftProposalNonce + ) + .rpc(); + + // Stake insufficient tokens (less than threshold) + const stakeAmount = new BN(1 * 10 ** 9); // Only 1 META token + await sharedLiquidityManagerClient + .stakeToDraftProposalIx( + draftProposal, + META, + stakeAmount, + proposalCreator.publicKey + ) + .signers([proposalCreator]) + .rpc(); + + // Try to initialize proposal with liquidity + const callbacks = expectError( + "InsufficientStake", + "Should have thrown error for insufficient stake" + ); + + let initializeProposalWithLiquidityTx = await sharedLiquidityManagerClient + .initializeProposalWithLiquidityIx( + dao, + META, + USDC, + proposalNonce, + draftProposal + ) + .transaction(); + + const lookupTable = await createLookupTableForTransaction( + initializeProposalWithLiquidityTx, + this + ); + + let messageV0 = new TransactionMessage({ + payerKey: this.payer.publicKey, + recentBlockhash: (await this.banksClient.getLatestBlockhash())[0], + instructions: [ + ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }), + ComputeBudgetProgram.requestHeapFrame({ bytes: 256 * 1024 }), + ].concat(initializeProposalWithLiquidityTx.instructions), + }).compileToV0Message([lookupTable]); + + let tx = new VersionedTransaction(messageV0); + tx.sign([this.payer]); + + const result = await this.banksClient.tryProcessTransaction(tx); + assert.isTrue( + result.meta.logMessages.some((log: string) => + log.includes("InsufficientStake") + ), + "Expected at least one log message to contain 'InsufficientStake'" + ); + }); + + it("fails when draft proposal is not in draft status", async function () { + const proposalCreator = Keypair.generate(); + await this.createTokenAccount(META, proposalCreator.publicKey); + await this.mintTo( + META, + proposalCreator.publicKey, + this.payer, + 100 * 10 ** 9 + ); + + // Create a draft proposal + const draftProposalNonce = new BN(Math.floor(Math.random() * 1000000)); + const [draftProposal] = getDraftProposalAddr( + sharedLiquidityManagerClient.getProgramId(), + draftProposalNonce + ); + + await sharedLiquidityManagerClient + .initializeDraftProposalIx( + slPool, + META, + { + programId: autocratClient.getProgramId(), + accounts: [ + { pubkey: dao, isSigner: false, isWritable: true }, + { + pubkey: Keypair.generate().publicKey, + isSigner: false, + isWritable: true, + }, + { + pubkey: Keypair.generate().publicKey, + isSigner: false, + isWritable: true, + }, + { + pubkey: proposalCreator.publicKey, + isSigner: true, + isWritable: false, + }, + { + pubkey: Keypair.generate().publicKey, + isSigner: false, + isWritable: false, + }, + { + pubkey: new PublicKey("11111111111111111111111111111111"), + isSigner: false, + isWritable: false, + }, + ], + data: Buffer.from([]), + }, + draftProposalNonce + ) + .rpc(); + + // Stake enough tokens + const stakeAmount = new BN(10 * 10 ** 9); + await sharedLiquidityManagerClient + .stakeToDraftProposalIx( + draftProposal, + META, + stakeAmount, + proposalCreator.publicKey + ) + .signers([proposalCreator]) + .rpc(); + + let initializeProposalWithLiquidityTx = await sharedLiquidityManagerClient + .initializeProposalWithLiquidityIx( + dao, + META, + USDC, + proposalNonce, + draftProposal + ) + .transaction(); + + let lookupTable = await createLookupTableForTransaction( + initializeProposalWithLiquidityTx, + this + ); + + let messageV0 = new TransactionMessage({ + payerKey: this.payer.publicKey, + recentBlockhash: (await this.banksClient.getLatestBlockhash())[0], + instructions: [ + ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }), + ComputeBudgetProgram.requestHeapFrame({ bytes: 256 * 1024 }), + ].concat(initializeProposalWithLiquidityTx.instructions), + }).compileToV0Message([lookupTable]); + let tx = new VersionedTransaction(messageV0); + tx.sign([this.payer]); + + await this.banksClient.processTransaction(tx); + + await this.advanceBySlots(DAY_IN_SLOTS); + + let { passAmm, failAmm } = autocratClient.getProposalPdas( + proposal, + META, + USDC, + dao + ); + + // Crank TWAPs multiple times to ensure markets are mature enough + // The markets need to have been updated for at least proposal.duration_in_slots + for (let i = 0; i < 50; i++) { + await this.advanceBySlots(20_000n); + + await ammClient + .crankThatTwapIx(passAmm) + .preInstructions([ + // Add compute unit price to avoid bankrun thinking we've processed the same transaction multiple times + ComputeBudgetProgram.setComputeUnitPrice({ + microLamports: i, + }), + await ammClient.crankThatTwapIx(failAmm).instruction(), + ]) + .rpc(); + } + + // Finalize the proposal with a pass outcome + await autocratClient.finalizeProposal(proposal); + + let removeProposalLiquidityTx = await sharedLiquidityManagerClient + .removeProposalLiquidityIx( + dao, + spotPool, + META, + USDC, + proposalNonce, + 100, + 0 + ) + .transaction(); + + lookupTable = await createLookupTableForTransaction( + removeProposalLiquidityTx, + this + ); + + const messageV0Remove = new TransactionMessage({ + payerKey: this.payer.publicKey, + recentBlockhash: (await this.banksClient.getLatestBlockhash())[0], + instructions: [ + ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }), + ComputeBudgetProgram.requestHeapFrame({ bytes: 256 * 1024 }), + ].concat(removeProposalLiquidityTx.instructions), + }).compileToV0Message([lookupTable]); + + let removeTx = new VersionedTransaction(messageV0Remove); + removeTx.sign([this.payer]); + await this.banksClient.processTransaction(removeTx); + + proposalNonce = new BN(Math.floor(Math.random() * 1000000)); + const [slPoolSigner] = getSharedLiquidityPoolSignerAddr( + sharedLiquidityManagerClient.getProgramId(), + slPool + ); + + [proposal] = getProposalAddr( + AUTOCRAT_PROGRAM_ID, + slPoolSigner, + proposalNonce + ); + + // Initialize question + await vaultClient.initializeQuestion( + sha256(`Will ${proposal} pass?/FAIL/PASS`), + proposal, + 2 + ); + + // Get proposal PDAs + const { + passBaseMint, + passQuoteMint, + failBaseMint, + failQuoteMint, + passLp, + failLp, + question, + } = autocratClient.getProposalPdas(proposal, META, USDC, dao); + + const storedDao = await autocratClient.fetchDao(dao); + + // Initialize vaults and AMMs + await vaultClient + .initializeVaultIx(question, META, 2) + .postInstructions( + await InstructionUtils.getInstructions( + vaultClient.initializeVaultIx(question, USDC, 2), + ammClient.initializeAmmIx( + passBaseMint, + passQuoteMint, + storedDao.twapStartDelaySlots, + storedDao.twapInitialObservation, + storedDao.twapMaxObservationChangePerUpdate + ), + ammClient.initializeAmmIx( + failBaseMint, + failQuoteMint, + storedDao.twapStartDelaySlots, + storedDao.twapInitialObservation, + storedDao.twapMaxObservationChangePerUpdate + ) + ) + ) + .rpc(); + + initializeProposalWithLiquidityTx = await sharedLiquidityManagerClient + .initializeProposalWithLiquidityIx( + dao, + META, + USDC, + proposalNonce, + draftProposal, + 1 + ) + .transaction(); + + lookupTable = await createLookupTableForTransaction( + initializeProposalWithLiquidityTx, + this + ); + + messageV0 = new TransactionMessage({ + payerKey: this.payer.publicKey, + recentBlockhash: (await this.banksClient.getLatestBlockhash())[0], + instructions: [ + ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }), + ComputeBudgetProgram.requestHeapFrame({ bytes: 256 * 1024 }), + ].concat(initializeProposalWithLiquidityTx.instructions), + }).compileToV0Message([lookupTable]); + tx = new VersionedTransaction(messageV0); + tx.sign([this.payer]); + + const result = await this.banksClient.tryProcessTransaction(tx); + assert.isTrue( + result.meta.logMessages.some((log: string) => + log.includes("ProposalNotInDraftStatus") + ), + "Expected at least one log message to contain 'ProposalNotInDraftStatus'" + ); + }); + + it("fails when no LP tokens in pool", async function () { + const [slPoolPosition] = getSlPoolPositionAddr( + sharedLiquidityManagerClient.getProgramId(), + slPool, + this.payer.publicKey + ); + + const slPoolPositionAccount = + await sharedLiquidityManagerClient.getSlPoolPosition(slPoolPosition); + // console.log("slPoolPositionAccount", slPoolPositionAccount); + // return; + + await sharedLiquidityManagerClient + .withdrawSharedLiquidityIx( + slPool, + spotPool, + META, + USDC, + slPoolPositionAccount.underlyingSpotLpShares, + new BN(0), + new BN(0) + ) + .rpc(); + + const proposalCreator = Keypair.generate(); + await this.createTokenAccount(META, proposalCreator.publicKey); + await this.mintTo( + META, + proposalCreator.publicKey, + this.payer, + 100 * 10 ** 9 + ); + + // Create a draft proposal + const draftProposalNonce = new BN(Math.floor(Math.random() * 1000000)); + const [draftProposal] = getDraftProposalAddr( + sharedLiquidityManagerClient.getProgramId(), + draftProposalNonce + ); + + await sharedLiquidityManagerClient + .initializeDraftProposalIx( + slPool, + META, + { + programId: autocratClient.getProgramId(), + accounts: [ + { pubkey: dao, isSigner: false, isWritable: true }, + { + pubkey: Keypair.generate().publicKey, + isSigner: false, + isWritable: true, + }, + { + pubkey: Keypair.generate().publicKey, + isSigner: false, + isWritable: true, + }, + { + pubkey: proposalCreator.publicKey, + isSigner: true, + isWritable: false, + }, + { + pubkey: Keypair.generate().publicKey, + isSigner: false, + isWritable: false, + }, + { + pubkey: new PublicKey("11111111111111111111111111111111"), + isSigner: false, + isWritable: false, + }, + ], + data: Buffer.from([]), + }, + draftProposalNonce + ) + .rpc(); + + // Stake enough tokens + const stakeAmount = new BN(10 * 10 ** 9); + await sharedLiquidityManagerClient + .stakeToDraftProposalIx( + draftProposal, + META, + stakeAmount, + proposalCreator.publicKey + ) + .signers([proposalCreator]) + .rpc(); + + const initializeProposalWithLiquidityTx = await sharedLiquidityManagerClient + .initializeProposalWithLiquidityIx( + dao, + META, + USDC, + proposalNonce, + draftProposal + ) + .transaction(); + + const lookupTable = await createLookupTableForTransaction( + initializeProposalWithLiquidityTx, + this + ); + + let messageV0 = new TransactionMessage({ + payerKey: this.payer.publicKey, + recentBlockhash: (await this.banksClient.getLatestBlockhash())[0], + instructions: [ + ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }), + ComputeBudgetProgram.requestHeapFrame({ bytes: 256 * 1024 }), + ].concat(initializeProposalWithLiquidityTx.instructions), + }).compileToV0Message([lookupTable]); + let tx = new VersionedTransaction(messageV0); + tx.sign([this.payer]); + + const result = await this.banksClient.tryProcessTransaction(tx); + assert.isTrue( + result.meta.logMessages.some((log: string) => + log.includes("NoLpTokensInPool") + ), + "Expected at least one log message to contain 'NoLpTokensInPool'" + ); + }); +} diff --git a/tests/sharedLiquidityManager/unit/initializeSharedLiquidityPool.test.ts b/tests/sharedLiquidityManager/unit/initializeSharedLiquidityPool.test.ts new file mode 100644 index 000000000..5f03b67e9 --- /dev/null +++ b/tests/sharedLiquidityManager/unit/initializeSharedLiquidityPool.test.ts @@ -0,0 +1,205 @@ +import { + SharedLiquidityManagerClient, + AutocratClient, + getSharedLiquidityPoolAddr, +} from "@metadaoproject/futarchy/v0.4"; +import { PublicKey, ComputeBudgetProgram } from "@solana/web3.js"; +import { assert } from "chai"; +import { createMint } from "spl-token-bankrun"; +import { BN } from "bn.js"; +import { DAY_IN_SLOTS, expectError } from "../../utils.js"; + +export default function suite() { + let sharedLiquidityManagerClient: SharedLiquidityManagerClient; + let autocratClient: AutocratClient; + let META: PublicKey; + let USDC: PublicKey; + + before(async function () { + sharedLiquidityManagerClient = this.sharedLiquidityManagerClient; + autocratClient = this.autocratClient; + }); + + beforeEach(async function () { + // Create fresh test tokens for each test to avoid address collisions + META = await createMint( + this.banksClient, + this.payer, + this.payer.publicKey, + this.payer.publicKey, + 9 + ); + USDC = await createMint( + this.banksClient, + this.payer, + this.payer.publicKey, + this.payer.publicKey, + 6 + ); + + // Create token accounts and mint tokens + await this.createTokenAccount(META, this.payer.publicKey); + await this.createTokenAccount(USDC, this.payer.publicKey); + await this.mintTo(META, this.payer.publicKey, this.payer, 100 * 10 ** 9); + await this.mintTo( + USDC, + this.payer.publicKey, + this.payer, + 100_000 * 10 ** 6 + ); + }); + + it("initializes shared liquidity pool with valid parameters", async function () { + const baseAmount = new BN(25 * 10 ** 9); // 25 META + const quoteAmount = new BN(25_000 * 10 ** 6); // 25,000 USDC + + const dao = await autocratClient.initializeDao( + META, + 1000, + 10, + 10_000, + USDC, + undefined, + new BN(DAY_IN_SLOTS.toString()) + ); + + await sharedLiquidityManagerClient + .initializeSharedLiquidityPoolIx(dao, META, USDC, baseAmount, quoteAmount) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 }), + ]) + .rpc(); + + const [slPool] = getSharedLiquidityPoolAddr( + sharedLiquidityManagerClient.getProgramId(), + dao, + this.payer.publicKey, + 100 + ); + + const storedSlPool = + await sharedLiquidityManagerClient.program.account.sharedLiquidityPool.fetch( + slPool + ); + + // Verify basic pool properties + assert.ok(storedSlPool.dao.equals(dao)); + assert.ok(storedSlPool.baseMint.equals(META)); + assert.ok(storedSlPool.quoteMint.equals(USDC)); + assert.equal(storedSlPool.proposalStakeRateThresholdBps, 100); + assert.equal(storedSlPool.seqNum.toString(), "0"); + assert.isNull(storedSlPool.activeProposal); + }); + + it("fails with insufficient base tokens", async function () { + const baseAmount = new BN(200 * 10 ** 9); // More than available + const quoteAmount = new BN(25_000 * 10 ** 6); + + const dao = await autocratClient.initializeDao( + META, + 1000, + 10, + 10_000, + USDC, + undefined, + new BN(DAY_IN_SLOTS.toString()) + ); + + const callbacks = expectError( + "InsufficientFunds", + "should fail with insufficient base tokens" + ); + + await sharedLiquidityManagerClient + .initializeSharedLiquidityPoolIx(dao, META, USDC, baseAmount, quoteAmount) + .rpc() + .then(callbacks[0], callbacks[1]); + }); + + it("fails with insufficient quote tokens", async function () { + const baseAmount = new BN(25 * 10 ** 9); + const quoteAmount = new BN(200_000 * 10 ** 6); // More than available + + const dao = await autocratClient.initializeDao( + META, + 1000, + 10, + 10_000, + USDC, + undefined, + new BN(DAY_IN_SLOTS.toString()) + ); + + const callbacks = expectError( + "InsufficientFunds", + "should fail with insufficient quote tokens" + ); + + await sharedLiquidityManagerClient + .initializeSharedLiquidityPoolIx(dao, META, USDC, baseAmount, quoteAmount) + .rpc() + .then(callbacks[0], callbacks[1]); + }); + + it("fails with zero base amount", async function () { + const baseAmount = new BN(0); + const quoteAmount = new BN(25_000 * 10 ** 6); + + const dao = await autocratClient.initializeDao( + META, + 1000, + 10, + 10_000, + USDC, + undefined, + new BN(DAY_IN_SLOTS.toString()) + ); + + try { + await sharedLiquidityManagerClient + .initializeSharedLiquidityPoolIx( + dao, + META, + USDC, + baseAmount, + quoteAmount + ) + .rpc(); + assert.fail("Should have thrown error"); + } catch (e) { + // Should fail at Raydium level for zero amounts + assert.exists(e); + } + }); + + it("fails with zero quote amount", async function () { + const baseAmount = new BN(25 * 10 ** 9); + const quoteAmount = new BN(0); + + const dao = await autocratClient.initializeDao( + META, + 1000, + 10, + 10_000, + USDC, + undefined, + new BN(DAY_IN_SLOTS.toString()) + ); + + try { + await sharedLiquidityManagerClient + .initializeSharedLiquidityPoolIx( + dao, + META, + USDC, + baseAmount, + quoteAmount + ) + .rpc(); + assert.fail("Should have thrown error"); + } catch (e) { + // Should fail at Raydium level for zero amounts + assert.exists(e); + } + }); +} diff --git a/tests/sharedLiquidityManager/unit/removeProposalLiquidity.test.ts b/tests/sharedLiquidityManager/unit/removeProposalLiquidity.test.ts new file mode 100644 index 000000000..b22f6d31d --- /dev/null +++ b/tests/sharedLiquidityManager/unit/removeProposalLiquidity.test.ts @@ -0,0 +1,337 @@ +import { + SharedLiquidityManagerClient, + AutocratClient, + getSharedLiquidityPoolAddr, + getSpotPoolAddr, + getDraftProposalAddr, + getProposalAddr, + AmmClient, + ConditionalVaultClient, + getSharedLiquidityPoolSignerAddr, + InstructionUtils, + getDaoTreasuryAddr, +} from "@metadaoproject/futarchy/v0.4"; +import { sha256 } from "@metadaoproject/futarchy"; +import { + PublicKey, + ComputeBudgetProgram, + Keypair, + Transaction, + TransactionMessage, + VersionedTransaction, +} from "@solana/web3.js"; +import { assert } from "chai"; +import { createMint, getAccount } from "spl-token-bankrun"; +import { BN } from "bn.js"; +import * as token from "@solana/spl-token"; +import { + DAY_IN_SLOTS, + expectError, + createLookupTableForTransaction, +} from "../../utils.js"; + +export default function suite() { + let sharedLiquidityManagerClient: SharedLiquidityManagerClient; + let autocratClient: AutocratClient; + let ammClient: AmmClient; + let vaultClient: ConditionalVaultClient; + let META: PublicKey; + let USDC: PublicKey; + let dao: PublicKey; + let slPool: PublicKey; + let spotPool: PublicKey; + let proposal: PublicKey; + let draftProposal: PublicKey; + let draftProposalNonce; + let proposalNonce; + + before(async function () { + sharedLiquidityManagerClient = this.sharedLiquidityManagerClient; + autocratClient = this.autocratClient; + ammClient = this.ammClient; + vaultClient = this.vaultClient; + }); + + beforeEach(async function () { + // Create fresh test tokens for each test to avoid address collisions + META = await createMint( + this.banksClient, + this.payer, + this.payer.publicKey, + this.payer.publicKey, + 9 + ); + USDC = await createMint( + this.banksClient, + this.payer, + this.payer.publicKey, + this.payer.publicKey, + 6 + ); + + // Create token accounts and mint tokens + await this.createTokenAccount(META, this.payer.publicKey); + await this.createTokenAccount(USDC, this.payer.publicKey); + await this.mintTo(META, this.payer.publicKey, this.payer, 100 * 10 ** 9); + await this.mintTo( + USDC, + this.payer.publicKey, + this.payer, + 100_000 * 10 ** 6 + ); + + dao = await autocratClient.initializeDao( + META, + 1000, + 10, + 10_000, + USDC, + undefined, + new BN(DAY_IN_SLOTS.toString()) + ); + + // Initialize shared liquidity pool + await sharedLiquidityManagerClient + .initializeSharedLiquidityPoolIx( + dao, + META, + USDC, + new BN(25 * 10 ** 9), + new BN(25_000 * 10 ** 6) + ) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 }), + ]) + .rpc(); + + // Calculate pool addresses + [slPool] = getSharedLiquidityPoolAddr( + sharedLiquidityManagerClient.getProgramId(), + dao, + this.payer.publicKey, + 100 + ); + + [spotPool] = getSpotPoolAddr( + sharedLiquidityManagerClient.getProgramId(), + slPool, + 0 + ); + + // Initialize proposal with liquidity, crank TWAP, and finalize + const [slPoolSigner] = getSharedLiquidityPoolSignerAddr( + sharedLiquidityManagerClient.getProgramId(), + slPool + ); + + draftProposalNonce = new BN(Math.floor(Math.random() * 1000000)); + [draftProposal] = getDraftProposalAddr( + sharedLiquidityManagerClient.getProgramId(), + draftProposalNonce + ); + + // Create a draft proposal + await sharedLiquidityManagerClient + .initializeDraftProposalIx( + slPool, + META, + { + programId: autocratClient.getProgramId(), + accounts: [ + { pubkey: dao, isSigner: false, isWritable: true }, + { + pubkey: Keypair.generate().publicKey, + isSigner: false, + isWritable: true, + }, + { + pubkey: Keypair.generate().publicKey, + isSigner: false, + isWritable: true, + }, + { pubkey: this.payer.publicKey, isSigner: true, isWritable: false }, + { + pubkey: Keypair.generate().publicKey, + isSigner: false, + isWritable: false, + }, + { + pubkey: new PublicKey("11111111111111111111111111111111"), + isSigner: false, + isWritable: false, + }, + ], + data: Buffer.from([]), + }, + draftProposalNonce + ) + .rpc(); + + // Stake enough tokens to meet threshold + const stakeAmount = new BN(10 * 10 ** 9); + await sharedLiquidityManagerClient + .stakeToDraftProposalIx( + draftProposal, + META, + stakeAmount, + this.payer.publicKey + ) + .rpc(); + + // Initialize proposal with liquidity + proposalNonce = new BN(Math.floor(Math.random() * 1000000)); + [proposal] = getProposalAddr( + autocratClient.getProgramId(), + slPoolSigner, + proposalNonce + ); + + // Initialize question + await vaultClient.initializeQuestion( + sha256(`Will ${proposal} pass?/FAIL/PASS`), + proposal, + 2 + ); + + // Get proposal PDAs + const { + passAmm, + failAmm, + passBaseMint, + passQuoteMint, + failBaseMint, + failQuoteMint, + passLp, + failLp, + question, + } = autocratClient.getProposalPdas(proposal, META, USDC, dao); + + const storedDao = await autocratClient.fetchDao(dao); + + // Initialize vaults and AMMs + await vaultClient + .initializeVaultIx(question, META, 2) + .postInstructions( + await InstructionUtils.getInstructions( + vaultClient.initializeVaultIx(question, USDC, 2), + ammClient.initializeAmmIx( + passBaseMint, + passQuoteMint, + storedDao.twapStartDelaySlots, + storedDao.twapInitialObservation, + storedDao.twapMaxObservationChangePerUpdate + ), + ammClient.initializeAmmIx( + failBaseMint, + failQuoteMint, + storedDao.twapStartDelaySlots, + storedDao.twapInitialObservation, + storedDao.twapMaxObservationChangePerUpdate + ) + ) + ) + .rpc(); + + // Initialize proposal with liquidity + let initProposalWithLiquidityTx: Transaction = + await sharedLiquidityManagerClient + .initializeProposalWithLiquidityIx( + dao, + META, + USDC, + proposalNonce, + draftProposal + ) + .transaction(); + + const lookupTable = await createLookupTableForTransaction( + initProposalWithLiquidityTx, + this + ); + + const messageV0 = new TransactionMessage({ + payerKey: this.payer.publicKey, + recentBlockhash: (await this.banksClient.getLatestBlockhash())[0], + instructions: [ + ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }), + ComputeBudgetProgram.requestHeapFrame({ bytes: 256 * 1024 }), + ].concat(initProposalWithLiquidityTx.instructions), + }).compileToV0Message([lookupTable]); + + let tx = new VersionedTransaction(messageV0); + tx.sign([this.payer]); + + const [daoTreasury] = getDaoTreasuryAddr( + autocratClient.getProgramId(), + dao + ); + + await this.createTokenAccount(passLp, daoTreasury); + await this.createTokenAccount(failLp, daoTreasury); + + await this.banksClient.processTransaction(tx); + + await this.advanceBySlots(DAY_IN_SLOTS); + + // Crank TWAPs multiple times to ensure markets are mature enough + // The markets need to have been updated for at least proposal.duration_in_slots + for (let i = 0; i < 50; i++) { + await this.advanceBySlots(20_000n); + + await ammClient + .crankThatTwapIx(passAmm) + .preInstructions([ + // Add compute unit price to avoid bankrun thinking we've processed the same transaction multiple times + ComputeBudgetProgram.setComputeUnitPrice({ + microLamports: i, + }), + await ammClient.crankThatTwapIx(failAmm).instruction(), + ]) + .rpc(); + } + + // Finalize the proposal with a pass outcome + await autocratClient.finalizeProposal(proposal); + }); + + it("removes proposal liquidity successfully", async function () { + // Get initial pool state + const initialSlPool = await sharedLiquidityManagerClient.getSlPool(slPool); + assert.isNotNull(initialSlPool.activeProposal); + + // Remove proposal liquidity using lookup table pattern + let removeProposalLiquidityTx = await sharedLiquidityManagerClient + .removeProposalLiquidityIx( + dao, + spotPool, + META, + USDC, + proposalNonce, + 100, + 0 + ) + .transaction(); + + const lookupTable = await createLookupTableForTransaction( + removeProposalLiquidityTx, + this + ); + + const messageV0Remove = new TransactionMessage({ + payerKey: this.payer.publicKey, + recentBlockhash: (await this.banksClient.getLatestBlockhash())[0], + instructions: [ + ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }), + ComputeBudgetProgram.requestHeapFrame({ bytes: 256 * 1024 }), + ].concat(removeProposalLiquidityTx.instructions), + }).compileToV0Message([lookupTable]); + + let removeTx = new VersionedTransaction(messageV0Remove); + removeTx.sign([this.payer]); + await this.banksClient.processTransaction(removeTx); + + // Check that the shared liquidity pool was updated + const finalSlPool = await sharedLiquidityManagerClient.getSlPool(slPool); + assert.isNull(finalSlPool.activeProposal); + }); +} diff --git a/tests/sharedLiquidityManager/unit/stakeToDraftProposal.test.ts b/tests/sharedLiquidityManager/unit/stakeToDraftProposal.test.ts new file mode 100644 index 000000000..fb38c2e99 --- /dev/null +++ b/tests/sharedLiquidityManager/unit/stakeToDraftProposal.test.ts @@ -0,0 +1,336 @@ +import { + SharedLiquidityManagerClient, + AutocratClient, + getDraftProposalAddr, + getStakeRecordAddr, + getSharedLiquidityPoolAddr, +} from "@metadaoproject/futarchy/v0.4"; +import { PublicKey, Keypair, ComputeBudgetProgram } from "@solana/web3.js"; +import { assert } from "chai"; +import { createMint, getAccount } from "spl-token-bankrun"; +import { BN } from "bn.js"; +import * as token from "@solana/spl-token"; +import { DAY_IN_SLOTS, expectError } from "../../utils.js"; + +export default function suite() { + let sharedLiquidityManagerClient: SharedLiquidityManagerClient; + let autocratClient: AutocratClient; + let META: PublicKey; + let USDC: PublicKey; + let dao: PublicKey; + let slPool: PublicKey; + + before(async function () { + sharedLiquidityManagerClient = this.sharedLiquidityManagerClient; + autocratClient = this.autocratClient; + }); + + beforeEach(async function () { + // Create fresh test tokens for each test to avoid address collisions + META = await createMint( + this.banksClient, + this.payer, + this.payer.publicKey, + this.payer.publicKey, + 9 + ); + USDC = await createMint( + this.banksClient, + this.payer, + this.payer.publicKey, + this.payer.publicKey, + 6 + ); + + // Create token accounts and mint tokens + await this.createTokenAccount(META, this.payer.publicKey); + await this.createTokenAccount(USDC, this.payer.publicKey); + await this.mintTo(META, this.payer.publicKey, this.payer, 100 * 10 ** 9); + await this.mintTo( + USDC, + this.payer.publicKey, + this.payer, + 100_000 * 10 ** 6 + ); + + // Initialize DAO + dao = await autocratClient.initializeDao( + META, + 1000, + 10, + 10_000, + USDC, + undefined, + new BN(DAY_IN_SLOTS.toString()) + ); + + // Initialize shared liquidity pool + await sharedLiquidityManagerClient + .initializeSharedLiquidityPoolIx( + dao, + META, + USDC, + new BN(25 * 10 ** 9), + new BN(25_000 * 10 ** 6) + ) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 }), + ]) + .rpc(); + + [slPool] = getSharedLiquidityPoolAddr( + sharedLiquidityManagerClient.getProgramId(), + dao, + this.payer.publicKey, + 100 + ); + }); + + it("stakes tokens to draft proposal", async function () { + const nonce = new BN(5001); + await sharedLiquidityManagerClient + .initializeDraftProposalIx( + slPool, + META, + { + programId: META, + accounts: [], + data: Buffer.from([]), + }, + nonce + ) + .rpc(); + + const [draftProposal] = getDraftProposalAddr( + sharedLiquidityManagerClient.getProgramId(), + nonce + ); + + const stakeAmount = new BN(1_000_000_000); // 1 META + + const initialBalance = ( + await getAccount( + this.banksClient, + token.getAssociatedTokenAddressSync(META, this.payer.publicKey) + ) + ).amount; + + await sharedLiquidityManagerClient + .stakeToDraftProposalIx(draftProposal, META, stakeAmount) + .rpc(); + + // Check stake record + const [stakeRecord] = getStakeRecordAddr( + sharedLiquidityManagerClient.getProgramId(), + draftProposal, + this.payer.publicKey + ); + + const storedStakeRecord = + await sharedLiquidityManagerClient.program.account.stakeRecord.fetch( + stakeRecord + ); + assert.ok(storedStakeRecord.staker.equals(this.payer.publicKey)); + assert.equal(storedStakeRecord.amount.toString(), stakeAmount.toString()); + + // Check draft proposal updated + const storedDraftProposal = + await sharedLiquidityManagerClient.program.account.draftProposal.fetch( + draftProposal + ); + assert.equal( + storedDraftProposal.stakedTokenAmount.toString(), + stakeAmount.toString() + ); + + // Check user balance decreased + const finalBalance = ( + await getAccount( + this.banksClient, + token.getAssociatedTokenAddressSync(META, this.payer.publicKey) + ) + ).amount; + + assert.equal( + Number(finalBalance), + Number(initialBalance) - Number(stakeAmount) + ); + }); + + it("allows multiple stakes from same user", async function () { + const nonce = new BN(5002); + await sharedLiquidityManagerClient + .initializeDraftProposalIx( + slPool, + META, + { + programId: META, + accounts: [], + data: Buffer.from([]), + }, + nonce + ) + .rpc(); + + const [draftProposal] = getDraftProposalAddr( + sharedLiquidityManagerClient.getProgramId(), + nonce + ); + + const firstStake = new BN(500_000_000); // 0.5 META + const secondStake = new BN(300_000_000); // 0.3 META + const totalStake = firstStake.add(secondStake); + + // First stake + await sharedLiquidityManagerClient + .stakeToDraftProposalIx(draftProposal, META, firstStake) + .rpc(); + + // Second stake + await sharedLiquidityManagerClient + .stakeToDraftProposalIx(draftProposal, META, secondStake) + .rpc(); + + // Check accumulated stake record + const [stakeRecord] = getStakeRecordAddr( + sharedLiquidityManagerClient.getProgramId(), + draftProposal, + this.payer.publicKey + ); + + const storedStakeRecord = + await sharedLiquidityManagerClient.program.account.stakeRecord.fetch( + stakeRecord + ); + assert.equal(storedStakeRecord.amount.toString(), totalStake.toString()); + + // Check draft proposal total + const storedDraftProposal = + await sharedLiquidityManagerClient.program.account.draftProposal.fetch( + draftProposal + ); + assert.equal( + storedDraftProposal.stakedTokenAmount.toString(), + totalStake.toString() + ); + }); + + it("fails with insufficient balance", async function () { + const nonce = new BN(5003); + await sharedLiquidityManagerClient + .initializeDraftProposalIx( + slPool, + META, + { + programId: META, + accounts: [], + data: Buffer.from([]), + }, + nonce + ) + .rpc(); + + const [draftProposal] = getDraftProposalAddr( + sharedLiquidityManagerClient.getProgramId(), + nonce + ); + + const stakeAmount = new BN(200 * 10 ** 9); // More than user has + + const callbacks = expectError( + "InsufficientFunds", + "should fail with insufficient balance" + ); + + await sharedLiquidityManagerClient + .stakeToDraftProposalIx(draftProposal, META, stakeAmount) + .rpc() + .then(callbacks[0], callbacks[1]); + }); + + it("allows stakes from multiple users", async function () { + const nonce = new BN(5004); + await sharedLiquidityManagerClient + .initializeDraftProposalIx( + slPool, + META, + { + programId: META, + accounts: [], + data: Buffer.from([]), + }, + nonce + ) + .rpc(); + + const [draftProposal] = getDraftProposalAddr( + sharedLiquidityManagerClient.getProgramId(), + nonce + ); + + // Create second user + const secondUser = Keypair.generate(); + await this.createTokenAccount(META, secondUser.publicKey); + await this.mintTo(META, secondUser.publicKey, this.payer, 10 * 10 ** 9); + + const firstUserStake = new BN(1_000_000_000); // 1 META + const secondUserStake = new BN(2_000_000_000); // 2 META + + // First user stakes + await sharedLiquidityManagerClient + .stakeToDraftProposalIx(draftProposal, META, firstUserStake) + .rpc(); + + // Second user stakes + await sharedLiquidityManagerClient + .stakeToDraftProposalIx( + draftProposal, + META, + secondUserStake, + secondUser.publicKey + ) + .signers([secondUser]) + .rpc(); + + // Check both stake records exist + const [firstStakeRecord] = getStakeRecordAddr( + sharedLiquidityManagerClient.getProgramId(), + draftProposal, + this.payer.publicKey + ); + + const [secondStakeRecord] = getStakeRecordAddr( + sharedLiquidityManagerClient.getProgramId(), + draftProposal, + secondUser.publicKey + ); + + const storedFirstStakeRecord = + await sharedLiquidityManagerClient.program.account.stakeRecord.fetch( + firstStakeRecord + ); + const storedSecondStakeRecord = + await sharedLiquidityManagerClient.program.account.stakeRecord.fetch( + secondStakeRecord + ); + + assert.equal( + storedFirstStakeRecord.amount.toString(), + firstUserStake.toString() + ); + assert.equal( + storedSecondStakeRecord.amount.toString(), + secondUserStake.toString() + ); + + // Check total in draft proposal + const storedDraftProposal = + await sharedLiquidityManagerClient.program.account.draftProposal.fetch( + draftProposal + ); + assert.equal( + storedDraftProposal.stakedTokenAmount.toString(), + firstUserStake.add(secondUserStake).toString() + ); + }); +} diff --git a/tests/sharedLiquidityManager/unit/unstakeFromDraftProposal.test.ts b/tests/sharedLiquidityManager/unit/unstakeFromDraftProposal.test.ts new file mode 100644 index 000000000..e798ea767 --- /dev/null +++ b/tests/sharedLiquidityManager/unit/unstakeFromDraftProposal.test.ts @@ -0,0 +1,358 @@ +import { + SharedLiquidityManagerClient, + AutocratClient, + getDraftProposalAddr, + getStakeRecordAddr, + getSharedLiquidityPoolAddr, +} from "@metadaoproject/futarchy/v0.4"; +import { PublicKey, Keypair, ComputeBudgetProgram } from "@solana/web3.js"; +import { assert } from "chai"; +import { createMint, getAccount } from "spl-token-bankrun"; +import { BN } from "bn.js"; +import * as token from "@solana/spl-token"; +import { DAY_IN_SLOTS, expectError } from "../../utils.js"; + +export default function suite() { + let sharedLiquidityManagerClient: SharedLiquidityManagerClient; + let autocratClient: AutocratClient; + let META: PublicKey; + let USDC: PublicKey; + let dao: PublicKey; + let slPool: PublicKey; + let draftProposal: PublicKey; + + before(async function () { + sharedLiquidityManagerClient = this.sharedLiquidityManagerClient; + autocratClient = this.autocratClient; + }); + + beforeEach(async function () { + // Create fresh test tokens for each test to avoid address collisions + META = await createMint( + this.banksClient, + this.payer, + this.payer.publicKey, + this.payer.publicKey, + 9 + ); + USDC = await createMint( + this.banksClient, + this.payer, + this.payer.publicKey, + this.payer.publicKey, + 6 + ); + + // Create token accounts and mint tokens + await this.createTokenAccount(META, this.payer.publicKey); + await this.createTokenAccount(USDC, this.payer.publicKey); + await this.mintTo(META, this.payer.publicKey, this.payer, 100 * 10 ** 9); + await this.mintTo( + USDC, + this.payer.publicKey, + this.payer, + 100_000 * 10 ** 6 + ); + + // Initialize common components + dao = await autocratClient.initializeDao( + META, + 1000, + 10, + 10_000, + USDC, + undefined, + new BN(DAY_IN_SLOTS.toString()) + ); + + await sharedLiquidityManagerClient + .initializeSharedLiquidityPoolIx( + dao, + META, + USDC, + new BN(25 * 10 ** 9), + new BN(25_000 * 10 ** 6) + ) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ units: 500_000 }), + ]) + .rpc(); + + [slPool] = getSharedLiquidityPoolAddr( + sharedLiquidityManagerClient.getProgramId(), + dao, + this.payer.publicKey, + 100 + ); + + const nonce = new BN(Math.floor(Math.random() * 1000000)); + await sharedLiquidityManagerClient + .initializeDraftProposalIx( + slPool, + META, + { + programId: META, + accounts: [], + data: Buffer.from([]), + }, + nonce + ) + .rpc(); + + [draftProposal] = getDraftProposalAddr( + sharedLiquidityManagerClient.getProgramId(), + nonce + ); + }); + + it("unstakes partial amount from draft proposal", async function () { + // Stake initial tokens + await sharedLiquidityManagerClient + .stakeToDraftProposalIx(draftProposal, META, new BN(5_000_000_000)) // 5 META + .rpc(); + + const unstakeAmount = new BN(2_000_000_000); // 2 META + const remainingStake = new BN(3_000_000_000); // 3 META + + const initialBalance = ( + await getAccount( + this.banksClient, + token.getAssociatedTokenAddressSync(META, this.payer.publicKey) + ) + ).amount; + + await sharedLiquidityManagerClient + .unstakeFromDraftProposalIx(draftProposal, META, unstakeAmount) + .rpc(); + + // Check stake record updated + const [stakeRecord] = getStakeRecordAddr( + sharedLiquidityManagerClient.getProgramId(), + draftProposal, + this.payer.publicKey + ); + + const storedStakeRecord = + await sharedLiquidityManagerClient.program.account.stakeRecord.fetch( + stakeRecord + ); + assert.equal( + storedStakeRecord.amount.toString(), + remainingStake.toString() + ); + + // Check draft proposal updated + const storedDraftProposal = + await sharedLiquidityManagerClient.program.account.draftProposal.fetch( + draftProposal + ); + assert.equal( + storedDraftProposal.stakedTokenAmount.toString(), + remainingStake.toString() + ); + + // Check user balance increased + const finalBalance = ( + await getAccount( + this.banksClient, + token.getAssociatedTokenAddressSync(META, this.payer.publicKey) + ) + ).amount; + + assert.equal( + Number(finalBalance), + Number(initialBalance) + Number(unstakeAmount) + ); + }); + + it("unstakes full amount from draft proposal", async function () { + // Stake initial tokens + await sharedLiquidityManagerClient + .stakeToDraftProposalIx(draftProposal, META, new BN(5_000_000_000)) // 5 META + .rpc(); + + const unstakeAmount = new BN(5_000_000_000); // All 5 META + + const initialBalance = ( + await getAccount( + this.banksClient, + token.getAssociatedTokenAddressSync(META, this.payer.publicKey) + ) + ).amount; + + await sharedLiquidityManagerClient + .unstakeFromDraftProposalIx(draftProposal, META, unstakeAmount) + .rpc(); + + // Check stake record updated to zero + const [stakeRecord] = getStakeRecordAddr( + sharedLiquidityManagerClient.getProgramId(), + draftProposal, + this.payer.publicKey + ); + + const storedStakeRecord = + await sharedLiquidityManagerClient.program.account.stakeRecord.fetch( + stakeRecord + ); + assert.equal(storedStakeRecord.amount.toString(), "0"); + + // Check draft proposal updated to zero + const storedDraftProposal = + await sharedLiquidityManagerClient.program.account.draftProposal.fetch( + draftProposal + ); + assert.equal(storedDraftProposal.stakedTokenAmount.toString(), "0"); + + // Check user balance increased by full amount + const finalBalance = ( + await getAccount( + this.banksClient, + token.getAssociatedTokenAddressSync(META, this.payer.publicKey) + ) + ).amount; + + assert.equal( + Number(finalBalance), + Number(initialBalance) + Number(unstakeAmount) + ); + }); + + it("fails when unstaking more than staked", async function () { + // Stake initial tokens + await sharedLiquidityManagerClient + .stakeToDraftProposalIx(draftProposal, META, new BN(5_000_000_000)) // 5 META + .rpc(); + + const unstakeAmount = new BN(6_000_000_000); // More than the 5 META staked + + const callbacks = expectError( + "InsufficientStake", + "should fail with insufficient stake" + ); + + await sharedLiquidityManagerClient + .unstakeFromDraftProposalIx(draftProposal, META, unstakeAmount) + .rpc() + .then(callbacks[0], callbacks[1]); + }); + + it("handles unstaking when multiple users have staked", async function () { + // Stake initial tokens from first user + const firstUserStake = new BN(5_000_000_000); // 5 META + await sharedLiquidityManagerClient + .stakeToDraftProposalIx(draftProposal, META, firstUserStake) + .rpc(); + + // Create second user and have them stake + const secondUser = Keypair.generate(); + await this.createTokenAccount(META, secondUser.publicKey); + await this.mintTo(META, secondUser.publicKey, this.payer, 10 * 10 ** 9); + + const secondUserStake = new BN(3_000_000_000); // 3 META + await sharedLiquidityManagerClient + .stakeToDraftProposalIx( + draftProposal, + META, + secondUserStake, + secondUser.publicKey + ) + .signers([secondUser]) + .rpc(); + + // Record initial balances + const firstUserInitialBalance = ( + await getAccount( + this.banksClient, + token.getAssociatedTokenAddressSync(META, this.payer.publicKey) + ) + ).amount; + + const secondUserInitialBalance = ( + await getAccount( + this.banksClient, + token.getAssociatedTokenAddressSync(META, secondUser.publicKey) + ) + ).amount; + + // First user unstakes partially + const firstUserUnstakeAmount = new BN(2_000_000_000); // 2 META + await sharedLiquidityManagerClient + .unstakeFromDraftProposalIx(draftProposal, META, firstUserUnstakeAmount) + .rpc(); + + // Second user unstakes partially + const secondUserUnstakeAmount = new BN(1_000_000_000); // 1 META + await sharedLiquidityManagerClient + .unstakeFromDraftProposalIx( + draftProposal, + META, + secondUserUnstakeAmount, + secondUser.publicKey + ) + .signers([secondUser]) + .rpc(); + + // Check first user's stake record + const [firstStakeRecord] = getStakeRecordAddr( + sharedLiquidityManagerClient.getProgramId(), + draftProposal, + this.payer.publicKey + ); + + const storedFirstStakeRecord = + await sharedLiquidityManagerClient.program.account.stakeRecord.fetch( + firstStakeRecord + ); + assert.equal(storedFirstStakeRecord.amount.toString(), "3000000000"); // 3 META remaining (5 - 2) + + // Check second user's stake record + const [secondStakeRecord] = getStakeRecordAddr( + sharedLiquidityManagerClient.getProgramId(), + draftProposal, + secondUser.publicKey + ); + + const storedSecondStakeRecord = + await sharedLiquidityManagerClient.program.account.stakeRecord.fetch( + secondStakeRecord + ); + assert.equal(storedSecondStakeRecord.amount.toString(), "2000000000"); // 2 META remaining (3 - 1) + + // Check total in draft proposal (3 + 2 = 5 META) + const storedDraftProposal = + await sharedLiquidityManagerClient.program.account.draftProposal.fetch( + draftProposal + ); + assert.equal( + storedDraftProposal.stakedTokenAmount.toString(), + "5000000000" + ); + + // Check first user's balance increased + const firstUserFinalBalance = ( + await getAccount( + this.banksClient, + token.getAssociatedTokenAddressSync(META, this.payer.publicKey) + ) + ).amount; + + assert.equal( + Number(firstUserFinalBalance), + Number(firstUserInitialBalance) + Number(firstUserUnstakeAmount) + ); + + // Check second user's balance increased + const secondUserFinalBalance = ( + await getAccount( + this.banksClient, + token.getAssociatedTokenAddressSync(META, secondUser.publicKey) + ) + ).amount; + + assert.equal( + Number(secondUserFinalBalance), + Number(secondUserInitialBalance) + Number(secondUserUnstakeAmount) + ); + }); +} diff --git a/tests/sharedLiquidityManager/unit/withdrawSharedLiquidity.test.ts b/tests/sharedLiquidityManager/unit/withdrawSharedLiquidity.test.ts new file mode 100644 index 000000000..d1c66c049 --- /dev/null +++ b/tests/sharedLiquidityManager/unit/withdrawSharedLiquidity.test.ts @@ -0,0 +1,347 @@ +import { + SharedLiquidityManagerClient, + AutocratClient, + getSharedLiquidityPoolAddr, + getSpotPoolAddr, + getSlPoolPositionAddr, + getRaydiumCpmmLpMintAddr, +} from "@metadaoproject/futarchy/v0.4"; +import { PublicKey, ComputeBudgetProgram, Keypair } from "@solana/web3.js"; +import { assert } from "chai"; +import { createMint, getAccount } from "spl-token-bankrun"; +import { BN } from "bn.js"; +import * as token from "@solana/spl-token"; +import { DAY_IN_SLOTS, expectError } from "../../utils.js"; + +export default function suite() { + let sharedLiquidityManagerClient: SharedLiquidityManagerClient; + let autocratClient: AutocratClient; + let META: PublicKey; + let USDC: PublicKey; + let dao: PublicKey; + let slPool: PublicKey; + let spotPool: PublicKey; + + before(async function () { + sharedLiquidityManagerClient = this.sharedLiquidityManagerClient; + autocratClient = this.autocratClient; + }); + + beforeEach(async function () { + // Create fresh test tokens for each test to avoid address collisions + META = await createMint( + this.banksClient, + this.payer, + this.payer.publicKey, + this.payer.publicKey, + 9 + ); + USDC = await createMint( + this.banksClient, + this.payer, + this.payer.publicKey, + this.payer.publicKey, + 6 + ); + + // Create token accounts and mint tokens + await this.createTokenAccount(META, this.payer.publicKey); + await this.createTokenAccount(USDC, this.payer.publicKey); + await this.mintTo(META, this.payer.publicKey, this.payer, 100 * 10 ** 9); + await this.mintTo( + USDC, + this.payer.publicKey, + this.payer, + 100_000 * 10 ** 6 + ); + + dao = await autocratClient.initializeDao( + META, + 1000, + 10, + 10_000, + USDC, + undefined, + new BN(DAY_IN_SLOTS.toString()) + ); + + // Initialize shared liquidity pool + await sharedLiquidityManagerClient + .initializeSharedLiquidityPoolIx( + dao, + META, + USDC, + new BN(25 * 10 ** 9), + new BN(25_000 * 10 ** 6) + ) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 }), + ]) + .rpc(); + + // Calculate pool addresses + [slPool] = getSharedLiquidityPoolAddr( + sharedLiquidityManagerClient.getProgramId(), + dao, + this.payer.publicKey, + 100 + ); + + [spotPool] = getSpotPoolAddr( + sharedLiquidityManagerClient.getProgramId(), + slPool, + 0 + ); + }); + + it("withdraws liquidity from shared pool", async function () { + const user = Keypair.generate(); + await this.createTokenAccount(META, user.publicKey); + await this.createTokenAccount(USDC, user.publicKey); + await this.mintTo(META, user.publicKey, this.payer, 100 * 10 ** 9); + await this.mintTo(USDC, user.publicKey, this.payer, 100_000 * 10 ** 6); + + // First deposit some liquidity + const depositLpTokenAmount = new BN(1_000_000); // 1 LP token + const maxBaseAmount = new BN(1 * 10 ** 9); // 1 META max + const maxQuoteAmount = new BN(1_000 * 10 ** 6); // 1,000 USDC max + + await sharedLiquidityManagerClient + .depositSharedLiquidityIx( + slPool, + spotPool, + META, + USDC, + depositLpTokenAmount, + maxBaseAmount, + maxQuoteAmount, + user.publicKey + ) + .signers([user]) + .rpc(); + + // Get initial balances + const initialBaseBalance = await this.getTokenBalance(META, user.publicKey); + const initialQuoteBalance = await this.getTokenBalance( + USDC, + user.publicKey + ); + + // Now withdraw some liquidity + const withdrawLpTokenAmount = new BN(500_000); // 0.5 LP tokens + const minimumToken0Amount = new BN(0); + const minimumToken1Amount = new BN(0); + + await sharedLiquidityManagerClient + .withdrawSharedLiquidityIx( + slPool, + spotPool, + META, + USDC, + withdrawLpTokenAmount, + minimumToken0Amount, + minimumToken1Amount, + user.publicKey + ) + .signers([user]) + .rpc(); + + // Check that user received tokens back + const finalBaseBalance = await this.getTokenBalance(META, user.publicKey); + const finalQuoteBalance = await this.getTokenBalance(USDC, user.publicKey); + + assert.isAbove(Number(finalBaseBalance), Number(initialBaseBalance)); + assert.isAbove(Number(finalQuoteBalance), Number(initialQuoteBalance)); + + // Check position was updated + const position = await sharedLiquidityManagerClient.getSlPoolPosition( + getSlPoolPositionAddr( + sharedLiquidityManagerClient.getProgramId(), + slPool, + user.publicKey + )[0] + ); + + const expectedRemainingShares = depositLpTokenAmount.sub( + withdrawLpTokenAmount + ); + assert.equal( + position.underlyingSpotLpShares.toString(), + expectedRemainingShares.toString() + ); + }); + + it("fails when pool is in use by active proposal", async function () { + const user = Keypair.generate(); + await this.createTokenAccount(META, user.publicKey); + await this.createTokenAccount(USDC, user.publicKey); + await this.mintTo(META, user.publicKey, this.payer, 100 * 10 ** 9); + await this.mintTo(USDC, user.publicKey, this.payer, 100_000 * 10 ** 6); + + // First deposit some liquidity + const depositLpTokenAmount = new BN(1_000_000); + const maxBaseAmount = new BN(1 * 10 ** 9); + const maxQuoteAmount = new BN(1_000 * 10 ** 6); + + await sharedLiquidityManagerClient + .depositSharedLiquidityIx( + slPool, + spotPool, + META, + USDC, + depositLpTokenAmount, + maxBaseAmount, + maxQuoteAmount, + user.publicKey + ) + .signers([user]) + .rpc(); + + // Simulate pool being in use by setting active_proposal + // This would normally be set by the program, but for testing we'll mock it + const slPoolAccount = await sharedLiquidityManagerClient.getSlPool(slPool); + // Note: In a real scenario, this would be set by the program when a proposal is active + + const withdrawLpTokenAmount = new BN(500_000); + const minimumToken0Amount = new BN(0); + const minimumToken1Amount = new BN(0); + + // This test would need to be updated when we have a way to set the pool as "in use" + // For now, we'll test the basic withdrawal functionality + await sharedLiquidityManagerClient + .withdrawSharedLiquidityIx( + slPool, + spotPool, + META, + USDC, + withdrawLpTokenAmount, + minimumToken0Amount, + minimumToken1Amount, + user.publicKey + ) + .signers([user]) + .rpc(); + }); + + it("fails with insufficient LP shares", async function () { + const user = Keypair.generate(); + await this.createTokenAccount(META, user.publicKey); + await this.createTokenAccount(USDC, user.publicKey); + await this.mintTo(META, user.publicKey, this.payer, 100 * 10 ** 9); + await this.mintTo(USDC, user.publicKey, this.payer, 100_000 * 10 ** 6); + + // First deposit some liquidity + const depositLpTokenAmount = new BN(1_000_000); + const maxBaseAmount = new BN(1 * 10 ** 9); + const maxQuoteAmount = new BN(1_000 * 10 ** 6); + + await sharedLiquidityManagerClient + .depositSharedLiquidityIx( + slPool, + spotPool, + META, + USDC, + depositLpTokenAmount, + maxBaseAmount, + maxQuoteAmount, + user.publicKey + ) + .signers([user]) + .rpc(); + + // Try to withdraw more than we have + const withdrawLpTokenAmount = new BN(2_000_000); // More than deposited + const minimumToken0Amount = new BN(0); + const minimumToken1Amount = new BN(0); + + const callbacks = expectError( + "InsufficientLpShares", + "Should have thrown error for insufficient LP shares" + ); + + await sharedLiquidityManagerClient + .withdrawSharedLiquidityIx( + slPool, + spotPool, + META, + USDC, + withdrawLpTokenAmount, + minimumToken0Amount, + minimumToken1Amount, + user.publicKey + ) + .signers([user]) + .rpc() + .then(callbacks[0], callbacks[1]); + }); + + it("fails when user is not position owner", async function () { + const user1 = Keypair.generate(); + const user2 = Keypair.generate(); + + await this.createTokenAccount(META, user1.publicKey); + await this.createTokenAccount(USDC, user1.publicKey); + await this.createTokenAccount(META, user2.publicKey); + await this.createTokenAccount(USDC, user2.publicKey); + + await this.mintTo(META, user1.publicKey, this.payer, 100 * 10 ** 9); + await this.mintTo(USDC, user1.publicKey, this.payer, 100_000 * 10 ** 6); + await this.mintTo(META, user2.publicKey, this.payer, 100 * 10 ** 9); + await this.mintTo(USDC, user2.publicKey, this.payer, 100_000 * 10 ** 6); + + // User1 deposits liquidity + const depositLpTokenAmount = new BN(1_000_000); + const maxBaseAmount = new BN(1 * 10 ** 9); + const maxQuoteAmount = new BN(1_000 * 10 ** 6); + + await sharedLiquidityManagerClient + .depositSharedLiquidityIx( + slPool, + spotPool, + META, + USDC, + depositLpTokenAmount, + maxBaseAmount, + maxQuoteAmount, + user1.publicKey + ) + .signers([user1]) + .rpc(); + + // User2 tries to withdraw user1's liquidity + const withdrawLpTokenAmount = new BN(500_000); + const minimumToken0Amount = new BN(0); + const minimumToken1Amount = new BN(0); + + const callbacks = expectError( + "ConstraintSeeds", + "Should have thrown error for unauthorized user" + ); + + const spotPoolLpMint = await getRaydiumCpmmLpMintAddr(spotPool, false)[0]; + + await this.createTokenAccount(spotPoolLpMint, user2.publicKey); + + await sharedLiquidityManagerClient + .withdrawSharedLiquidityIx( + slPool, + spotPool, + META, + USDC, + withdrawLpTokenAmount, + minimumToken0Amount, + minimumToken1Amount, + user2.publicKey + ) + .accounts({ + userSlPoolPosition: getSlPoolPositionAddr( + sharedLiquidityManagerClient.getProgramId(), + slPool, + user1.publicKey + )[0], + }) + .signers([user2]) + .rpc() + .then(callbacks[0], callbacks[1]); + }); +} diff --git a/tests/utils.ts b/tests/utils.ts index fb0abc86a..284c191b5 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -1,13 +1,124 @@ import { assert } from "chai"; import { Clock, ProgramTestContext } from "solana-bankrun"; import { BN } from "bn.js"; +import { + AddressLookupTableAccount, + AddressLookupTableProgram, + Keypair, + PublicKey, + Transaction, +} from "@solana/web3.js"; export const TEN_SECONDS_IN_SLOTS = 25n; export const ONE_MINUTE_IN_SLOTS = TEN_SECONDS_IN_SLOTS * 6n; export const HOUR_IN_SLOTS = ONE_MINUTE_IN_SLOTS * 60n; export const DAY_IN_SLOTS = HOUR_IN_SLOTS * 24n; -export const toBN = (val: bigint): typeof BN.prototype => new BN(val.toString()); +export const toBN = (val: bigint): typeof BN.prototype => + new BN(val.toString()); + +/** + * Creates a lookup table for all unique accounts in a transaction + * @param transaction - The transaction to create a lookup table for + * @param context - Test context containing banksClient, payer, and advanceBySlots + * @param additionalAddresses - Optional additional addresses to include in the lookup table + * @returns Promise - The created lookup table account + */ +export async function createLookupTableForTransaction( + transaction: Transaction, + context: { + banksClient: any; + payer: Keypair; + advanceBySlots: (slots: bigint) => Promise; + }, + additionalAddresses: PublicKey[] = [] +): Promise { + // use a different authority for the lookup table to avoid conflicts + const lookupAuthority = Keypair.generate(); + const slot = await context.banksClient.getSlot(); + + const [createTableIx, lookupTableAddress] = + AddressLookupTableProgram.createLookupTable({ + authority: lookupAuthority.publicKey, + payer: context.payer.publicKey, + recentSlot: slot - 1n, + }); + + // Extract all unique accounts from the transaction + const accountsToAdd = transaction.instructions.map((instruction) => + instruction.keys.map((key) => key.pubkey) + ); + const uniqueAccounts = [...new Set(accountsToAdd.flat())] as PublicKey[]; + console.log("uniqueAccounts", uniqueAccounts.length); + + // Add any additional addresses + const allAddresses = [...uniqueAccounts, ...additionalAddresses]; + const finalUniqueAddresses = [...new Set(allAddresses)] as PublicKey[]; + + // Create the lookup table + let createLutTx = new Transaction().add(createTableIx); + createLutTx.recentBlockhash = ( + await context.banksClient.getLatestBlockhash() + )[0]; + createLutTx.feePayer = context.payer.publicKey; + createLutTx.sign(context.payer, lookupAuthority); + // createLutTx.partialSign(lookupAuthority); + + await context.banksClient.processTransaction(createLutTx); + await context.advanceBySlots(1n); + + // Extend the lookup table with all unique accounts + const addressesPerExtend = 20; + for (let i = 0; i < finalUniqueAddresses.length; i += addressesPerExtend) { + const batch = finalUniqueAddresses.slice(i, i + addressesPerExtend); + + const extendTableIx = AddressLookupTableProgram.extendLookupTable({ + authority: lookupAuthority.publicKey, + payer: context.payer.publicKey, + lookupTable: lookupTableAddress, + addresses: batch, + }); + + let extendLutTx = new Transaction().add(extendTableIx); + extendLutTx.recentBlockhash = ( + await context.banksClient.getLatestBlockhash() + )[0]; + extendLutTx.feePayer = context.payer.publicKey; + extendLutTx.sign(context.payer, lookupAuthority); + + await context.banksClient.processTransaction(extendLutTx); + await context.advanceBySlots(1n); + } + + // Add a dummy account to ensure the lookup table has enough entries for all indexes + const dummyAccount = Keypair.generate().publicKey; + const extendTableIx = AddressLookupTableProgram.extendLookupTable({ + authority: lookupAuthority.publicKey, + payer: context.payer.publicKey, + lookupTable: lookupTableAddress, + addresses: [dummyAccount], + }); + + let extendLutTx = new Transaction().add(extendTableIx); + extendLutTx.recentBlockhash = ( + await context.banksClient.getLatestBlockhash() + )[0]; + extendLutTx.feePayer = context.payer.publicKey; + extendLutTx.sign(context.payer, lookupAuthority); + + await context.banksClient.processTransaction(extendLutTx); + await context.advanceBySlots(1n); + + // Fetch and return the lookup table account + let rawStoredLookupTable = await context.banksClient.getAccount( + lookupTableAddress + ); + + return new AddressLookupTableAccount({ + key: lookupTableAddress, + state: AddressLookupTableAccount.deserialize(rawStoredLookupTable.data), + }); +} export const expectError = ( expectedError: string, diff --git a/yarn.lock b/yarn.lock index 338ecc656..409b49647 100644 --- a/yarn.lock +++ b/yarn.lock @@ -786,16 +786,17 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" -"@metadaoproject/futarchy@0.4.0-alpha.73": - version "0.4.0-alpha.73" - resolved "https://registry.yarnpkg.com/@metadaoproject/futarchy/-/futarchy-0.4.0-alpha.73.tgz#c422acf5bc0c45ff1c95183354bc04a87ee59c6e" - integrity sha512-erQ+jahuKy14wmTRza0YtVdKoEv/oYBo4QnHF4hZq0tAlJeJyszWZLNn8ItdHHtuKBypN+R3+bdy6kCvVCyllg== +"@metadaoproject/futarchy@0.4.0-alpha.74": + version "0.4.0-alpha.74" + resolved "https://registry.yarnpkg.com/@metadaoproject/futarchy/-/futarchy-0.4.0-alpha.74.tgz#f43d7fa41f9d49a6283ad3723b5db0d16d4cc8a9" + integrity sha512-CckorrrtCAAe4EyYdSD049CwNEkPpXkJdNyFJZwpbghjtgJ6dwHNpEZ7npa2yTNA+dng3rFz9KiriNHqa4H4eQ== dependencies: "@coral-xyz/anchor" "^0.29.0" "@metaplex-foundation/umi" "^0.9.2" "@metaplex-foundation/umi-bundle-defaults" "^0.9.2" "@metaplex-foundation/umi-uploader-bundlr" "^0.9.2" "@noble/hashes" "^1.4.0" + "@solana/spl-memo" "^0.2.5" "@solana/spl-token" "^0.3.7" "@solana/web3.js" "^1.74.0" bn.js "^5.2.1" @@ -1159,6 +1160,13 @@ dependencies: buffer "^6.0.3" +"@solana/spl-memo@^0.2.5": + version "0.2.5" + resolved "https://registry.yarnpkg.com/@solana/spl-memo/-/spl-memo-0.2.5.tgz#a7828cdd1e810ff77c7c015ac97dfa166d0651fe" + integrity sha512-0Zx5t3gAdcHlRTt2O3RgGlni1x7vV7Xq7j4z9q8kKOMgU03PyoTbFQ/BSYCcICHzkaqD7ZxAiaJ6dlXolg01oA== + dependencies: + buffer "^6.0.3" + "@solana/spl-token-metadata@^0.1.2": version "0.1.2" resolved "https://registry.yarnpkg.com/@solana/spl-token-metadata/-/spl-token-metadata-0.1.2.tgz#876e13432bd2960bd3cac16b9b0af63e69e37719"