From d7fc89a7dcfb66c05106d0bcb07959c66c8e78d6 Mon Sep 17 00:00:00 2001 From: Pileks Date: Tue, 20 Jan 2026 16:37:21 -0800 Subject: [PATCH 01/29] close_bid_wall should return funds to authority always --- programs/bid_wall/src/instructions/close_bid_wall.rs | 4 ++-- sdk/src/v0.7/types/bid_wall.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/programs/bid_wall/src/instructions/close_bid_wall.rs b/programs/bid_wall/src/instructions/close_bid_wall.rs index 7ff7236f5..03ec34f51 100644 --- a/programs/bid_wall/src/instructions/close_bid_wall.rs +++ b/programs/bid_wall/src/instructions/close_bid_wall.rs @@ -13,7 +13,7 @@ use crate::{ pub struct CloseBidWall<'info> { #[account( mut, - close=payer, + close=authority, has_one = authority )] pub bid_wall: Account<'info, BidWall>, @@ -22,7 +22,7 @@ pub struct CloseBidWall<'info> { pub payer: Signer<'info>, /// CHECK: used for constraints - #[account(address = bid_wall.authority)] + #[account(mut, address = bid_wall.authority)] pub authority: UncheckedAccount<'info>, /// CHECK: used for constraints diff --git a/sdk/src/v0.7/types/bid_wall.ts b/sdk/src/v0.7/types/bid_wall.ts index 9f1f49892..cb4cdc31e 100644 --- a/sdk/src/v0.7/types/bid_wall.ts +++ b/sdk/src/v0.7/types/bid_wall.ts @@ -105,7 +105,7 @@ export type BidWall = { }, { name: "authority"; - isMut: false; + isMut: true; isSigner: false; }, { @@ -829,7 +829,7 @@ export const IDL: BidWall = { }, { name: "authority", - isMut: false, + isMut: true, isSigner: false, }, { From 9d2ff64731a7914b1ce5d5b66a943bafd40670f9 Mon Sep 17 00:00:00 2001 From: Pileks Date: Tue, 20 Jan 2026 16:58:27 -0800 Subject: [PATCH 02/29] set bid wall fee recipient to metadao multisig vault everywhere --- .../src/instructions/cancel_bid_wall.rs | 5 +-- .../src/instructions/close_bid_wall.rs | 5 +-- .../bid_wall/src/instructions/collect_fees.rs | 8 +---- .../src/instructions/initialize_bid_wall.rs | 6 ++-- programs/bid_wall/src/lib.rs | 7 ++++ sdk/src/v0.7/BidWallClient.ts | 8 ++--- tests/bidWall/unit/cancelBidWall.test.ts | 33 +++++++++++++++++-- tests/bidWall/unit/closeBidWall.test.ts | 30 +++++++++++++++-- tests/bidWall/unit/initializeBidWall.test.ts | 5 +-- tests/bidWall/unit/sellTokens.test.ts | 5 --- 10 files changed, 80 insertions(+), 32 deletions(-) diff --git a/programs/bid_wall/src/instructions/cancel_bid_wall.rs b/programs/bid_wall/src/instructions/cancel_bid_wall.rs index 6cada6b03..5d0e45c20 100644 --- a/programs/bid_wall/src/instructions/cancel_bid_wall.rs +++ b/programs/bid_wall/src/instructions/cancel_bid_wall.rs @@ -3,6 +3,7 @@ use anchor_spl::token::{self, Mint, Token, TokenAccount, Transfer}; use crate::{ events::{BidWallCanceledEvent, CommonFields}, + metadao_multisig_vault, state::BidWall, usdc_mint, }; @@ -24,8 +25,8 @@ pub struct CancelBidWall<'info> { #[account(address = bid_wall.authority)] pub authority: Signer<'info>, - /// CHECK: used for constraints - #[account(address = bid_wall.fee_recipient)] + /// CHECK: the fee recipient is always the metadao multisig vault + #[account(address = metadao_multisig_vault::ID)] pub fee_recipient: UncheckedAccount<'info>, #[account(mut, associated_token::mint = quote_mint, associated_token::authority = bid_wall)] diff --git a/programs/bid_wall/src/instructions/close_bid_wall.rs b/programs/bid_wall/src/instructions/close_bid_wall.rs index 03ec34f51..2fac01379 100644 --- a/programs/bid_wall/src/instructions/close_bid_wall.rs +++ b/programs/bid_wall/src/instructions/close_bid_wall.rs @@ -4,6 +4,7 @@ use anchor_spl::token::{self, Mint, Token, TokenAccount, Transfer}; use crate::{ error::BidWallError, events::{BidWallClosedEvent, CommonFields}, + metadao_multisig_vault, state::BidWall, usdc_mint, }; @@ -25,8 +26,8 @@ pub struct CloseBidWall<'info> { #[account(mut, address = bid_wall.authority)] pub authority: UncheckedAccount<'info>, - /// CHECK: used for constraints - #[account(address = bid_wall.fee_recipient)] + /// CHECK: the fee recipient is always the metadao multisig vault + #[account(address = metadao_multisig_vault::ID)] pub fee_recipient: UncheckedAccount<'info>, #[account(mut, associated_token::mint = quote_mint, associated_token::authority = bid_wall)] diff --git a/programs/bid_wall/src/instructions/collect_fees.rs b/programs/bid_wall/src/instructions/collect_fees.rs index e049be42d..957f9fe6f 100644 --- a/programs/bid_wall/src/instructions/collect_fees.rs +++ b/programs/bid_wall/src/instructions/collect_fees.rs @@ -5,17 +5,11 @@ use anchor_spl::token::{self, Mint, Token, TokenAccount, Transfer}; use crate::error::BidWallError; use crate::{ events::{BidWallFeesCollectedEvent, CommonFields}, + metadao_multisig_vault, state::BidWall, usdc_mint, }; -pub mod metadao_multisig_vault { - use anchor_lang::prelude::declare_id; - - // MetaDAO operations multisig vault - hardcoded fee destination - declare_id!("6awyHMshBGVjJ3ozdSJdyyDE1CTAXUwrpNMaRGMsb4sf"); -} - pub mod metadao_cranker { use anchor_lang::prelude::declare_id; diff --git a/programs/bid_wall/src/instructions/initialize_bid_wall.rs b/programs/bid_wall/src/instructions/initialize_bid_wall.rs index 9f49b01e3..a503259ea 100644 --- a/programs/bid_wall/src/instructions/initialize_bid_wall.rs +++ b/programs/bid_wall/src/instructions/initialize_bid_wall.rs @@ -6,6 +6,7 @@ use anchor_spl::{ use crate::{ events::{BidWallInitializedEvent, CommonFields}, + metadao_multisig_vault, state::BidWall, usdc_mint, }; @@ -34,8 +35,9 @@ pub struct InitializeBidWall<'info> { #[account(mut)] pub payer: Signer<'info>, - /// CHECK: This is the recipient of the fees collected by the bid wall, no need to validate - pub fee_recipient: AccountInfo<'info>, + /// CHECK: The fee recipient is always the metadao multisig vault + #[account(address = metadao_multisig_vault::ID)] + pub fee_recipient: UncheckedAccount<'info>, // Creator must sign to prevent unauthorized bid wall initialization on their behalf pub creator: Signer<'info>, diff --git a/programs/bid_wall/src/lib.rs b/programs/bid_wall/src/lib.rs index 29a6a8743..05ee0eafc 100644 --- a/programs/bid_wall/src/lib.rs +++ b/programs/bid_wall/src/lib.rs @@ -34,6 +34,13 @@ pub mod usdc_mint { declare_id!("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); } +pub mod metadao_multisig_vault { + use anchor_lang::prelude::declare_id; + + // MetaDAO operations multisig vault - hardcoded fee destination + declare_id!("6awyHMshBGVjJ3ozdSJdyyDE1CTAXUwrpNMaRGMsb4sf"); +} + pub const FEE_BPS: u16 = 100; pub const TOKEN_SCALE: u64 = 1_000_000; diff --git a/sdk/src/v0.7/BidWallClient.ts b/sdk/src/v0.7/BidWallClient.ts index 347fd2182..a64babc21 100644 --- a/sdk/src/v0.7/BidWallClient.ts +++ b/sdk/src/v0.7/BidWallClient.ts @@ -62,7 +62,7 @@ export class BidWallClient { baseMint, creator = this.provider.publicKey, nonce = new BN(0), - feeRecipient, + feeRecipient = METADAO_MULTISIG_VAULT, quoteMint = MAINNET_USDC, payer = this.provider.publicKey, }: { @@ -74,7 +74,7 @@ export class BidWallClient { nonce?: BN; authority?: PublicKey; baseMint: PublicKey; - feeRecipient: PublicKey; + feeRecipient?: PublicKey; quoteMint?: PublicKey; payer?: PublicKey; }) { @@ -208,7 +208,7 @@ export class BidWallClient { bidWall, authority, baseMint, - feeRecipient = PublicKey.default, + feeRecipient = METADAO_MULTISIG_VAULT, quoteMint = MAINNET_USDC, payer = this.provider.publicKey, }: { @@ -254,7 +254,7 @@ export class BidWallClient { bidWall, authority, baseMint, - feeRecipient = PublicKey.default, + feeRecipient = METADAO_MULTISIG_VAULT, quoteMint = MAINNET_USDC, payer = this.provider.publicKey, }: { diff --git a/tests/bidWall/unit/cancelBidWall.test.ts b/tests/bidWall/unit/cancelBidWall.test.ts index 49e12b4de..034604e52 100644 --- a/tests/bidWall/unit/cancelBidWall.test.ts +++ b/tests/bidWall/unit/cancelBidWall.test.ts @@ -1,6 +1,7 @@ import { Keypair, PublicKey, + Transaction, TransactionMessage, VersionedTransaction, } from "@solana/web3.js"; @@ -11,9 +12,13 @@ import { BidWallClient, MAINNET_USDC, getBidWallAddr, + METADAO_MULTISIG_VAULT, } from "@metadaoproject/futarchy/v0.7"; import { BN } from "bn.js"; -import { getAssociatedTokenAddressSync } from "@solana/spl-token"; +import { + createAssociatedTokenAccountIdempotentInstruction, + getAssociatedTokenAddressSync, +} from "@solana/spl-token"; import { initializeMintWithSeeds } from "../utils.js"; import { createLookupTableForTransaction } from "../../utils.js"; @@ -137,8 +142,30 @@ export default function suite() { await this.getTokenBalance(MAINNET_USDC, dao), ); - feeRecipient = Keypair.generate().publicKey; - await this.createTokenAccount(MAINNET_USDC, feeRecipient); + feeRecipient = METADAO_MULTISIG_VAULT; + + const feeRecipientQuoteTokenAccount = getAssociatedTokenAddressSync( + MAINNET_USDC, + feeRecipient, + true, + ); + + const createAtaTx = new Transaction().add( + createAssociatedTokenAccountIdempotentInstruction( + this.payer.publicKey, + feeRecipientQuoteTokenAccount, + feeRecipient, + MAINNET_USDC, + ), + ); + + createAtaTx.recentBlockhash = ( + await this.banksClient.getLatestBlockhash() + )[0]; + createAtaTx.feePayer = this.payer.publicKey; + createAtaTx.sign(this.payer); + + await this.banksClient.processTransaction(createAtaTx); // Claim tokens for the payer await launchpadClient.claimIx(launch, META).rpc(); diff --git a/tests/bidWall/unit/closeBidWall.test.ts b/tests/bidWall/unit/closeBidWall.test.ts index 3507fffe0..9aa56ad97 100644 --- a/tests/bidWall/unit/closeBidWall.test.ts +++ b/tests/bidWall/unit/closeBidWall.test.ts @@ -2,6 +2,7 @@ import { ComputeBudgetProgram, Keypair, PublicKey, + Transaction, TransactionMessage, VersionedTransaction, } from "@solana/web3.js"; @@ -12,11 +13,13 @@ import { BidWallClient, MAINNET_USDC, getBidWallAddr, + METADAO_MULTISIG_VAULT, } from "@metadaoproject/futarchy/v0.7"; import { BN } from "bn.js"; import { getAssociatedTokenAddressSync } from "@solana/spl-token"; import { initializeMintWithSeeds } from "../utils.js"; import { createLookupTableForTransaction } from "../../utils.js"; +import { createAssociatedTokenAccountIdempotentInstruction } from "@solana/spl-token"; export default function suite() { let futarchyClient: FutarchyClient; @@ -138,8 +141,30 @@ export default function suite() { await this.getTokenBalance(MAINNET_USDC, dao), ); - feeRecipient = Keypair.generate().publicKey; - await this.createTokenAccount(MAINNET_USDC, feeRecipient); + feeRecipient = METADAO_MULTISIG_VAULT; + + const feeRecipientQuoteTokenAccount = getAssociatedTokenAddressSync( + MAINNET_USDC, + feeRecipient, + true, + ); + + const createAtaTx = new Transaction().add( + createAssociatedTokenAccountIdempotentInstruction( + this.payer.publicKey, + feeRecipientQuoteTokenAccount, + feeRecipient, + MAINNET_USDC, + ), + ); + + createAtaTx.recentBlockhash = ( + await this.banksClient.getLatestBlockhash() + )[0]; + createAtaTx.feePayer = this.payer.publicKey; + createAtaTx.sign(this.payer); + + await this.banksClient.processTransaction(createAtaTx); // Claim tokens for the payer await launchpadClient.claimIx(launch, META).rpc(); @@ -156,7 +181,6 @@ export default function suite() { nonce: new BN(0), daoTreasury: daoTreasury, baseMint: META, - feeRecipient, quoteMint: MAINNET_USDC, payer: this.payer.publicKey, }) diff --git a/tests/bidWall/unit/initializeBidWall.test.ts b/tests/bidWall/unit/initializeBidWall.test.ts index 56b1547ec..336e58399 100644 --- a/tests/bidWall/unit/initializeBidWall.test.ts +++ b/tests/bidWall/unit/initializeBidWall.test.ts @@ -11,6 +11,7 @@ import { BidWallClient, MAINNET_USDC, getBidWallAddr, + METADAO_MULTISIG_VAULT, } from "@metadaoproject/futarchy/v0.7"; import BN from "bn.js"; import { getAssociatedTokenAddressSync } from "@solana/spl-token"; @@ -144,9 +145,6 @@ export default function suite() { it("successfully initializes a bid wall", async function () { let durationSeconds = 100; - const feeRecipient = Keypair.generate().publicKey; - await this.createTokenAccount(MAINNET_USDC, feeRecipient); - let launchAccount = await this.launchpad_v7.fetchLaunch(launch); await bidWallClient @@ -159,7 +157,6 @@ export default function suite() { nonce: new BN(0), daoTreasury: launchAccount.daoVault, baseMint: META, - feeRecipient, quoteMint: MAINNET_USDC, payer: this.payer.publicKey, }) diff --git a/tests/bidWall/unit/sellTokens.test.ts b/tests/bidWall/unit/sellTokens.test.ts index 3c568e69e..a8beabf21 100644 --- a/tests/bidWall/unit/sellTokens.test.ts +++ b/tests/bidWall/unit/sellTokens.test.ts @@ -31,7 +31,6 @@ export default function suite() { let funderUsdcAccount: PublicKey; let secondFunder: Keypair; let bidWall: PublicKey; - let feeRecipient: PublicKey; let durationSeconds: number; before(async function () { @@ -138,9 +137,6 @@ export default function suite() { await this.getTokenBalance(MAINNET_USDC, dao), ); - feeRecipient = Keypair.generate().publicKey; - await this.createTokenAccount(MAINNET_USDC, feeRecipient); - // Claim tokens for the payer await launchpadClient.claimIx(launch, META).rpc(); @@ -156,7 +152,6 @@ export default function suite() { nonce: new BN(0), daoTreasury: daoTreasury, baseMint: META, - feeRecipient, quoteMint: MAINNET_USDC, payer: this.payer.publicKey, }) From e9ce2e2cefa39d1b016e5e1c5cede3f2015f4cde Mon Sep 17 00:00:00 2001 From: Pileks Date: Tue, 20 Jan 2026 17:38:50 -0800 Subject: [PATCH 03/29] add min_amount_out to bid wall sell instruction --- programs/bid_wall/src/error.rs | 2 ++ .../bid_wall/src/instructions/sell_tokens.rs | 12 +++++++++- sdk/src/v0.7/BidWallClient.ts | 7 +++++- sdk/src/v0.7/types/bid_wall.ts | 18 +++++++++++++++ tests/bidWall/unit/sellTokens.test.ts | 23 ++++++++++++++++++- 5 files changed, 59 insertions(+), 3 deletions(-) diff --git a/programs/bid_wall/src/error.rs b/programs/bid_wall/src/error.rs index 0e19747c6..0dd043fbe 100644 --- a/programs/bid_wall/src/error.rs +++ b/programs/bid_wall/src/error.rs @@ -16,4 +16,6 @@ pub enum BidWallError { InvalidInputAmount, #[msg("Invalid crank address")] InvalidCrankAddress, + #[msg("Insufficient output amount")] + InsufficientOutputAmount, } diff --git a/programs/bid_wall/src/instructions/sell_tokens.rs b/programs/bid_wall/src/instructions/sell_tokens.rs index 1081297c1..20e22d0a6 100644 --- a/programs/bid_wall/src/instructions/sell_tokens.rs +++ b/programs/bid_wall/src/instructions/sell_tokens.rs @@ -11,6 +11,7 @@ use anchor_spl::token::{self, Burn, Mint, Token, TokenAccount, Transfer}; #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct SellTokensArgs { pub amount_in: u64, + pub min_amount_out: u64, } #[event_cpi] @@ -85,7 +86,10 @@ impl SellTokens<'_> { } pub fn handle(ctx: Context, args: SellTokensArgs) -> Result<()> { - let SellTokensArgs { amount_in } = args; + let SellTokensArgs { + amount_in, + min_amount_out, + } = args; // We calculate the total NAV as as sum of: // - The initial quote reserves of the Futarchy AMM @@ -147,6 +151,12 @@ impl SellTokens<'_> { amount_out_after_fee, )?; + require_gte!( + amount_out_after_fee, + min_amount_out, + BidWallError::InsufficientOutputAmount + ); + // Fees can't be used for future token buys, so we subtract the quote amount before fees. ctx.accounts.bid_wall.quote_amount -= amount_out_before_fee; // Track fees collected for fee distribution. diff --git a/sdk/src/v0.7/BidWallClient.ts b/sdk/src/v0.7/BidWallClient.ts index a64babc21..862708753 100644 --- a/sdk/src/v0.7/BidWallClient.ts +++ b/sdk/src/v0.7/BidWallClient.ts @@ -118,6 +118,7 @@ export class BidWallClient { sellTokensIx({ amount, + minAmountOut = 0, bidWall, baseMint, daoTreasury, @@ -125,6 +126,7 @@ export class BidWallClient { user = this.provider.publicKey, }: { amount: number; + minAmountOut?: number; bidWall: PublicKey; baseMint: PublicKey; daoTreasury: PublicKey; @@ -156,7 +158,10 @@ export class BidWallClient { ); return this.bidWallProgram.methods - .sellTokens({ amountIn: new BN(amount) }) + .sellTokens({ + amountIn: new BN(amount), + minAmountOut: new BN(minAmountOut), + }) .accounts({ bidWall, user, diff --git a/sdk/src/v0.7/types/bid_wall.ts b/sdk/src/v0.7/types/bid_wall.ts index cb4cdc31e..438bea796 100644 --- a/sdk/src/v0.7/types/bid_wall.ts +++ b/sdk/src/v0.7/types/bid_wall.ts @@ -502,6 +502,10 @@ export type BidWall = { name: "amountIn"; type: "u64"; }, + { + name: "minAmountOut"; + type: "u64"; + }, ]; }; }, @@ -719,6 +723,11 @@ export type BidWall = { name: "InvalidCrankAddress"; msg: "Invalid crank address"; }, + { + code: 6007; + name: "InsufficientOutputAmount"; + msg: "Insufficient output amount"; + }, ]; }; @@ -1226,6 +1235,10 @@ export const IDL: BidWall = { name: "amountIn", type: "u64", }, + { + name: "minAmountOut", + type: "u64", + }, ], }, }, @@ -1443,5 +1456,10 @@ export const IDL: BidWall = { name: "InvalidCrankAddress", msg: "Invalid crank address", }, + { + code: 6007, + name: "InsufficientOutputAmount", + msg: "Insufficient output amount", + }, ], }; diff --git a/tests/bidWall/unit/sellTokens.test.ts b/tests/bidWall/unit/sellTokens.test.ts index a8beabf21..48b49fedd 100644 --- a/tests/bidWall/unit/sellTokens.test.ts +++ b/tests/bidWall/unit/sellTokens.test.ts @@ -16,7 +16,7 @@ import { import BN from "bn.js"; import { getAssociatedTokenAddressSync } from "@solana/spl-token"; import { initializeMintWithSeeds } from "../utils.js"; -import { createLookupTableForTransaction } from "../../utils.js"; +import { createLookupTableForTransaction, expectError } from "../../utils.js"; export default function suite() { let futarchyClient: FutarchyClient; @@ -193,6 +193,7 @@ export default function suite() { await bidWallClient .sellTokensIx({ amount: 5_000_000_000000, + minAmountOut: 99_000_000000, // We should receive exactly 99K USDC bidWall, baseMint: META, daoTreasury: daoTreasury, @@ -420,6 +421,26 @@ export default function suite() { assert.equal(bidWallUsdcBalanceAfterThirdSell, 10_900_000000n); }); + it("fails to sell tokens into a bid wall when the output amount is less than the minimum output amount", async function () { + const callbacks = expectError( + "InsufficientOutputAmount", + "bid wall should fail to sell tokens when the output amount is less than the minimum output amount", + ); + + await bidWallClient + .sellTokensIx({ + amount: 5_000_000_000000, + minAmountOut: 100_000_000000, + bidWall, + baseMint: META, + daoTreasury: daoTreasury, + quoteMint: MAINNET_USDC, + user: this.payer.publicKey, + }) + .rpc() + .then(callbacks[0], callbacks[1]); + }); + it("sending quote tokens to a bid wall beyond what was originally allocated doesn't change the NAV per token", async function () { // Send 1M USDC to the bid wall await this.transfer(MAINNET_USDC, this.payer, bidWall, 1_000_000_000000); From 6800e457fa7df22b1f978b7c1ddb85a8aa2740be Mon Sep 17 00:00:00 2001 From: Pileks Date: Tue, 20 Jan 2026 17:43:33 -0800 Subject: [PATCH 04/29] disallow zero output amount after fees --- .../bid_wall/src/instructions/sell_tokens.rs | 6 ++++++ tests/bidWall/unit/sellTokens.test.ts | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/programs/bid_wall/src/instructions/sell_tokens.rs b/programs/bid_wall/src/instructions/sell_tokens.rs index 20e22d0a6..2d3239755 100644 --- a/programs/bid_wall/src/instructions/sell_tokens.rs +++ b/programs/bid_wall/src/instructions/sell_tokens.rs @@ -157,6 +157,12 @@ impl SellTokens<'_> { BidWallError::InsufficientOutputAmount ); + require_gt!( + amount_out_after_fee, + 0, + BidWallError::InsufficientOutputAmount + ); + // Fees can't be used for future token buys, so we subtract the quote amount before fees. ctx.accounts.bid_wall.quote_amount -= amount_out_before_fee; // Track fees collected for fee distribution. diff --git a/tests/bidWall/unit/sellTokens.test.ts b/tests/bidWall/unit/sellTokens.test.ts index 48b49fedd..e25417335 100644 --- a/tests/bidWall/unit/sellTokens.test.ts +++ b/tests/bidWall/unit/sellTokens.test.ts @@ -558,4 +558,23 @@ export default function suite() { assert.include(e.message, "InvalidInputAmount"); } }); + + it("fails to sell tokens into a bid wall when the input amount would result in a zero output amount", async function () { + const callbacks = expectError( + "InsufficientOutputAmount", + "bid wall should fail to sell tokens when the input amount would result in a zero output amount", + ); + + await bidWallClient + .sellTokensIx({ + amount: 1, + bidWall, + baseMint: META, + daoTreasury: daoTreasury, + quoteMint: MAINNET_USDC, + user: this.payer.publicKey, + }) + .rpc() + .then(callbacks[0], callbacks[1]); + }); } From 8748a384f9ce92ef3fafcbb9f0aac0a64863c5e8 Mon Sep 17 00:00:00 2001 From: Pileks Date: Fri, 23 Jan 2026 15:18:25 -0800 Subject: [PATCH 05/29] prevent LP position hijack/freeze --- .../src/instructions/provide_liquidity.rs | 22 +- tests/futarchy/main.test.ts | 2 + tests/futarchy/unit/provideLiquidity.test.ts | 216 ++++++++++++++++++ 3 files changed, 233 insertions(+), 7 deletions(-) create mode 100644 tests/futarchy/unit/provideLiquidity.test.ts diff --git a/programs/futarchy/src/instructions/provide_liquidity.rs b/programs/futarchy/src/instructions/provide_liquidity.rs index 5557af291..87fbf31d3 100644 --- a/programs/futarchy/src/instructions/provide_liquidity.rs +++ b/programs/futarchy/src/instructions/provide_liquidity.rs @@ -79,7 +79,7 @@ impl ProvideLiquidity<'_> { quote_amount, max_base_amount, min_liquidity, - position_authority: _, + position_authority, } = params; let total_liquidity = dao.amm.total_liquidity; @@ -128,11 +128,19 @@ impl ProvideLiquidity<'_> { spot.base_reserves += base_amount; spot.quote_reserves += quote_amount; - amm_position.set_inner(AmmPosition { - dao: dao.key(), - position_authority: liquidity_provider.key(), - liquidity: amm_position.liquidity + liquidity_to_mint, - }); + if amm_position.position_authority == Pubkey::default() { + // New account - initialize all fields + // Use position_authority to ensure consistency with PDA derivation + amm_position.set_inner(AmmPosition { + dao: dao.key(), + position_authority, + liquidity: liquidity_to_mint, + }); + } else { + // Existing account - only update liquidity + // The position_authority is immutable once set + amm_position.liquidity += liquidity_to_mint; + } dao.amm.total_liquidity += liquidity_to_mint; @@ -168,7 +176,7 @@ impl ProvideLiquidity<'_> { common: CommonFields::new(&clock, dao.seq_num), dao: dao.key(), liquidity_provider: liquidity_provider.key(), - position_authority: params.position_authority, + position_authority, quote_amount, base_amount, liquidity_minted: liquidity_to_mint, diff --git a/tests/futarchy/main.test.ts b/tests/futarchy/main.test.ts index 1b654bcb6..aa42dad40 100644 --- a/tests/futarchy/main.test.ts +++ b/tests/futarchy/main.test.ts @@ -6,6 +6,7 @@ import finalizeProposal from "./unit/finalizeProposal.test.js"; import collectFees from "./unit/collectFees.test.js"; import conditionalSwap from "./unit/conditionalSwap.test.js"; +import provideLiquidity from "./unit/provideLiquidity.test.js"; import executeSpendingLimitChange from "./unit/executeSpendingLimitChange.test.js"; @@ -48,6 +49,7 @@ export default function suite() { describe("#collect_fees", collectFees); describe("#conditional_swap", conditionalSwap); + describe("#provide_liquidity", provideLiquidity); describe("#execute_spending_limit_change", executeSpendingLimitChange); describe("#collect_meteora_damm_fees", collectMeteoraDammFees); diff --git a/tests/futarchy/unit/provideLiquidity.test.ts b/tests/futarchy/unit/provideLiquidity.test.ts new file mode 100644 index 000000000..3464a10c2 --- /dev/null +++ b/tests/futarchy/unit/provideLiquidity.test.ts @@ -0,0 +1,216 @@ +import { Keypair, PublicKey } from "@solana/web3.js"; +import BN from "bn.js"; +import { assert } from "chai"; +import { FUTARCHY_PROGRAM_ID } from "@metadaoproject/futarchy/v0.7"; +import { + getAssociatedTokenAddressSync, + TOKEN_PROGRAM_ID, +} from "@solana/spl-token"; + +export default function suite() { + let META: PublicKey, USDC: PublicKey, dao: PublicKey; + + beforeEach(async function () { + META = await this.createMint(this.payer.publicKey, 6); + USDC = await this.createMint(this.payer.publicKey, 6); + + // Mint extra tokens for test (on top of what setupBasicDaoWithLiquidity mints) + await this.mintTo(USDC, this.payer.publicKey, this.payer, 1000 * 10 ** 6); + await this.mintTo(META, this.payer.publicKey, this.payer, 1000 * 10 ** 6); + + dao = await this.setupBasicDaoWithLiquidity({ + baseMint: META, + quoteMint: USDC, + }); + }); + + it("allows providing additional liquidity to existing position", async function () { + // Derive ammPosition PDA + const [ammPositionPda] = PublicKey.findProgramAddressSync( + [ + Buffer.from("amm_position"), + dao.toBuffer(), + this.payer.publicKey.toBuffer(), + ], + FUTARCHY_PROGRAM_ID, + ); + + // Fetch position before + const positionBefore = + await this.futarchy.autocrat.account.ammPosition.fetch(ammPositionPda); + + // Call provideLiquidityIx to add more liquidity + // maxBaseAmount needs buffer for rounding (add 1% or more) + await this.futarchy + .provideLiquidityIx({ + dao, + baseMint: META, + quoteMint: USDC, + quoteAmount: new BN(10 * 10 ** 6), + maxBaseAmount: new BN(11 * 10 ** 6), + minLiquidity: new BN(1), + positionAuthority: this.payer.publicKey, + liquidityProvider: this.payer.publicKey, + }) + .rpc(); + + // Fetch position after + const positionAfter = + await this.futarchy.autocrat.account.ammPosition.fetch(ammPositionPda); + + // Assert liquidity increased + assert.isTrue(positionAfter.liquidity.gt(positionBefore.liquidity)); + + // Assert position_authority unchanged + assert.isTrue( + positionAfter.positionAuthority.equals(positionBefore.positionAuthority), + ); + assert.isTrue(positionAfter.positionAuthority.equals(this.payer.publicKey)); + }); + + it("prevents attacker from overwriting victim's position authority", async function () { + // Create attacker keypair + const attacker = Keypair.generate(); + + // Fund attacker with META and USDC + await this.mintTo(META, attacker.publicKey, this.payer, 100 * 10 ** 6); + await this.mintTo(USDC, attacker.publicKey, this.payer, 100 * 10 ** 6); + + // Derive ammPosition PDA using victim's pubkey + const [ammPositionPda] = PublicKey.findProgramAddressSync( + [ + Buffer.from("amm_position"), + dao.toBuffer(), + this.payer.publicKey.toBuffer(), + ], + FUTARCHY_PROGRAM_ID, + ); + + // Fetch position before attack + const positionBefore = + await this.futarchy.autocrat.account.ammPosition.fetch(ammPositionPda); + + // Attacker calls provideLiquidityIx with positionAuthority=victim, liquidityProvider=attacker + await this.futarchy + .provideLiquidityIx({ + dao, + baseMint: META, + quoteMint: USDC, + quoteAmount: new BN(10 * 10 ** 6), + maxBaseAmount: new BN(11 * 10 ** 6), + minLiquidity: new BN(1), + positionAuthority: this.payer.publicKey, // victim + liquidityProvider: attacker.publicKey, // attacker + }) + .signers([attacker]) + .rpc(); + + // Fetch position after attack + const positionAfter = + await this.futarchy.autocrat.account.ammPosition.fetch(ammPositionPda); + + // Assert position_authority is still victim (not overwritten to attacker) + assert.isTrue(positionAfter.positionAuthority.equals(this.payer.publicKey)); + assert.isFalse(positionAfter.positionAuthority.equals(attacker.publicKey)); + + // Assert liquidity increased (attacker's liquidity was added) + assert.isTrue(positionAfter.liquidity.gt(positionBefore.liquidity)); + }); + + it("victim can still withdraw after attacker's hijack attempt", async function () { + // Create attacker keypair + const attacker = Keypair.generate(); + + // Fund attacker with META and USDC + await this.mintTo(META, attacker.publicKey, this.payer, 100 * 10 ** 6); + await this.mintTo(USDC, attacker.publicKey, this.payer, 100 * 10 ** 6); + + // Derive ammPosition PDA using victim's pubkey (payer is the victim) + const [ammPositionPda] = PublicKey.findProgramAddressSync( + [ + Buffer.from("amm_position"), + dao.toBuffer(), + this.payer.publicKey.toBuffer(), + ], + FUTARCHY_PROGRAM_ID, + ); + + // Attacker calls provideLiquidityIx attempting hijack + await this.futarchy + .provideLiquidityIx({ + dao, + baseMint: META, + quoteMint: USDC, + quoteAmount: new BN(10 * 10 ** 6), + maxBaseAmount: new BN(11 * 10 ** 6), + minLiquidity: new BN(1), + positionAuthority: this.payer.publicKey, // victim + liquidityProvider: attacker.publicKey, // attacker + }) + .signers([attacker]) + .rpc(); + + // Get victim's token balances before withdrawal + const victimBaseBalanceBefore = await this.getTokenBalance( + META, + this.payer.publicKey, + ); + const victimQuoteBalanceBefore = await this.getTokenBalance( + USDC, + this.payer.publicKey, + ); + + // Fetch position to get current liquidity + const position = + await this.futarchy.autocrat.account.ammPosition.fetch(ammPositionPda); + + // Derive event authority + const [eventAuthority] = PublicKey.findProgramAddressSync( + [Buffer.from("__event_authority")], + FUTARCHY_PROGRAM_ID, + ); + + // Victim withdraws all liquidity + await this.futarchy.autocrat.methods + .withdrawLiquidity({ + liquidityToWithdraw: position.liquidity, + minBaseAmount: new BN(0), + minQuoteAmount: new BN(0), + }) + .accounts({ + dao, + positionAuthority: this.payer.publicKey, + liquidityProviderBaseAccount: getAssociatedTokenAddressSync( + META, + this.payer.publicKey, + true, + ), + liquidityProviderQuoteAccount: getAssociatedTokenAddressSync( + USDC, + this.payer.publicKey, + true, + ), + ammBaseVault: getAssociatedTokenAddressSync(META, dao, true), + ammQuoteVault: getAssociatedTokenAddressSync(USDC, dao, true), + ammPosition: ammPositionPda, + tokenProgram: TOKEN_PROGRAM_ID, + eventAuthority, + program: FUTARCHY_PROGRAM_ID, + }) + .rpc(); + + // Get victim's token balances after withdrawal + const victimBaseBalanceAfter = await this.getTokenBalance( + META, + this.payer.publicKey, + ); + const victimQuoteBalanceAfter = await this.getTokenBalance( + USDC, + this.payer.publicKey, + ); + + // Assert victim received tokens back + assert.isTrue(victimBaseBalanceAfter > victimBaseBalanceBefore); + assert.isTrue(victimQuoteBalanceAfter > victimQuoteBalanceBefore); + }); +} From d35656183aaf1d7b14c4545f653c8da2e104163b Mon Sep 17 00:00:00 2001 From: Pileks Date: Fri, 23 Jan 2026 15:37:57 -0800 Subject: [PATCH 06/29] prevent supplying wrong multisig when initializing proposal --- programs/futarchy/src/instructions/initialize_proposal.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/programs/futarchy/src/instructions/initialize_proposal.rs b/programs/futarchy/src/instructions/initialize_proposal.rs index daf612870..13265e222 100644 --- a/programs/futarchy/src/instructions/initialize_proposal.rs +++ b/programs/futarchy/src/instructions/initialize_proposal.rs @@ -13,7 +13,7 @@ pub struct InitializeProposal<'info> { pub proposal: Box>, pub squads_proposal: Box>, pub squads_multisig: Box>, - #[account(mut)] + #[account(mut, has_one = squads_multisig)] pub dao: Box>, #[account( constraint = question.oracle == proposal.key() From 3e4b4e77383b29325daef07ba37758b0124c4c98 Mon Sep 17 00:00:00 2001 From: Pileks Date: Fri, 23 Jan 2026 16:03:42 -0800 Subject: [PATCH 07/29] prevent overcharging by 1 atom in provide_liquidity --- programs/futarchy/src/instructions/provide_liquidity.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/programs/futarchy/src/instructions/provide_liquidity.rs b/programs/futarchy/src/instructions/provide_liquidity.rs index 87fbf31d3..37f6421cf 100644 --- a/programs/futarchy/src/instructions/provide_liquidity.rs +++ b/programs/futarchy/src/instructions/provide_liquidity.rs @@ -95,8 +95,11 @@ impl ProvideLiquidity<'_> { let quote_reserves = spot.quote_reserves as u128; let base_reserves = spot.base_reserves as u128; - // this should only panic in an extreme scenario: when (quote_amount * base_reserve) / quote_reserve > u64::MAX - let base_amount: u64 = (((quote_amount as u128 * base_reserves) / quote_reserves) + 1) + // Use ceiling division to ensure the depositor provides at least their fair + // share of base tokens, protecting existing LPs from rounding-based value extraction. + // Formula: ceil(a / b) = (a + b - 1) / b + let numerator = quote_amount as u128 * base_reserves; + let base_amount: u64 = ((numerator + quote_reserves - 1) / quote_reserves) .try_into() .map_err(|_| FutarchyError::CastingOverflow)?; From f01a1bab1f8c27f88b03b439d9011da2e1dcd8b2 Mon Sep 17 00:00:00 2001 From: Pileks Date: Fri, 23 Jan 2026 16:08:42 -0800 Subject: [PATCH 08/29] apply min_liquidity slippage parameter to both first and subsequent lp provisioning --- programs/futarchy/src/instructions/provide_liquidity.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/programs/futarchy/src/instructions/provide_liquidity.rs b/programs/futarchy/src/instructions/provide_liquidity.rs index 37f6421cf..919a1a8a5 100644 --- a/programs/futarchy/src/instructions/provide_liquidity.rs +++ b/programs/futarchy/src/instructions/provide_liquidity.rs @@ -125,6 +125,12 @@ impl ProvideLiquidity<'_> { let initial_liquidity = quote_amount as u128 * 1_000_000_000; + require_gte!( + initial_liquidity, + min_liquidity, + // AmmError::AddLiquiditySlippageExceeded + ); + (initial_liquidity, base_amount) }; From c9db7a945f9578abded5ee1143e884e97a912225 Mon Sep 17 00:00:00 2001 From: Pileks Date: Fri, 23 Jan 2026 16:39:33 -0800 Subject: [PATCH 09/29] replace unreachable with proper errors --- programs/futarchy/src/instructions/provide_liquidity.rs | 3 +-- programs/futarchy/src/instructions/withdraw_liquidity.rs | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/programs/futarchy/src/instructions/provide_liquidity.rs b/programs/futarchy/src/instructions/provide_liquidity.rs index 919a1a8a5..74077a4a4 100644 --- a/programs/futarchy/src/instructions/provide_liquidity.rs +++ b/programs/futarchy/src/instructions/provide_liquidity.rs @@ -84,8 +84,7 @@ impl ProvideLiquidity<'_> { let total_liquidity = dao.amm.total_liquidity; let PoolState::Spot { ref mut spot } = dao.amm.state else { - // TODO: check that pool is already in right state - unreachable!(); + return err!(FutarchyError::PoolNotInSpotState); }; let (liquidity_to_mint, base_amount) = if total_liquidity > 0 { diff --git a/programs/futarchy/src/instructions/withdraw_liquidity.rs b/programs/futarchy/src/instructions/withdraw_liquidity.rs index 4ab95f21a..939e7bf0c 100644 --- a/programs/futarchy/src/instructions/withdraw_liquidity.rs +++ b/programs/futarchy/src/instructions/withdraw_liquidity.rs @@ -93,8 +93,7 @@ impl WithdrawLiquidity<'_> { let (base_to_withdraw, quote_to_withdraw) = { let PoolState::Spot { ref spot } = dao.amm.state else { - // TODO: check that pool is already in right state - unreachable!(); + return err!(FutarchyError::PoolNotInSpotState); }; spot.get_base_and_quote_withdrawable( liquidity_to_withdraw as u64, @@ -120,7 +119,7 @@ impl WithdrawLiquidity<'_> { dao.amm.total_liquidity -= liquidity_to_withdraw; { let PoolState::Spot { ref mut spot } = dao.amm.state else { - unreachable!(); + return err!(FutarchyError::PoolNotInSpotState); }; spot.base_reserves -= base_to_withdraw; spot.quote_reserves -= quote_to_withdraw; From f72bdba4b4364a2d1570627f8ab844eb3df9a602 Mon Sep 17 00:00:00 2001 From: Pileks Date: Mon, 26 Jan 2026 11:54:39 -0800 Subject: [PATCH 10/29] change comment to point to correct implementation reference --- programs/v07_launchpad/src/instructions/complete_launch.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/programs/v07_launchpad/src/instructions/complete_launch.rs b/programs/v07_launchpad/src/instructions/complete_launch.rs index a9b72d8db..797160b22 100644 --- a/programs/v07_launchpad/src/instructions/complete_launch.rs +++ b/programs/v07_launchpad/src/instructions/complete_launch.rs @@ -578,7 +578,7 @@ impl CompleteLaunch<'_> { LaunchpadError::InvariantViolated ); - // ref: https://github.com/MeteoraAg/damm-v2-sdk/blob/3d740ea8434af20a024d5d6fd08d60792dca9ca4/src/helpers/utils.ts#L121-L133 + // ref: https://github.com/MeteoraAg/damm-v2-sdk/blob/3d740ea8434af20a024d5d6fd08d60792dca9ca4/src/helpers/utils.ts#L135-L152 let float_price = final_raise_amount as f64 / TOKENS_TO_PARTICIPANTS as f64; let sqrt_price = (float_price.sqrt() * 2_f64.powf(64.0)) as u128; From 11408ba0fd368831572078fde43fcdae6e3e567b Mon Sep 17 00:00:00 2001 From: Pileks Date: Wed, 28 Jan 2026 13:11:54 -0800 Subject: [PATCH 11/29] prevent launch front-running by merging instructions into single transaction --- scripts/v0.7/launchTemplate.ts | 55 ++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/scripts/v0.7/launchTemplate.ts b/scripts/v0.7/launchTemplate.ts index 9ade1d758..e72ca5358 100644 --- a/scripts/v0.7/launchTemplate.ts +++ b/scripts/v0.7/launchTemplate.ts @@ -57,28 +57,24 @@ export const launch = async () => { const [launch] = getLaunchAddr(undefined, TOKEN); const [launchSigner] = getLaunchSignerAddr(undefined, launch); - const tx = new Transaction().add( - SystemProgram.createAccountWithSeed({ - fromPubkey: payer.publicKey, - newAccountPubkey: TOKEN, - basePubkey: payer.publicKey, - seed: TOKEN_SEED, - lamports: lamports, - space: token.MINT_SIZE, - programId: token.TOKEN_PROGRAM_ID, - }), - token.createInitializeMint2Instruction(TOKEN, 6, launchSigner, null), + const createAccountIx = SystemProgram.createAccountWithSeed({ + fromPubkey: payer.publicKey, + newAccountPubkey: TOKEN, + basePubkey: payer.publicKey, + seed: TOKEN_SEED, + lamports: lamports, + space: token.MINT_SIZE, + programId: token.TOKEN_PROGRAM_ID, + }); + + const createMintIx = token.createInitializeMint2Instruction( + TOKEN, + 6, + launchSigner, + null, ); - tx.recentBlockhash = ( - await provider.connection.getLatestBlockhash() - ).blockhash; - tx.feePayer = payer.publicKey; - tx.sign(payer); - const txHash = await provider.connection.sendRawTransaction(tx.serialize()); - await provider.connection.confirmTransaction(txHash, "confirmed"); - - const initializeLaunchTxSignature = await launchpad + const initializeLaunchIx = await launchpad .initializeLaunchIx({ tokenName: TOKEN_NAME, tokenSymbol: TOKEN_SYMBOL, @@ -100,12 +96,25 @@ export const launch = async () => { additionalTokensRecipient: ADDITIONAL_CARVEOUT_RECIPIENT, launchAuthority: LAUNCH_AUTHORITY, }) - .rpc(); + .instruction(); + + const tx = new Transaction().add( + createAccountIx, + createMintIx, + initializeLaunchIx, + ); + tx.recentBlockhash = ( + await provider.connection.getLatestBlockhash() + ).blockhash; + tx.feePayer = payer.publicKey; + tx.sign(payer); + + const txHash = await provider.connection.sendRawTransaction(tx.serialize()); + await provider.connection.confirmTransaction(txHash, "confirmed"); - console.log("Launch initialized", initializeLaunchTxSignature); + console.log("Launch initialized", txHash); console.log("Launch address:", launch.toBase58()); - // await launchpad.startLaunchIx({ launch }).rpc(); }; launch().catch(console.error); From efaca01f5b08838aeea8738f59269f9cf8213cfa Mon Sep 17 00:00:00 2001 From: Pileks Date: Wed, 28 Jan 2026 14:09:33 -0800 Subject: [PATCH 12/29] remove MAX_PREMINE from launchpad v7 --- .../v07_launchpad/src/instructions/initialize_launch.rs | 7 ------- programs/v07_launchpad/src/lib.rs | 3 --- 2 files changed, 10 deletions(-) diff --git a/programs/v07_launchpad/src/instructions/initialize_launch.rs b/programs/v07_launchpad/src/instructions/initialize_launch.rs index 06fbed155..e648ed0d3 100644 --- a/programs/v07_launchpad/src/instructions/initialize_launch.rs +++ b/programs/v07_launchpad/src/instructions/initialize_launch.rs @@ -5,7 +5,6 @@ use anchor_spl::token::{self, Mint, MintTo, Token, TokenAccount}; use crate::error::LaunchpadError; use crate::events::{CommonFields, LaunchInitializedEvent}; use crate::state::{Launch, LaunchState}; -use crate::MAX_PREMINE; use crate::{ usdc_mint, TOKENS_TO_DAMM_V2_LIQUIDITY, TOKENS_TO_FUTARCHY_LIQUIDITY, TOKENS_TO_PARTICIPANTS, }; @@ -155,12 +154,6 @@ impl InitializeLaunch<'_> { LaunchpadError::InvalidMonthlySpendingLimitMembers ); - require_gte!( - MAX_PREMINE, - args.performance_package_token_amount, - LaunchpadError::InvalidPriceBasedPremineAmount - ); - require_gte!( args.months_until_insiders_can_unlock, 18, diff --git a/programs/v07_launchpad/src/lib.rs b/programs/v07_launchpad/src/lib.rs index 8fd64ad9a..f481153b3 100644 --- a/programs/v07_launchpad/src/lib.rs +++ b/programs/v07_launchpad/src/lib.rs @@ -38,9 +38,6 @@ pub const TOKENS_TO_DAMM_V2_LIQUIDITY: u64 = TOKENS_TO_DAMM_V2_LIQUIDITY_UNSCALE /// we need this to prevent overflow pub const TOKENS_TO_DAMM_V2_LIQUIDITY_UNSCALED: u64 = 900_000; -/// Max 50% premine -pub const MAX_PREMINE: u64 = 15_000_000 * TOKEN_SCALE; - pub mod usdc_mint { use anchor_lang::prelude::declare_id; From 30307620cdb5fb3ef23a5a0a0bcf1043c2b9c460 Mon Sep 17 00:00:00 2001 From: Pileks Date: Wed, 28 Jan 2026 14:46:25 -0800 Subject: [PATCH 13/29] Disallow oracle changes while in Unlocking state to prevent TWAP corruption. --- .../src/instructions/execute_change.rs | 14 +++++++++++++- .../src/instructions/propose_change.rs | 14 ++++++++++++-- .../price_based_performance_package/src/lib.rs | 2 +- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/programs/price_based_performance_package/src/instructions/execute_change.rs b/programs/price_based_performance_package/src/instructions/execute_change.rs index c808592a8..4b860c028 100644 --- a/programs/price_based_performance_package/src/instructions/execute_change.rs +++ b/programs/price_based_performance_package/src/instructions/execute_change.rs @@ -1,6 +1,6 @@ use crate::{ ChangeExecuted, ChangeRequest, ChangeType, CommonFields, PerformancePackage, - PriceBasedPerformancePackageError, ProposerType, + PerformancePackageState, PriceBasedPerformancePackageError, ProposerType, }; use anchor_lang::prelude::*; @@ -49,6 +49,18 @@ impl<'info> ExecuteChange<'info> { let performance_package = &mut ctx.accounts.performance_package; let change_request = &ctx.accounts.change_request; + // Disallow oracle changes while in Unlocking state to prevent TWAP corruption. + // This is checked here (in addition to propose_change) because a change request + // could have been proposed before start_unlock was called. + if matches!(change_request.change_type, ChangeType::Oracle { .. }) + && matches!( + performance_package.state, + PerformancePackageState::Unlocking { .. } + ) + { + return Err(PriceBasedPerformancePackageError::InvalidPerformancePackageState.into()); + } + // Apply the change based on type match &change_request.change_type { ChangeType::Oracle { new_oracle_config } => { diff --git a/programs/price_based_performance_package/src/instructions/propose_change.rs b/programs/price_based_performance_package/src/instructions/propose_change.rs index 920dcacd9..e873e576a 100644 --- a/programs/price_based_performance_package/src/instructions/propose_change.rs +++ b/programs/price_based_performance_package/src/instructions/propose_change.rs @@ -1,5 +1,5 @@ use crate::{ - ChangeProposed, ChangeRequest, ChangeType, PerformancePackage, + ChangeProposed, ChangeRequest, ChangeType, PerformancePackage, PerformancePackageState, PriceBasedPerformancePackageError, ProposerType, }; use anchor_lang::prelude::*; @@ -36,7 +36,7 @@ pub struct ProposeChange<'info> { } impl<'info> ProposeChange<'info> { - pub fn validate(&self) -> Result<()> { + pub fn validate(&self, params: &ProposeChangeParams) -> Result<()> { if self.proposer.key() != self.performance_package.recipient && self.proposer.key() != self.performance_package.performance_package_authority { @@ -44,6 +44,16 @@ impl<'info> ProposeChange<'info> { return Err(PriceBasedPerformancePackageError::UnauthorizedChangeRequest.into()); } + // Disallow oracle changes while in Unlocking state to prevent TWAP corruption + if matches!(params.change_type, ChangeType::Oracle { .. }) + && matches!( + self.performance_package.state, + PerformancePackageState::Unlocking { .. } + ) + { + return Err(PriceBasedPerformancePackageError::InvalidPerformancePackageState.into()); + } + Ok(()) } diff --git a/programs/price_based_performance_package/src/lib.rs b/programs/price_based_performance_package/src/lib.rs index 2ca5c838a..d161b4c1c 100644 --- a/programs/price_based_performance_package/src/lib.rs +++ b/programs/price_based_performance_package/src/lib.rs @@ -56,7 +56,7 @@ pub mod price_based_performance_package { CompleteUnlock::handle(ctx) } - #[access_control(ctx.accounts.validate())] + #[access_control(ctx.accounts.validate(¶ms))] pub fn propose_change(ctx: Context, params: ProposeChangeParams) -> Result<()> { ProposeChange::handle(ctx, params) } From 46d9613ea33f855c2e05760a92e868dc713e3366 Mon Sep 17 00:00:00 2001 From: Pileks Date: Tue, 3 Feb 2026 11:42:46 -0800 Subject: [PATCH 14/29] adjust liquidity provision logic when position is a new account --- programs/futarchy/src/instructions/provide_liquidity.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/programs/futarchy/src/instructions/provide_liquidity.rs b/programs/futarchy/src/instructions/provide_liquidity.rs index 74077a4a4..d009725ca 100644 --- a/programs/futarchy/src/instructions/provide_liquidity.rs +++ b/programs/futarchy/src/instructions/provide_liquidity.rs @@ -136,7 +136,11 @@ impl ProvideLiquidity<'_> { spot.base_reserves += base_amount; spot.quote_reserves += quote_amount; - if amm_position.position_authority == Pubkey::default() { + // Check `dao` instead of `position_authority` to detect new accounts. + // A valid DAO is always a PDA, never Pubkey::default(). Using `position_authority` + // would fail for donations where position_authority = Pubkey::default(), causing + // subsequent donations to overwrite liquidity instead of accumulating it. + if amm_position.dao == Pubkey::default() { // New account - initialize all fields // Use position_authority to ensure consistency with PDA derivation amm_position.set_inner(AmmPosition { From e8c014ef40c364eb085745b11d328d98551caf78 Mon Sep 17 00:00:00 2001 From: Pileks Date: Tue, 3 Feb 2026 15:03:45 -0800 Subject: [PATCH 15/29] When a Proposal is Rejected by the Market The Squads Proposal Should Be Closed --- .../futarchy/src/instructions/finalize_proposal.rs | 13 +++++++++++++ tests/futarchy/unit/finalizeProposal.test.ts | 14 ++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/programs/futarchy/src/instructions/finalize_proposal.rs b/programs/futarchy/src/instructions/finalize_proposal.rs index 22c5929d7..1abf2eb8a 100644 --- a/programs/futarchy/src/instructions/finalize_proposal.rs +++ b/programs/futarchy/src/instructions/finalize_proposal.rs @@ -197,6 +197,19 @@ impl FinalizeProposal<'_> { spot.base_protocol_fee_balance += pass.base_protocol_fee_balance; spot.quote_protocol_fee_balance += pass.quote_protocol_fee_balance; } else { + squads_multisig_program::cpi::proposal_reject( + CpiContext::new_with_signer( + squads_multisig_program.to_account_info(), + squads_multisig_program::cpi::accounts::ProposalVote { + proposal: squads_proposal.to_account_info(), + multisig: squads_multisig.to_account_info(), + member: dao.to_account_info(), + }, + dao_signer, + ), + squads_multisig_program::ProposalVoteArgs { memo: None }, + )?; + spot.base_reserves += fail.base_reserves; spot.quote_reserves += fail.quote_reserves; spot.base_protocol_fee_balance += fail.base_protocol_fee_balance; diff --git a/tests/futarchy/unit/finalizeProposal.test.ts b/tests/futarchy/unit/finalizeProposal.test.ts index e0686dbb5..56e3c47e9 100644 --- a/tests/futarchy/unit/finalizeProposal.test.ts +++ b/tests/futarchy/unit/finalizeProposal.test.ts @@ -293,6 +293,20 @@ export default function suite() { const storedProposal = await this.futarchy.getProposal(proposal); assert.exists(storedProposal.state.failed); + + // Verify Squads proposal is rejected + const multisigPda = multisig.getMultisigPda({ createKey: dao })[0]; + const [squadsProposalPda] = multisig.getProposalPda({ + multisigPda, + transactionIndex: 1n, + }); + const squadsProposal = await multisig.accounts.Proposal.fromAccountAddress( + this.squadsConnection, + squadsProposalPda, + ); + assert.isTrue( + multisig.generated.isProposalStatusRejected(squadsProposal.status), + ); }); it("passes proposals when the team sponsors them and pass twap is slightly below fail twap", async function () { From 66e4a0226c3d42437200e5a2365337d6bc7c1999 Mon Sep 17 00:00:00 2001 From: Pileks Date: Tue, 3 Feb 2026 16:24:40 -0800 Subject: [PATCH 16/29] prevent dao parameters being upadted during active futarchy markets --- .../futarchy/src/instructions/update_dao.rs | 8 + programs/futarchy/src/lib.rs | 1 + tests/futarchy/main.test.ts | 2 + tests/futarchy/unit/updateDao.test.ts | 343 ++++++++++++++++++ 4 files changed, 354 insertions(+) create mode 100644 tests/futarchy/unit/updateDao.test.ts diff --git a/programs/futarchy/src/instructions/update_dao.rs b/programs/futarchy/src/instructions/update_dao.rs index 3d3babb39..672fa26f6 100644 --- a/programs/futarchy/src/instructions/update_dao.rs +++ b/programs/futarchy/src/instructions/update_dao.rs @@ -23,6 +23,14 @@ pub struct UpdateDao<'info> { } impl UpdateDao<'_> { + pub fn validate(&self) -> Result<()> { + // Prevent parameter updates during active futarchy markets + if !matches!(self.dao.amm.state, PoolState::Spot { .. }) { + return Err(FutarchyError::PoolNotInSpotState.into()); + } + Ok(()) + } + pub fn handle(ctx: Context, dao_params: UpdateDaoParams) -> Result<()> { let dao = &mut ctx.accounts.dao; diff --git a/programs/futarchy/src/lib.rs b/programs/futarchy/src/lib.rs index 4f68ecf3b..ea022df16 100644 --- a/programs/futarchy/src/lib.rs +++ b/programs/futarchy/src/lib.rs @@ -94,6 +94,7 @@ pub mod futarchy { FinalizeProposal::handle(ctx) } + #[access_control(ctx.accounts.validate())] pub fn update_dao(ctx: Context, dao_params: UpdateDaoParams) -> Result<()> { UpdateDao::handle(ctx, dao_params) } diff --git a/tests/futarchy/main.test.ts b/tests/futarchy/main.test.ts index aa42dad40..1cd8671c1 100644 --- a/tests/futarchy/main.test.ts +++ b/tests/futarchy/main.test.ts @@ -3,6 +3,7 @@ import futarchyAmm from "./integration/futarchyAmm.test.js"; import initializeDao from "./unit/initializeDao.test.js"; import initializeProposal from "./unit/initializeProposal.test.js"; import finalizeProposal from "./unit/finalizeProposal.test.js"; +import updateDao from "./unit/updateDao.test.js"; import collectFees from "./unit/collectFees.test.js"; import conditionalSwap from "./unit/conditionalSwap.test.js"; @@ -46,6 +47,7 @@ export default function suite() { describe("#initialize_dao", initializeDao); describe("#initialize_proposal", initializeProposal); describe("#finalize_proposal", finalizeProposal); + describe("#update_dao", updateDao); describe("#collect_fees", collectFees); describe("#conditional_swap", conditionalSwap); diff --git a/tests/futarchy/unit/updateDao.test.ts b/tests/futarchy/unit/updateDao.test.ts new file mode 100644 index 000000000..73c647991 --- /dev/null +++ b/tests/futarchy/unit/updateDao.test.ts @@ -0,0 +1,343 @@ +import { + ComputeBudgetProgram, + PublicKey, + Transaction, + TransactionMessage, +} from "@solana/web3.js"; +import { assert } from "chai"; +import * as multisig from "@sqds/multisig"; +import { MEMO_PROGRAM_ID } from "@solana/spl-memo"; +import { + PERMISSIONLESS_ACCOUNT, + getProposalAddrV2, + InstructionUtils, +} from "@metadaoproject/futarchy/v0.7"; +import { sha256 } from "@metadaoproject/futarchy"; +import BN from "bn.js"; + +export default function suite() { + let META: PublicKey, USDC: PublicKey, dao: PublicKey; + + beforeEach(async function () { + META = await this.createMint(this.payer.publicKey, 6); + USDC = await this.createMint(this.payer.publicKey, 6); + + await this.mintTo( + USDC, + this.payer.publicKey, + this.payer, + 10_000_000_000_000, + ); + await this.mintTo( + META, + this.payer.publicKey, + this.payer, + 10_000_000_000_000, + ); + + dao = await this.setupBasicDaoWithLiquidity({ + baseMint: META, + quoteMint: USDC, + }); + }); + + it("should fail updateDao execution when DAO is in Futarchy state", async function () { + const daoAccount = await this.futarchy.getDao(dao); + + // Step 1: Create updateDao squads vault transaction (index 1) + const updateDaoIx = await this.futarchy + .updateDaoIx({ + dao, + params: { + passThresholdBps: 500, + secondsPerProposal: null, + twapInitialObservation: null, + twapMaxObservationChangePerUpdate: null, + minQuoteFutarchicLiquidity: null, + minBaseFutarchicLiquidity: null, + baseToStake: null, + teamSponsoredPassThresholdBps: null, + teamAddress: null, + twapStartDelaySeconds: null, + }, + }) + .instruction(); + + const updateDaoMessage = new TransactionMessage({ + payerKey: daoAccount.squadsMultisigVault, + recentBlockhash: (await this.banksClient.getLatestBlockhash())[0], + instructions: [updateDaoIx], + }); + + const vaultTxCreateIx = multisig.instructions.vaultTransactionCreate({ + multisigPda: daoAccount.squadsMultisig, + transactionIndex: 1n, + creator: PERMISSIONLESS_ACCOUNT.publicKey, + rentPayer: this.payer.publicKey, + vaultIndex: 0, + ephemeralSigners: 0, + transactionMessage: updateDaoMessage, + }); + + const squadsProposalCreateIx = multisig.instructions.proposalCreate({ + multisigPda: daoAccount.squadsMultisig, + transactionIndex: 1n, + creator: PERMISSIONLESS_ACCOUNT.publicKey, + rentPayer: this.payer.publicKey, + }); + + const [squadsProposalPda] = multisig.getProposalPda({ + multisigPda: daoAccount.squadsMultisig, + transactionIndex: 1n, + }); + + const createSquadsTx = new Transaction().add( + vaultTxCreateIx, + squadsProposalCreateIx, + ); + createSquadsTx.recentBlockhash = ( + await this.banksClient.getLatestBlockhash() + )[0]; + createSquadsTx.feePayer = this.payer.publicKey; + createSquadsTx.sign(this.payer, PERMISSIONLESS_ACCOUNT); + + await this.banksClient.processTransaction(createSquadsTx); + + // Step 2: Create futarchy proposal A linked to updateDao squads proposal + let [proposalA] = getProposalAddrV2({ squadsProposal: squadsProposalPda }); + + await this.conditionalVault.initializeQuestion( + sha256(`Will ${proposalA} pass?/FAIL/PASS`), + proposalA, + 2, + ); + + const { question, baseVault, quoteVault } = this.futarchy.getProposalPdas( + proposalA, + META, + USDC, + dao, + ); + + await this.conditionalVault + .initializeVaultIx(question, META, 2) + .postInstructions( + await InstructionUtils.getInstructions( + this.conditionalVault.initializeVaultIx(question, USDC, 2), + ), + ) + .rpc(); + + await this.futarchy + .initializeProposalIx(squadsProposalPda, dao, META, USDC, question) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ units: 300_000 }), + ]) + .rpc(); + + // Split tokens before launching proposal + await this.conditionalVault + .splitTokensIx(question, baseVault, META, new BN(1000_000_000), 2) + .rpc(); + await this.conditionalVault + .splitTokensIx(question, quoteVault, USDC, new BN(1000_000_000), 2) + .rpc(); + + // Launch proposal A to put DAO in Futarchy state + await this.futarchy + .launchProposalIx({ + proposal: proposalA, + dao, + baseMint: META, + quoteMint: USDC, + squadsProposal: squadsProposalPda, + }) + .rpc(); + + // Verify DAO is in Futarchy state + let daoState = await this.futarchy.getDao(dao); + assert.isDefined(daoState.amm.state.futarchy); + + // Step 3: Trade on pass market to make proposal A pass + // Using conditionalSwapIx which handles all the AMM interaction + await this.futarchy + .conditionalSwapIx({ + dao, + baseMint: META, + quoteMint: USDC, + proposal: proposalA, + market: "pass", + swapType: "buy", + inputAmount: new BN(900_000_000), + minOutputAmount: new BN(0), + }) + .rpc(); + + // Crank TWAP over time by doing small swaps (this updates the TWAP) + for (let i = 0; i < 100; i++) { + await this.advanceBySeconds(20_000); + + await this.futarchy + .conditionalSwapIx({ + dao, + baseMint: META, + quoteMint: USDC, + proposal: proposalA, + market: "pass", + swapType: "buy", + inputAmount: new BN(10), + minOutputAmount: new BN(0), + payer: this.payer.publicKey, + }) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitPrice({ microLamports: i }), + ]) + .rpc(); + } + + // Step 4: Finalize proposal A - DAO returns to Spot state + await this.futarchy.finalizeProposal(proposalA); + + // Verify proposal A is passed + const proposalAAccount = await this.futarchy.getProposal(proposalA); + console.log("proposalAAccount", proposalAAccount); + assert.isDefined(proposalAAccount.state.passed); + + // Verify DAO is back in Spot state + daoState = await this.futarchy.getDao(dao); + assert.isDefined(daoState.amm.state.spot); + + // Step 5: Launch proposal B to put DAO back into Futarchy state + // We need to manually create this with transaction index 2 since index 1 is used by updateDao + const memoInstruction = { + programId: MEMO_PROGRAM_ID, + keys: [], + data: Buffer.from("test proposal B"), + }; + + const memoMessage = new TransactionMessage({ + payerKey: daoAccount.squadsMultisigVault, + recentBlockhash: (await this.banksClient.getLatestBlockhash())[0], + instructions: [memoInstruction], + }); + + const vaultTxCreateIx2 = multisig.instructions.vaultTransactionCreate({ + multisigPda: daoAccount.squadsMultisig, + transactionIndex: 2n, + creator: PERMISSIONLESS_ACCOUNT.publicKey, + rentPayer: this.payer.publicKey, + vaultIndex: 0, + ephemeralSigners: 0, + transactionMessage: memoMessage, + }); + + const squadsProposalCreateIx2 = multisig.instructions.proposalCreate({ + multisigPda: daoAccount.squadsMultisig, + transactionIndex: 2n, + creator: PERMISSIONLESS_ACCOUNT.publicKey, + rentPayer: this.payer.publicKey, + }); + + const [squadsProposalPda2] = multisig.getProposalPda({ + multisigPda: daoAccount.squadsMultisig, + transactionIndex: 2n, + }); + + const createSquadsTx2 = new Transaction().add( + vaultTxCreateIx2, + squadsProposalCreateIx2, + ); + createSquadsTx2.recentBlockhash = ( + await this.banksClient.getLatestBlockhash() + )[0]; + createSquadsTx2.feePayer = this.payer.publicKey; + createSquadsTx2.sign(this.payer, PERMISSIONLESS_ACCOUNT); + + await this.banksClient.processTransaction(createSquadsTx2); + + // Create futarchy proposal B linked to squads proposal 2 + let [proposalB] = getProposalAddrV2({ squadsProposal: squadsProposalPda2 }); + + await this.conditionalVault.initializeQuestion( + sha256(`Will ${proposalB} pass?/FAIL/PASS`), + proposalB, + 2, + ); + + const proposalBPdas = this.futarchy.getProposalPdas( + proposalB, + META, + USDC, + dao, + ); + + await this.conditionalVault + .initializeVaultIx(proposalBPdas.question, META, 2) + .postInstructions( + await InstructionUtils.getInstructions( + this.conditionalVault.initializeVaultIx( + proposalBPdas.question, + USDC, + 2, + ), + ), + ) + .rpc(); + + await this.futarchy + .initializeProposalIx( + squadsProposalPda2, + dao, + META, + USDC, + proposalBPdas.question, + ) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ units: 300_000 }), + ]) + .rpc(); + + // Launch proposal B to put DAO in Futarchy state + await this.futarchy + .launchProposalIx({ + proposal: proposalB, + dao, + baseMint: META, + quoteMint: USDC, + squadsProposal: squadsProposalPda2, + }) + .rpc(); + + // Verify DAO is in Futarchy state again + daoState = await this.futarchy.getDao(dao); + assert.isDefined(daoState.amm.state.futarchy); + + // Step 6: Try to execute updateDao squads transaction + // This should fail because DAO is in Futarchy state + const txExecuteIx = await multisig.instructions.vaultTransactionExecute({ + connection: this.squadsConnection, + multisigPda: daoAccount.squadsMultisig, + transactionIndex: 1n, + member: PERMISSIONLESS_ACCOUNT.publicKey, + }); + + const txExecute = new Transaction().add(txExecuteIx.instruction); + txExecute.recentBlockhash = ( + await this.banksClient.getLatestBlockhash() + )[0]; + txExecute.feePayer = this.payer.publicKey; + txExecute.sign(this.payer, PERMISSIONLESS_ACCOUNT); + + try { + await this.banksClient.processTransaction(txExecute); + assert.fail("Should have failed with PoolNotInSpotState"); + } catch (e) { + // The error comes from the CPI call failing, check for PoolNotInSpotState (0x178a = 6026) + assert( + e.toString().includes("PoolNotInSpotState") || + e.toString().includes("0x178a"), + `Expected PoolNotInSpotState error, got: ${e}`, + ); + } + }); +} From 3865e80491c3a2005e3515b4d3bd58551e6b91a3 Mon Sep 17 00:00:00 2001 From: Pileks Date: Tue, 3 Feb 2026 16:39:43 -0800 Subject: [PATCH 17/29] split_tokens and merge_tokens should verify question is unresolved --- .../src/instructions/merge_tokens.rs | 8 +++++ .../src/instructions/split_tokens.rs | 8 +++++ programs/conditional_vault/src/lib.rs | 2 ++ .../conditionalVault/unit/mergeTokens.test.ts | 30 +++++++++++++++- .../conditionalVault/unit/splitTokens.test.ts | 34 ++++++++++++++++++- 5 files changed, 80 insertions(+), 2 deletions(-) diff --git a/programs/conditional_vault/src/instructions/merge_tokens.rs b/programs/conditional_vault/src/instructions/merge_tokens.rs index 6a9ff848d..c40fb1c50 100644 --- a/programs/conditional_vault/src/instructions/merge_tokens.rs +++ b/programs/conditional_vault/src/instructions/merge_tokens.rs @@ -1,6 +1,14 @@ use super::*; impl<'info, 'c: 'info> InteractWithVault<'info> { + pub fn validate_merge_tokens(&self) -> Result<()> { + require!( + !self.question.is_resolved(), + VaultError::QuestionAlreadyResolved + ); + Ok(()) + } + pub fn handle_merge_tokens(ctx: Context<'_, '_, 'c, 'info, Self>, amount: u64) -> Result<()> { let accs = &ctx.accounts; diff --git a/programs/conditional_vault/src/instructions/split_tokens.rs b/programs/conditional_vault/src/instructions/split_tokens.rs index 4213d88fa..592838102 100644 --- a/programs/conditional_vault/src/instructions/split_tokens.rs +++ b/programs/conditional_vault/src/instructions/split_tokens.rs @@ -1,6 +1,14 @@ use super::*; impl<'info, 'c: 'info> InteractWithVault<'info> { + pub fn validate_split_tokens(&self) -> Result<()> { + require!( + !self.question.is_resolved(), + VaultError::QuestionAlreadyResolved + ); + Ok(()) + } + pub fn handle_split_tokens(ctx: Context<'_, '_, 'c, 'info, Self>, amount: u64) -> Result<()> { let accs = &ctx.accounts; diff --git a/programs/conditional_vault/src/lib.rs b/programs/conditional_vault/src/lib.rs index 4568f3e70..9dfa1400f 100644 --- a/programs/conditional_vault/src/lib.rs +++ b/programs/conditional_vault/src/lib.rs @@ -59,6 +59,7 @@ pub mod conditional_vault { InitializeConditionalVault::handle(ctx) } + #[access_control(ctx.accounts.validate_split_tokens())] pub fn split_tokens<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, InteractWithVault<'info>>, amount: u64, @@ -66,6 +67,7 @@ pub mod conditional_vault { InteractWithVault::handle_split_tokens(ctx, amount) } + #[access_control(ctx.accounts.validate_merge_tokens())] pub fn merge_tokens<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, InteractWithVault<'info>>, amount: u64, diff --git a/tests/conditionalVault/unit/mergeTokens.test.ts b/tests/conditionalVault/unit/mergeTokens.test.ts index 8ef54640c..9e609b763 100644 --- a/tests/conditionalVault/unit/mergeTokens.test.ts +++ b/tests/conditionalVault/unit/mergeTokens.test.ts @@ -18,13 +18,15 @@ export default function suite() { let vault: PublicKey; let underlyingTokenMint: PublicKey; let userUnderlyingTokenAccount: PublicKey; + let oracle: Keypair; + before(function () { vaultClient = this.conditionalVault; }); beforeEach(async function () { const questionId = sha256(new Uint8Array([9, 2, 1])); - const oracle = Keypair.generate(); + oracle = Keypair.generate(); question = await vaultClient.initializeQuestion( questionId, @@ -151,4 +153,30 @@ export default function suite() { updatedVault = await vaultClient.fetchVault(vault); assert.equal(updatedVault.seqNum.toString(), "3"); }); + + it("throws error when trying to merge tokens after question is resolved", async function () { + // Resolve the question + await vaultClient.vaultProgram.methods + .resolveQuestion({ payoutNumerators: [1, 0] }) + .accounts({ + question, + oracle: oracle.publicKey, + }) + .signers([oracle]) + .rpc(); + + // Attempt to merge tokens after resolution should fail + const callbacks = expectError( + "QuestionAlreadyResolved", + "merge succeeded despite question being resolved", + ); + + await vaultClient + .mergeTokensIx(question, vault, underlyingTokenMint, new BN(500), 2) + .postInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ units: 200_000 }), + ]) + .rpc() + .then(callbacks[0], callbacks[1]); + }); } diff --git a/tests/conditionalVault/unit/splitTokens.test.ts b/tests/conditionalVault/unit/splitTokens.test.ts index f295fc8dd..65ab5e9f3 100644 --- a/tests/conditionalVault/unit/splitTokens.test.ts +++ b/tests/conditionalVault/unit/splitTokens.test.ts @@ -12,6 +12,7 @@ export default function suite() { let question: PublicKey; let vault: PublicKey; let underlyingTokenMint: PublicKey; + let oracle: Keypair; before(function () { vaultClient = this.conditionalVault; @@ -19,7 +20,7 @@ export default function suite() { beforeEach(async function () { const questionId = sha256(new Uint8Array([5, 2, 1])); - const oracle = Keypair.generate(); + oracle = Keypair.generate(); question = await vaultClient.initializeQuestion( questionId, @@ -238,4 +239,35 @@ export default function suite() { await this.assertBalance(mint, this.payer.publicKey, 2000); } }); + + it("throws error when trying to split tokens after question is resolved", async function () { + // First, split some tokens while the question is unresolved + await vaultClient + .splitTokensIx(question, vault, underlyingTokenMint, new BN(1000), 2) + .rpc(); + + // Resolve the question + await vaultClient.vaultProgram.methods + .resolveQuestion({ payoutNumerators: [1, 0] }) + .accounts({ + question, + oracle: oracle.publicKey, + }) + .signers([oracle]) + .rpc(); + + // Attempt to split tokens after resolution should fail + const callbacks = expectError( + "QuestionAlreadyResolved", + "split succeeded despite question being resolved", + ); + + await vaultClient + .splitTokensIx(question, vault, underlyingTokenMint, new BN(1000), 2) + .postInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ units: 200_000 }), + ]) + .rpc() + .then(callbacks[0], callbacks[1]); + }); } From 9d35ae8418bdd4803aa8367e33cfa98d1a3c390a Mon Sep 17 00:00:00 2001 From: Pileks Date: Thu, 5 Feb 2026 12:51:31 -0800 Subject: [PATCH 18/29] bid wall quote amount debits rounding --- programs/bid_wall/src/instructions/sell_tokens.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/programs/bid_wall/src/instructions/sell_tokens.rs b/programs/bid_wall/src/instructions/sell_tokens.rs index 2d3239755..f5d4662e9 100644 --- a/programs/bid_wall/src/instructions/sell_tokens.rs +++ b/programs/bid_wall/src/instructions/sell_tokens.rs @@ -107,16 +107,22 @@ impl SellTokens<'_> { let amount_out_before_fee = (amount_in as u128 * total_nav as u128 / remaining_base as u128) as u64; + // Ceiling division: ensures rounding dust is debited from quote_amount + // rather than accumulating and inflating total_nav on subsequent sells. + let quote_amount_debit = ((amount_in as u128 * total_nav as u128 + remaining_base as u128 + - 1) + / remaining_base as u128) as u64; + require_gte!( ctx.accounts.bid_wall.quote_amount, - amount_out_before_fee, + quote_amount_debit, BidWallError::InsufficientQuoteReserves ); let amount_out_after_fee = ((10_000_u128 - FEE_BPS as u128) * amount_out_before_fee as u128 / 10_000_u128) as u64; - let fee = amount_out_before_fee - amount_out_after_fee; + let fee = quote_amount_debit - amount_out_after_fee; // Burn base tokens token::burn( @@ -163,8 +169,9 @@ impl SellTokens<'_> { BidWallError::InsufficientOutputAmount ); - // Fees can't be used for future token buys, so we subtract the quote amount before fees. - ctx.accounts.bid_wall.quote_amount -= amount_out_before_fee; + // Fees can't be used for future token buys, so we subtract + // the quote amount debit (total amount of quote debited from the bid wall). + ctx.accounts.bid_wall.quote_amount -= quote_amount_debit; // Track fees collected for fee distribution. ctx.accounts.bid_wall.fees_collected += fee; // Track base tokens bought up by the bid wall for NAV calculation. From 5ae14194d697647d08e484f46f23dc0484963c1f Mon Sep 17 00:00:00 2001 From: Pileks Date: Thu, 5 Feb 2026 13:39:45 -0800 Subject: [PATCH 19/29] minor rename --- jup-sdk/src/futarchy_amm.rs | 10 +++++----- programs/futarchy/src/state/futarchy_amm.rs | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/jup-sdk/src/futarchy_amm.rs b/jup-sdk/src/futarchy_amm.rs index cd6274cd8..5361ea5fd 100644 --- a/jup-sdk/src/futarchy_amm.rs +++ b/jup-sdk/src/futarchy_amm.rs @@ -1,7 +1,7 @@ use anchor_lang::prelude::{ - AccountMeta, AnchorDeserialize, AnchorSerialize, InitSpace, Pubkey, borsh, + borsh, AccountMeta, AnchorDeserialize, AnchorSerialize, InitSpace, Pubkey, }; -use anyhow::{Result, anyhow, bail}; +use anyhow::{anyhow, bail, Result}; use crate::FutarchyAmmError; @@ -206,18 +206,18 @@ impl Pool { bail!(FutarchyAmmError::InvalidReserves); } - let input_amount_with_lp_fee = (input_amount_after_protocol_fee as u128) + let input_amount_after_lp_fee = (input_amount_after_protocol_fee as u128) .checked_mul((MAX_BPS - LP_TAKER_FEE_BPS) as u128) .ok_or_else(|| anyhow!(FutarchyAmmError::MathOverflow))?; - let numerator = input_amount_with_lp_fee + let numerator = input_amount_after_lp_fee .checked_mul(output_reserve as u128) .ok_or_else(|| anyhow!(FutarchyAmmError::MathOverflow))?; let denominator = (input_reserve as u128) .checked_mul(MAX_BPS as u128) .ok_or_else(|| anyhow!(FutarchyAmmError::MathOverflow))? - .checked_add(input_amount_with_lp_fee as u128) + .checked_add(input_amount_after_lp_fee as u128) .ok_or_else(|| anyhow!(FutarchyAmmError::MathOverflow))?; let output_amount = (numerator diff --git a/programs/futarchy/src/state/futarchy_amm.rs b/programs/futarchy/src/state/futarchy_amm.rs index 2fcb0cb3e..d61e53911 100644 --- a/programs/futarchy/src/state/futarchy_amm.rs +++ b/programs/futarchy/src/state/futarchy_amm.rs @@ -509,13 +509,13 @@ impl Pool { require_neq!(input_reserve, 0); require_neq!(output_reserve, 0); - let input_amount_with_lp_fee = + let input_amount_after_lp_fee = input_amount_after_protocol_fee as u128 * (MAX_BPS - LP_TAKER_FEE_BPS) as u128; - let numerator = input_amount_with_lp_fee * output_reserve as u128; + let numerator = input_amount_after_lp_fee * output_reserve as u128; let denominator = - (input_reserve as u128 * MAX_BPS as u128) + input_amount_with_lp_fee as u128; + (input_reserve as u128 * MAX_BPS as u128) + input_amount_after_lp_fee as u128; let output_amount = (numerator / denominator) as u64; From a1beb2fc87946a799716cf63d6026f8f32164069 Mon Sep 17 00:00:00 2001 From: Pileks Date: Thu, 5 Feb 2026 14:05:16 -0800 Subject: [PATCH 20/29] slight rename for internal consistency --- .../admin_approve_execute_multisig_proposal.rs | 2 +- .../instructions/collect_meteora_damm_fees.rs | 2 +- .../src/instructions/complete_launch.rs | 18 +++++++++--------- .../initialize_performance_package.rs | 2 +- sdk/src/v0.7/LaunchpadClient.ts | 4 ++-- sdk/src/v0.7/types/launchpad_v7.ts | 4 ++-- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/programs/futarchy/src/instructions/admin_approve_execute_multisig_proposal.rs b/programs/futarchy/src/instructions/admin_approve_execute_multisig_proposal.rs index 673af6bd3..924dad343 100644 --- a/programs/futarchy/src/instructions/admin_approve_execute_multisig_proposal.rs +++ b/programs/futarchy/src/instructions/admin_approve_execute_multisig_proposal.rs @@ -15,7 +15,7 @@ pub struct AdminApproveExecuteMultisigProposal<'info> { #[account(mut)] pub admin: Signer<'info>, - /// CHECK: checked by autocrat program + /// CHECK: checked by futarchy program #[account(mut, seeds = [squads_multisig_program::SEED_PREFIX, squads_multisig_program::SEED_MULTISIG, dao.key().as_ref()], bump, seeds::program = squads_multisig_program)] pub squads_multisig: Account<'info, squads_multisig_program::Multisig>, /// CHECK: squads proposal, initialized by squads multisig program, checked by squads multisig program diff --git a/programs/futarchy/src/instructions/collect_meteora_damm_fees.rs b/programs/futarchy/src/instructions/collect_meteora_damm_fees.rs index d068f5e2d..93ce666c2 100644 --- a/programs/futarchy/src/instructions/collect_meteora_damm_fees.rs +++ b/programs/futarchy/src/instructions/collect_meteora_damm_fees.rs @@ -38,7 +38,7 @@ pub struct CollectMeteoraDammFees<'info> { #[account(mut)] pub admin: Signer<'info>, - /// CHECK: checked by autocrat program + /// CHECK: checked by futarchy program #[account(mut, seeds = [squads_multisig_program::SEED_PREFIX, squads_multisig_program::SEED_MULTISIG, dao.key().as_ref()], bump, seeds::program = squads_program)] pub squads_multisig: Account<'info, squads_multisig_program::Multisig>, /// CHECK: signer for the squads transaction, checked by squads program diff --git a/programs/v07_launchpad/src/instructions/complete_launch.rs b/programs/v07_launchpad/src/instructions/complete_launch.rs index 4880e0b50..cf3c98af6 100644 --- a/programs/v07_launchpad/src/instructions/complete_launch.rs +++ b/programs/v07_launchpad/src/instructions/complete_launch.rs @@ -35,8 +35,8 @@ use damm_v2_cpi::program::DammV2Cpi; pub struct StaticCompleteLaunchAccounts<'info> { pub futarchy_program: Program<'info, Futarchy>, pub token_metadata_program: Program<'info, Metadata>, - /// CHECK: checked by autocrat program - pub autocrat_event_authority: UncheckedAccount<'info>, + /// CHECK: checked by futarchy program + pub futarchy_event_authority: UncheckedAccount<'info>, pub squads_program: Program<'info, squads_multisig_program::program::SquadsMultisigProgram>, /// CHECK: checked by squads multisig program #[account(seeds = [squads_multisig_program::SEED_PREFIX, squads_multisig_program::SEED_PROGRAM_CONFIG], bump, seeds::program = squads_program)] @@ -178,23 +178,23 @@ pub struct CompleteLaunch<'info> { #[account(address = meteora_accounts.quote_mint.key())] pub quote_mint: Box>, - /// CHECK: init by autocrat + /// CHECK: init by futarchy program #[account(mut, seeds = [b"amm_position", dao.key().as_ref(), squads_multisig_vault.key().as_ref()], bump, seeds::program = static_accounts.futarchy_program)] pub dao_owned_lp_position: UncheckedAccount<'info>, - /// CHECK: checked by autocrat + /// CHECK: checked by futarchy program #[account(mut)] pub futarchy_amm_base_vault: UncheckedAccount<'info>, - /// CHECK: checked by autocrat + /// CHECK: checked by futarchy program #[account(mut)] pub futarchy_amm_quote_vault: UncheckedAccount<'info>, - /// CHECK: this is the DAO account, init by autocrat + /// CHECK: this is the DAO account, init by futarchy program #[account(mut)] pub dao: UncheckedAccount<'info>, - /// CHECK: checked by autocrat program + /// CHECK: checked by futarchy program #[account(mut, seeds = [squads_multisig_program::SEED_PREFIX, squads_multisig_program::SEED_MULTISIG, dao.key().as_ref()], bump, seeds::program = static_accounts.squads_program)] pub squads_multisig: UncheckedAccount<'info>, /// CHECK: just a signer @@ -397,7 +397,7 @@ impl CompleteLaunch<'_> { quote_mint: self.quote_mint.to_account_info(), event_authority: self .static_accounts - .autocrat_event_authority + .futarchy_event_authority .to_account_info(), program: self.static_accounts.futarchy_program.to_account_info(), squads_multisig: self.squads_multisig.to_account_info(), @@ -506,7 +506,7 @@ impl CompleteLaunch<'_> { program: self.static_accounts.futarchy_program.to_account_info(), event_authority: self .static_accounts - .autocrat_event_authority + .futarchy_event_authority .to_account_info(), }, launch_signer, diff --git a/programs/v07_launchpad/src/instructions/initialize_performance_package.rs b/programs/v07_launchpad/src/instructions/initialize_performance_package.rs index 1ed0d9809..5fe29aa9a 100644 --- a/programs/v07_launchpad/src/instructions/initialize_performance_package.rs +++ b/programs/v07_launchpad/src/instructions/initialize_performance_package.rs @@ -39,7 +39,7 @@ pub struct InitializePerformancePackage<'info> { #[account(mut, address = launch.base_mint.key())] pub base_mint: Box>, - /// CHECK: this is the DAO account, init by autocrat + /// CHECK: this is the DAO account, init by futarchy program #[account(address = launch.dao.as_ref().unwrap().key())] pub dao: UncheckedAccount<'info>, diff --git a/sdk/src/v0.7/LaunchpadClient.ts b/sdk/src/v0.7/LaunchpadClient.ts index 2a752b44e..c0c6cef6b 100644 --- a/sdk/src/v0.7/LaunchpadClient.ts +++ b/sdk/src/v0.7/LaunchpadClient.ts @@ -322,7 +322,7 @@ export class LaunchpadClient { daoCreator: launchSigner, }); - const [autocratEventAuthority] = getEventAuthorityAddr( + const [futarchyEventAuthority] = getEventAuthorityAddr( this.autocratClient.getProgramId(), ); @@ -464,7 +464,7 @@ export class LaunchpadClient { staticAccounts: { futarchyProgram: this.autocratClient.getProgramId(), tokenMetadataProgram: MPL_TOKEN_METADATA_PROGRAM_ID, - autocratEventAuthority, + futarchyEventAuthority, squadsProgram: SQUADS_PROGRAM_ID, squadsProgramConfig: SQUADS_PROGRAM_CONFIG, squadsProgramConfigTreasury: isDevnet diff --git a/sdk/src/v0.7/types/launchpad_v7.ts b/sdk/src/v0.7/types/launchpad_v7.ts index ca9d53543..98cf391a3 100644 --- a/sdk/src/v0.7/types/launchpad_v7.ts +++ b/sdk/src/v0.7/types/launchpad_v7.ts @@ -362,7 +362,7 @@ export type LaunchpadV7 = { isSigner: false; }, { - name: "autocratEventAuthority"; + name: "futarchyEventAuthority"; isMut: false; isSigner: false; }, @@ -2063,7 +2063,7 @@ export const IDL: LaunchpadV7 = { isSigner: false, }, { - name: "autocratEventAuthority", + name: "futarchyEventAuthority", isMut: false, isSigner: false, }, From 3a6d4742e7181cf96e69ae42d281331bf49a4f1c Mon Sep 17 00:00:00 2001 From: Pileks Date: Thu, 5 Feb 2026 15:32:31 -0800 Subject: [PATCH 21/29] ensure conditional liquidities are always greater than zero --- .../futarchy/src/instructions/launch_proposal.rs | 5 +++++ programs/futarchy/src/state/dao.rs | 12 ++++++++++++ .../src/instructions/complete_launch.rs | 4 ++-- .../src/instructions/complete_launch.rs | 4 ++-- 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/programs/futarchy/src/instructions/launch_proposal.rs b/programs/futarchy/src/instructions/launch_proposal.rs index 71d32f19a..06d0defc2 100644 --- a/programs/futarchy/src/instructions/launch_proposal.rs +++ b/programs/futarchy/src/instructions/launch_proposal.rs @@ -105,6 +105,11 @@ impl LaunchProposal<'_> { let base_to_lp = spot.base_reserves / 2; let quote_to_lp = spot.quote_reserves / 2; + // Prevent launching proposals with zero reserves, which would permanently + // freeze the DAO (no swaps possible, finalize_proposal fails, no recovery path) + require_gt!(base_to_lp, 0, FutarchyError::InsufficientLiquidity); + require_gt!(quote_to_lp, 0, FutarchyError::InsufficientLiquidity); + spot.base_reserves -= base_to_lp; spot.quote_reserves -= quote_to_lp; diff --git a/programs/futarchy/src/state/dao.rs b/programs/futarchy/src/state/dao.rs index 24e066cae..e31a41f58 100644 --- a/programs/futarchy/src/state/dao.rs +++ b/programs/futarchy/src/state/dao.rs @@ -97,6 +97,18 @@ impl Dao { FutarchyError::InvalidTeamSponsoredPassThreshold ); + require_gt!( + self.min_base_futarchic_liquidity, + 0, + FutarchyError::InsufficientLiquidity + ); + + require_gt!( + self.min_quote_futarchic_liquidity, + 0, + FutarchyError::InsufficientLiquidity + ); + Ok(()) } } diff --git a/programs/v06_launchpad/src/instructions/complete_launch.rs b/programs/v06_launchpad/src/instructions/complete_launch.rs index 62e746942..9704633f1 100644 --- a/programs/v06_launchpad/src/instructions/complete_launch.rs +++ b/programs/v06_launchpad/src/instructions/complete_launch.rs @@ -400,8 +400,8 @@ impl CompleteLaunch<'_> { twap_initial_observation: launch_price_1e12, twap_max_observation_change_per_update: launch_price_1e12 / 20, // We're providing liquidity, so that can be used for proposals - min_quote_futarchic_liquidity: 0, - min_base_futarchic_liquidity: 0, + min_quote_futarchic_liquidity: 1, + min_base_futarchic_liquidity: 1, pass_threshold_bps: 150, base_to_stake: TOKENS_TO_PARTICIPANTS / 20, seconds_per_proposal: 3 * 24 * 60 * 60, diff --git a/programs/v07_launchpad/src/instructions/complete_launch.rs b/programs/v07_launchpad/src/instructions/complete_launch.rs index cf3c98af6..8a7177386 100644 --- a/programs/v07_launchpad/src/instructions/complete_launch.rs +++ b/programs/v07_launchpad/src/instructions/complete_launch.rs @@ -423,8 +423,8 @@ impl CompleteLaunch<'_> { twap_initial_observation: launch_price_1e12, twap_max_observation_change_per_update: launch_price_1e12 / 20, // We're providing liquidity, so that can be used for proposals - min_quote_futarchic_liquidity: 0, - min_base_futarchic_liquidity: 0, + min_quote_futarchic_liquidity: 1, + min_base_futarchic_liquidity: 1, pass_threshold_bps: 300, base_to_stake: PROPOSAL_MIN_STAKE_TOKENS, seconds_per_proposal: 3 * 24 * 60 * 60, From d062aa895ba21e0dcaaa567a29bfa6903257a2e4 Mon Sep 17 00:00:00 2001 From: Pileks Date: Thu, 5 Feb 2026 15:42:53 -0800 Subject: [PATCH 22/29] ensure max_base_amount is a positive nonzero integer when providing liquidity --- programs/futarchy/src/instructions/provide_liquidity.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/programs/futarchy/src/instructions/provide_liquidity.rs b/programs/futarchy/src/instructions/provide_liquidity.rs index d009725ca..95bd53843 100644 --- a/programs/futarchy/src/instructions/provide_liquidity.rs +++ b/programs/futarchy/src/instructions/provide_liquidity.rs @@ -119,6 +119,7 @@ impl ProvideLiquidity<'_> { } else { // equivalent to $0.1 if the quote is USDC, here for rounding require_gte!(quote_amount, MIN_QUOTE_LIQUIDITY); + require_gt!(max_base_amount, 0); let base_amount = max_base_amount; From a213eda59e75411cc5776bfbe1ec643a0041dda8 Mon Sep 17 00:00:00 2001 From: Pileks Date: Thu, 5 Feb 2026 16:01:07 -0800 Subject: [PATCH 23/29] short-circuit arbitrage when it doesn't yield results --- programs/futarchy/src/state/futarchy_amm.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/programs/futarchy/src/state/futarchy_amm.rs b/programs/futarchy/src/state/futarchy_amm.rs index d61e53911..d107b4ebd 100644 --- a/programs/futarchy/src/state/futarchy_amm.rs +++ b/programs/futarchy/src/state/futarchy_amm.rs @@ -668,6 +668,15 @@ pub fn arbitrage_after_spot_swap( } } + // No profitable arbitrage found — skip the feeless swaps to save compute + if best_input_amount == 0 { + return Ok(ArbitrageResult { + spot_profit: 0, + pass_profit: 0, + fail_profit: 0, + }); + } + let final_spot_output = spot .feeless_swap(best_input_amount, spot_direction) .unwrap(); @@ -789,6 +798,15 @@ pub fn arbitrage_after_conditional_swap( } } + // No profitable arbitrage found — skip the feeless swaps to save compute + if best_arb_input_amount == 0 { + return Ok(ArbitrageResult { + spot_profit: 0, + pass_profit: 0, + fail_profit: 0, + }); + } + let final_pass_output = pass .feeless_swap(best_arb_input_amount, conditional_direction) .unwrap(); From d1cd0e61ee927f9ef500632b23addf9a60556ffa Mon Sep 17 00:00:00 2001 From: Pileks Date: Thu, 5 Feb 2026 16:36:42 -0800 Subject: [PATCH 24/29] refactor seeds and invariants into constants --- .../admin_approve_execute_multisig_proposal.rs | 2 +- .../futarchy/src/instructions/collect_fees.rs | 2 +- .../instructions/collect_meteora_damm_fees.rs | 2 +- .../src/instructions/conditional_swap.rs | 2 +- .../execute_spending_limit_change.rs | 2 +- .../src/instructions/finalize_proposal.rs | 4 ++-- .../src/instructions/initialize_dao.rs | 4 ++-- .../src/instructions/initialize_proposal.rs | 2 +- .../src/instructions/provide_liquidity.rs | 2 +- .../futarchy/src/instructions/spot_swap.rs | 6 +----- .../src/instructions/stake_to_proposal.rs | 2 +- .../src/instructions/unstake_from_proposal.rs | 4 ++-- .../src/instructions/withdraw_liquidity.rs | 4 ++-- programs/futarchy/src/state/amm_position.rs | 2 ++ programs/futarchy/src/state/dao.rs | 18 +++++++++++++----- programs/futarchy/src/state/proposal.rs | 2 ++ programs/futarchy/src/state/stake_account.rs | 2 ++ 17 files changed, 36 insertions(+), 26 deletions(-) diff --git a/programs/futarchy/src/instructions/admin_approve_execute_multisig_proposal.rs b/programs/futarchy/src/instructions/admin_approve_execute_multisig_proposal.rs index 924dad343..824c637ef 100644 --- a/programs/futarchy/src/instructions/admin_approve_execute_multisig_proposal.rs +++ b/programs/futarchy/src/instructions/admin_approve_execute_multisig_proposal.rs @@ -77,7 +77,7 @@ impl<'info, 'c: 'info> AdminApproveExecuteMultisigProposal<'info> { let dao_nonce = &dao.nonce.to_le_bytes(); let dao_creator_key = &dao.dao_creator.as_ref(); - let dao_seeds = &[b"dao".as_ref(), dao_creator_key, dao_nonce, &[dao.pda_bump]]; + let dao_seeds = &[SEED_DAO, dao_creator_key, dao_nonce, &[dao.pda_bump]]; let dao_signer = &[&dao_seeds[..]]; // Approve the proposal diff --git a/programs/futarchy/src/instructions/collect_fees.rs b/programs/futarchy/src/instructions/collect_fees.rs index 68b8ae3a4..63b934e6f 100644 --- a/programs/futarchy/src/instructions/collect_fees.rs +++ b/programs/futarchy/src/instructions/collect_fees.rs @@ -68,7 +68,7 @@ impl CollectFees<'_> { let dao_creator = dao.dao_creator; let nonce = dao.nonce.to_le_bytes(); let signer_seeds = &[ - b"dao".as_ref(), + SEED_DAO, dao_creator.as_ref(), nonce.as_ref(), &[dao.pda_bump], diff --git a/programs/futarchy/src/instructions/collect_meteora_damm_fees.rs b/programs/futarchy/src/instructions/collect_meteora_damm_fees.rs index 93ce666c2..3d8c387ce 100644 --- a/programs/futarchy/src/instructions/collect_meteora_damm_fees.rs +++ b/programs/futarchy/src/instructions/collect_meteora_damm_fees.rs @@ -238,7 +238,7 @@ impl CollectMeteoraDammFees<'_> { let dao_nonce = &ctx.accounts.dao.nonce.to_le_bytes(); let dao_creator_key = ctx.accounts.dao.dao_creator.as_ref(); let dao_seeds = &[ - b"dao".as_ref(), + SEED_DAO, dao_creator_key, dao_nonce, &[ctx.accounts.dao.pda_bump], diff --git a/programs/futarchy/src/instructions/conditional_swap.rs b/programs/futarchy/src/instructions/conditional_swap.rs index 82bb34fc5..f7702a5ec 100644 --- a/programs/futarchy/src/instructions/conditional_swap.rs +++ b/programs/futarchy/src/instructions/conditional_swap.rs @@ -164,7 +164,7 @@ impl ConditionalSwap<'_> { let dao_creator = dao.dao_creator; let nonce = dao.nonce.to_le_bytes(); let signer_seeds = &[ - b"dao".as_ref(), + SEED_DAO, dao_creator.as_ref(), nonce.as_ref(), &[dao.pda_bump], diff --git a/programs/futarchy/src/instructions/execute_spending_limit_change.rs b/programs/futarchy/src/instructions/execute_spending_limit_change.rs index 5f46afdb6..644f84d88 100644 --- a/programs/futarchy/src/instructions/execute_spending_limit_change.rs +++ b/programs/futarchy/src/instructions/execute_spending_limit_change.rs @@ -73,7 +73,7 @@ impl<'info, 'c: 'info> ExecuteSpendingLimitChange<'info> { let dao_nonce = &dao.nonce.to_le_bytes(); let dao_creator_key = &dao.dao_creator.as_ref(); - let dao_seeds = &[b"dao".as_ref(), dao_creator_key, dao_nonce, &[dao.pda_bump]]; + let dao_seeds = &[SEED_DAO, dao_creator_key, dao_nonce, &[dao.pda_bump]]; let dao_signer = &[&dao_seeds[..]]; squads_multisig_program::cpi::vault_transaction_execute( diff --git a/programs/futarchy/src/instructions/finalize_proposal.rs b/programs/futarchy/src/instructions/finalize_proposal.rs index 1abf2eb8a..03efde3f6 100644 --- a/programs/futarchy/src/instructions/finalize_proposal.rs +++ b/programs/futarchy/src/instructions/finalize_proposal.rs @@ -109,7 +109,7 @@ impl FinalizeProposal<'_> { let squads_proposal_key = squads_proposal.key(); let proposal_seeds = &[ - b"proposal", + SEED_PROPOSAL, squads_proposal_key.as_ref(), &[proposal.pda_bump], ]; @@ -175,7 +175,7 @@ impl FinalizeProposal<'_> { let dao_nonce = &dao.nonce.to_le_bytes(); let dao_creator_key = &dao.dao_creator.as_ref(); - let dao_seeds = &[b"dao".as_ref(), dao_creator_key, dao_nonce, &[dao.pda_bump]]; + let dao_seeds = &[SEED_DAO, dao_creator_key, dao_nonce, &[dao.pda_bump]]; let dao_signer = &[&dao_seeds[..]]; if new_proposal_state == ProposalState::Passed { diff --git a/programs/futarchy/src/instructions/initialize_dao.rs b/programs/futarchy/src/instructions/initialize_dao.rs index a2503459f..b271f51c0 100644 --- a/programs/futarchy/src/instructions/initialize_dao.rs +++ b/programs/futarchy/src/instructions/initialize_dao.rs @@ -27,7 +27,7 @@ pub struct InitializeDao<'info> { #[account( init, payer = payer, - seeds = [b"dao", dao_creator.key().as_ref(), params.nonce.to_le_bytes().as_ref()], + seeds = [SEED_DAO, dao_creator.key().as_ref(), params.nonce.to_le_bytes().as_ref()], bump, space = 8 + Dao::INIT_SPACE, )] @@ -90,7 +90,7 @@ impl InitializeDao<'_> { let creator_key = ctx.accounts.dao_creator.key(); let dao_seeds = &[ - b"dao".as_ref(), + SEED_DAO, creator_key.as_ref(), &nonce.to_le_bytes(), &[ctx.bumps.dao], diff --git a/programs/futarchy/src/instructions/initialize_proposal.rs b/programs/futarchy/src/instructions/initialize_proposal.rs index 13265e222..47d1a6b4e 100644 --- a/programs/futarchy/src/instructions/initialize_proposal.rs +++ b/programs/futarchy/src/instructions/initialize_proposal.rs @@ -7,7 +7,7 @@ pub struct InitializeProposal<'info> { init, payer = payer, space = 8 + Proposal::INIT_SPACE, - seeds = [b"proposal", squads_proposal.key().as_ref()], + seeds = [SEED_PROPOSAL, squads_proposal.key().as_ref()], bump )] pub proposal: Box>, diff --git a/programs/futarchy/src/instructions/provide_liquidity.rs b/programs/futarchy/src/instructions/provide_liquidity.rs index 95bd53843..bcf0503a0 100644 --- a/programs/futarchy/src/instructions/provide_liquidity.rs +++ b/programs/futarchy/src/instructions/provide_liquidity.rs @@ -50,7 +50,7 @@ pub struct ProvideLiquidity<'info> { #[account( init_if_needed, payer = payer, - seeds = [b"amm_position", dao.key().as_ref(), params.position_authority.key().as_ref()], + seeds = [SEED_AMM_POSITION, dao.key().as_ref(), params.position_authority.key().as_ref()], bump, space = 8 + AmmPosition::INIT_SPACE, )] diff --git a/programs/futarchy/src/instructions/spot_swap.rs b/programs/futarchy/src/instructions/spot_swap.rs index 97b76f905..224353a37 100644 --- a/programs/futarchy/src/instructions/spot_swap.rs +++ b/programs/futarchy/src/instructions/spot_swap.rs @@ -98,13 +98,9 @@ impl SpotSwap<'_> { input_amount, )?; - // let dao_key = dao.key(); - // let dao_creator = dao.dao_creator; - // let nonce = dao.nonce; - // let signer_seeds = &[b"dao".as_ref(), dao_creator.as_ref(), nonce.to_le_bytes().as_ref(), &[dao.pda_bump]]; let dao_nonce = &dao.nonce.to_le_bytes(); let dao_creator_key = &dao.dao_creator.as_ref(); - let dao_seeds = &[b"dao".as_ref(), dao_creator_key, dao_nonce, &[dao.pda_bump]]; + let dao_seeds = &[SEED_DAO, dao_creator_key, dao_nonce, &[dao.pda_bump]]; token::transfer( CpiContext::new_with_signer( diff --git a/programs/futarchy/src/instructions/stake_to_proposal.rs b/programs/futarchy/src/instructions/stake_to_proposal.rs index 1c349e2dc..0a0281d26 100644 --- a/programs/futarchy/src/instructions/stake_to_proposal.rs +++ b/programs/futarchy/src/instructions/stake_to_proposal.rs @@ -28,7 +28,7 @@ pub struct StakeToProposal<'info> { #[account( init_if_needed, payer = payer, - seeds = [b"stake", proposal.key().as_ref(), staker.key().as_ref()], + seeds = [SEED_STAKE, proposal.key().as_ref(), staker.key().as_ref()], bump, space = 8 + StakeAccount::INIT_SPACE, )] diff --git a/programs/futarchy/src/instructions/unstake_from_proposal.rs b/programs/futarchy/src/instructions/unstake_from_proposal.rs index ec0af8e6f..c29328de0 100644 --- a/programs/futarchy/src/instructions/unstake_from_proposal.rs +++ b/programs/futarchy/src/instructions/unstake_from_proposal.rs @@ -27,7 +27,7 @@ pub struct UnstakeFromProposal<'info> { pub proposal_base_account: Box>, #[account( mut, - seeds = [b"stake", proposal.key().as_ref(), staker.key().as_ref()], + seeds = [SEED_STAKE, proposal.key().as_ref(), staker.key().as_ref()], bump = stake_account.bump, )] pub stake_account: Box>, @@ -70,7 +70,7 @@ impl UnstakeFromProposal<'_> { // Transfer tokens from proposal back to staker let seeds = &[ - b"proposal", + SEED_PROPOSAL, proposal.squads_proposal.as_ref(), &[proposal.pda_bump], ]; diff --git a/programs/futarchy/src/instructions/withdraw_liquidity.rs b/programs/futarchy/src/instructions/withdraw_liquidity.rs index 4d7b773a8..9f8ac37e4 100644 --- a/programs/futarchy/src/instructions/withdraw_liquidity.rs +++ b/programs/futarchy/src/instructions/withdraw_liquidity.rs @@ -44,7 +44,7 @@ pub struct WithdrawLiquidity<'info> { pub amm_quote_vault: Account<'info, TokenAccount>, #[account( mut, - seeds = [b"amm_position", dao.key().as_ref(), position_authority.key().as_ref()], + seeds = [SEED_AMM_POSITION, dao.key().as_ref(), position_authority.key().as_ref()], bump, has_one = dao, has_one = position_authority, @@ -125,7 +125,7 @@ impl WithdrawLiquidity<'_> { let dao_creator = dao.dao_creator; let nonce = dao.nonce.to_le_bytes(); let signer_seeds = &[ - b"dao".as_ref(), + SEED_DAO, dao_creator.as_ref(), nonce.as_ref(), &[dao.pda_bump], diff --git a/programs/futarchy/src/state/amm_position.rs b/programs/futarchy/src/state/amm_position.rs index 2da1fc30e..9555e9d25 100644 --- a/programs/futarchy/src/state/amm_position.rs +++ b/programs/futarchy/src/state/amm_position.rs @@ -1,5 +1,7 @@ use anchor_lang::prelude::*; +pub const SEED_AMM_POSITION: &[u8] = b"amm_position"; + #[account] #[derive(InitSpace)] pub struct AmmPosition { diff --git a/programs/futarchy/src/state/dao.rs b/programs/futarchy/src/state/dao.rs index e31a41f58..48d97d9fd 100644 --- a/programs/futarchy/src/state/dao.rs +++ b/programs/futarchy/src/state/dao.rs @@ -1,7 +1,15 @@ pub use super::*; +pub const SEED_DAO: &[u8] = b"dao"; + pub const MAX_SPENDING_LIMIT_MEMBERS: usize = 10; +pub const MIN_PROPOSAL_DURATION_TWAP_MULTIPLIER: u32 = 2; +pub const MIN_PROPOSAL_DURATION_SECONDS: u32 = 60 * 60 * 24; +pub const MAX_PASS_THRESHOLD_BPS: u16 = 1_000; +pub const MAX_TEAM_SPONSORED_PASS_THRESHOLD_BPS: i16 = 1_000; +pub const MIN_TEAM_SPONSORED_PASS_THRESHOLD_BPS: i16 = -1_000; + #[account] #[derive(InitSpace)] pub struct Dao { @@ -69,30 +77,30 @@ impl Dao { pub fn invariant(&self) -> Result<()> { require_gte!( self.seconds_per_proposal, - self.twap_start_delay_seconds * 2, + self.twap_start_delay_seconds * MIN_PROPOSAL_DURATION_TWAP_MULTIPLIER, FutarchyError::ProposalDurationTooShort ); require_gte!( self.seconds_per_proposal, - 60 * 60 * 24, + MIN_PROPOSAL_DURATION_SECONDS, FutarchyError::ProposalDurationTooShort ); require_gte!( - 1_000, + MAX_PASS_THRESHOLD_BPS, self.pass_threshold_bps, FutarchyError::PassThresholdTooHigh ); require_gte!( self.team_sponsored_pass_threshold_bps, - -1_000, + MIN_TEAM_SPONSORED_PASS_THRESHOLD_BPS, FutarchyError::InvalidTeamSponsoredPassThreshold ); require_gte!( - 1_000, + MAX_TEAM_SPONSORED_PASS_THRESHOLD_BPS, self.team_sponsored_pass_threshold_bps, FutarchyError::InvalidTeamSponsoredPassThreshold ); diff --git a/programs/futarchy/src/state/proposal.rs b/programs/futarchy/src/state/proposal.rs index ae8ff9e71..331bf9e21 100644 --- a/programs/futarchy/src/state/proposal.rs +++ b/programs/futarchy/src/state/proposal.rs @@ -1,5 +1,7 @@ use super::*; +pub const SEED_PROPOSAL: &[u8] = b"proposal"; + #[derive(Clone, Copy, AnchorSerialize, AnchorDeserialize, PartialEq, Eq, Debug, InitSpace)] pub enum ProposalState { Draft { amount_staked: u64 }, diff --git a/programs/futarchy/src/state/stake_account.rs b/programs/futarchy/src/state/stake_account.rs index cd3f35815..afd44f122 100644 --- a/programs/futarchy/src/state/stake_account.rs +++ b/programs/futarchy/src/state/stake_account.rs @@ -1,5 +1,7 @@ use super::*; +pub const SEED_STAKE: &[u8] = b"stake"; + #[account] #[derive(InitSpace)] pub struct StakeAccount { From 00c2da7909e3f1e2a381094ae2554b35a112e582 Mon Sep 17 00:00:00 2001 From: Pileks Date: Thu, 5 Feb 2026 16:51:22 -0800 Subject: [PATCH 25/29] explicitly prevent silent truncation in pass thresholds --- programs/futarchy/src/instructions/finalize_proposal.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/programs/futarchy/src/instructions/finalize_proposal.rs b/programs/futarchy/src/instructions/finalize_proposal.rs index 03efde3f6..aeacfe146 100644 --- a/programs/futarchy/src/instructions/finalize_proposal.rs +++ b/programs/futarchy/src/instructions/finalize_proposal.rs @@ -142,7 +142,8 @@ impl FinalizeProposal<'_> { let threshold_bps = if proposal.is_team_sponsored { dao.team_sponsored_pass_threshold_bps } else { - dao.pass_threshold_bps as i16 + // Thanks to invariants this will never error - still it's better to be safe here. + i16::try_from(dao.pass_threshold_bps).map_err(|_| FutarchyError::CastingOverflow)? }; // this can't overflow because each twap can only be MAX_PRICE (~1e31), From 83d2079619e38a9a26f7de87a16cb1c4795a3d38 Mon Sep 17 00:00:00 2001 From: Pileks Date: Thu, 5 Feb 2026 16:58:03 -0800 Subject: [PATCH 26/29] ensure max twap change is nonzero --- programs/futarchy/src/state/dao.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/programs/futarchy/src/state/dao.rs b/programs/futarchy/src/state/dao.rs index 48d97d9fd..04a5c10bb 100644 --- a/programs/futarchy/src/state/dao.rs +++ b/programs/futarchy/src/state/dao.rs @@ -117,6 +117,12 @@ impl Dao { FutarchyError::InsufficientLiquidity ); + require_gt!( + self.twap_max_observation_change_per_update, + 0u128, + FutarchyError::InvalidMaxObservationChange + ); + Ok(()) } } From 75b8d71ab5ca6ea06fe8cde35605ccd599ba04ea Mon Sep 17 00:00:00 2001 From: Pileks Date: Thu, 5 Feb 2026 17:33:41 -0800 Subject: [PATCH 27/29] rename variable & comments to reflect actual situation --- programs/futarchy/src/state/futarchy_amm.rs | 14 +++++++------- sdk/src/v0.7/types/futarchy.ts | 20 ++++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/programs/futarchy/src/state/futarchy_amm.rs b/programs/futarchy/src/state/futarchy_amm.rs index d107b4ebd..4dc6d7cd4 100644 --- a/programs/futarchy/src/state/futarchy_amm.rs +++ b/programs/futarchy/src/state/futarchy_amm.rs @@ -268,16 +268,16 @@ impl PoolState { #[derive(Default, Clone, Copy, Debug, AnchorDeserialize, AnchorSerialize, InitSpace)] pub struct TwapOracle { - /// Running sum of slots_per_last_update * last_observation. + /// Running sum of seconds_since_last_update * last_observation. /// /// Assuming latest observations are as big as possible (u64::MAX * 1e12), - /// we can store 18 million slots worth of observations, which turns out to - /// be ~85 days worth of slots. + /// we can store 18 million seconds worth of observations, which turns out to + /// be ~208 days. /// /// Assuming that latest observations are 100x smaller than they could theoretically - /// be, we can store 8500 days (23 years) worth of them. Even this is a very + /// be, we can store ~57 years worth of them. Even this is a very /// very conservative assumption - META/USDC prices should be between 1e9 and - /// 1e15, which would overflow after 1e15 years worth of slots. + /// 1e15, which would overflow after 1e15 years. /// /// So in the case of an overflow, the aggregator rolls back to 0. It's the /// client's responsibility to sanity check the assets or to handle an @@ -400,13 +400,13 @@ impl Pool { let effective_last_updated_timestamp = oracle.last_updated_timestamp.max(twap_start_timestamp); - let slot_difference: u128 = (current_timestamp - effective_last_updated_timestamp) + let time_difference: u128 = (current_timestamp - effective_last_updated_timestamp) .try_into() .unwrap(); // if this saturates, the aggregator will wrap back to 0, so this value doesn't // really matter. we just can't panic. - let weighted_observation = new_observation.saturating_mul(slot_difference); + let weighted_observation = new_observation.saturating_mul(time_difference); oracle.aggregator.wrapping_add(weighted_observation) }; diff --git a/sdk/src/v0.7/types/futarchy.ts b/sdk/src/v0.7/types/futarchy.ts index ee544d9d8..816d54e4e 100644 --- a/sdk/src/v0.7/types/futarchy.ts +++ b/sdk/src/v0.7/types/futarchy.ts @@ -1856,16 +1856,16 @@ export type Futarchy = { { name: "aggregator"; docs: [ - "Running sum of slots_per_last_update * last_observation.", + "Running sum of seconds_since_last_update * last_observation.", "", "Assuming latest observations are as big as possible (u64::MAX * 1e12),", - "we can store 18 million slots worth of observations, which turns out to", - "be ~85 days worth of slots.", + "we can store 18 million seconds worth of observations, which turns out to", + "be ~208 days.", "", "Assuming that latest observations are 100x smaller than they could theoretically", - "be, we can store 8500 days (23 years) worth of them. Even this is a very", + "be, we can store ~57 years worth of them. Even this is a very", "very conservative assumption - META/USDC prices should be between 1e9 and", - "1e15, which would overflow after 1e15 years worth of slots.", + "1e15, which would overflow after 1e15 years.", "", "So in the case of an overflow, the aggregator rolls back to 0. It's the", "client's responsibility to sanity check the assets or to handle an", @@ -4907,16 +4907,16 @@ export const IDL: Futarchy = { { name: "aggregator", docs: [ - "Running sum of slots_per_last_update * last_observation.", + "Running sum of seconds_since_last_update * last_observation.", "", "Assuming latest observations are as big as possible (u64::MAX * 1e12),", - "we can store 18 million slots worth of observations, which turns out to", - "be ~85 days worth of slots.", + "we can store 18 million seconds worth of observations, which turns out to", + "be ~208 days.", "", "Assuming that latest observations are 100x smaller than they could theoretically", - "be, we can store 8500 days (23 years) worth of them. Even this is a very", + "be, we can store ~57 years worth of them. Even this is a very", "very conservative assumption - META/USDC prices should be between 1e9 and", - "1e15, which would overflow after 1e15 years worth of slots.", + "1e15, which would overflow after 1e15 years.", "", "So in the case of an overflow, the aggregator rolls back to 0. It's the", "client's responsibility to sanity check the assets or to handle an", From 6e8c5aa32e5a3ef39384ea72fc84e0014ae3f92d Mon Sep 17 00:00:00 2001 From: Pileks Date: Fri, 6 Feb 2026 11:35:11 -0800 Subject: [PATCH 28/29] disallow empty spending limit members and duplicate spending limit members --- .../src/instructions/initialize_launch.rs | 13 ++++ .../unit/initializeLaunch.test.ts | 67 +++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/programs/v07_launchpad/src/instructions/initialize_launch.rs b/programs/v07_launchpad/src/instructions/initialize_launch.rs index e648ed0d3..262c2aec6 100644 --- a/programs/v07_launchpad/src/instructions/initialize_launch.rs +++ b/programs/v07_launchpad/src/instructions/initialize_launch.rs @@ -154,6 +154,19 @@ impl InitializeLaunch<'_> { LaunchpadError::InvalidMonthlySpendingLimitMembers ); + require!( + !args.monthly_spending_limit_members.is_empty(), + LaunchpadError::InvalidMonthlySpendingLimitMembers + ); + + let mut sorted_members = args.monthly_spending_limit_members.clone(); + sorted_members.sort(); + let has_duplicates = sorted_members.windows(2).any(|win| win[0] == win[1]); + require!( + !has_duplicates, + LaunchpadError::InvalidMonthlySpendingLimitMembers + ); + require_gte!( args.months_until_insiders_can_unlock, 18, diff --git a/tests/launchpad_v7/unit/initializeLaunch.test.ts b/tests/launchpad_v7/unit/initializeLaunch.test.ts index d025dc575..6a4615fa2 100644 --- a/tests/launchpad_v7/unit/initializeLaunch.test.ts +++ b/tests/launchpad_v7/unit/initializeLaunch.test.ts @@ -108,6 +108,73 @@ export default function suite() { assert.isNull(storedLaunch.dao); }); + it("fails when monthly spending limit members contains duplicates", async function () { + const minRaise = new BN(1000_000000); + const secondsForLaunch = 60 * 60 * 24 * 7; + const monthlySpend = new BN(100_000000); + const recipientAddress = Keypair.generate().publicKey; + const premineAmount = new BN(500_000_000); + + try { + await launchpadClient + .initializeLaunchIx({ + tokenName: "META", + tokenSymbol: "META", + tokenUri: "https://example.com", + minimumRaiseAmount: minRaise, + secondsForLaunch: secondsForLaunch, + baseMint: META, + quoteMint: MAINNET_USDC, + monthlySpendingLimitAmount: monthlySpend, + monthlySpendingLimitMembers: [ + this.payer.publicKey, + this.payer.publicKey, + ], + performancePackageGrantee: recipientAddress, + performancePackageTokenAmount: premineAmount, + monthsUntilInsidersCanUnlock: 18, + teamAddress: PublicKey.default, + launchAuthority: launchAuthority.publicKey, + }) + .rpc(); + assert.fail("Should have thrown error"); + } catch (e) { + assert.include(e.message, "InvalidMonthlySpendingLimitMembers"); + } + }); + + it("fails when monthly spending limit members is empty", async function () { + const minRaise = new BN(1000_000000); + const secondsForLaunch = 60 * 60 * 24 * 7; + const monthlySpend = new BN(100_000000); + const recipientAddress = Keypair.generate().publicKey; + const premineAmount = new BN(500_000_000); + + try { + await launchpadClient + .initializeLaunchIx({ + tokenName: "META", + tokenSymbol: "META", + tokenUri: "https://example.com", + minimumRaiseAmount: minRaise, + secondsForLaunch: secondsForLaunch, + baseMint: META, + quoteMint: MAINNET_USDC, + monthlySpendingLimitAmount: monthlySpend, + monthlySpendingLimitMembers: [], + performancePackageGrantee: recipientAddress, + performancePackageTokenAmount: premineAmount, + monthsUntilInsidersCanUnlock: 18, + teamAddress: PublicKey.default, + launchAuthority: launchAuthority.publicKey, + }) + .rpc(); + assert.fail("Should have thrown error"); + } catch (e) { + assert.include(e.message, "InvalidMonthlySpendingLimitMembers"); + } + }); + it("fails when launch signer is faked", async function () { const minRaise = new BN(1000_000000); // 1000 USDC const secondsForLaunch = 60 * 60 * 24 * 7; // 1 week From 056755d715e5a0bc6dff7487e64e7038bf3e1996 Mon Sep 17 00:00:00 2001 From: Pileks Date: Wed, 11 Feb 2026 14:05:04 -0800 Subject: [PATCH 29/29] box dao account where applicable --- programs/futarchy/src/instructions/collect_fees.rs | 2 +- programs/futarchy/src/instructions/update_dao.rs | 2 +- programs/futarchy/src/instructions/withdraw_liquidity.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/programs/futarchy/src/instructions/collect_fees.rs b/programs/futarchy/src/instructions/collect_fees.rs index 63b934e6f..7dbf667cf 100644 --- a/programs/futarchy/src/instructions/collect_fees.rs +++ b/programs/futarchy/src/instructions/collect_fees.rs @@ -17,7 +17,7 @@ pub mod metadao_admin { #[event_cpi] pub struct CollectFees<'info> { #[account(mut)] - pub dao: Account<'info, Dao>, + pub dao: Box>, pub admin: Signer<'info>, #[account(mut, associated_token::mint = dao.base_mint, associated_token::authority = metadao_multisig_vault::ID)] pub base_token_account: Account<'info, TokenAccount>, diff --git a/programs/futarchy/src/instructions/update_dao.rs b/programs/futarchy/src/instructions/update_dao.rs index 672fa26f6..7d57ebb5a 100644 --- a/programs/futarchy/src/instructions/update_dao.rs +++ b/programs/futarchy/src/instructions/update_dao.rs @@ -18,7 +18,7 @@ pub struct UpdateDaoParams { #[event_cpi] pub struct UpdateDao<'info> { #[account(mut, has_one = squads_multisig_vault)] - pub dao: Account<'info, Dao>, + pub dao: Box>, pub squads_multisig_vault: Signer<'info>, } diff --git a/programs/futarchy/src/instructions/withdraw_liquidity.rs b/programs/futarchy/src/instructions/withdraw_liquidity.rs index 9f8ac37e4..9f598ca08 100644 --- a/programs/futarchy/src/instructions/withdraw_liquidity.rs +++ b/programs/futarchy/src/instructions/withdraw_liquidity.rs @@ -16,7 +16,7 @@ pub struct WithdrawLiquidityParams { #[event_cpi] pub struct WithdrawLiquidity<'info> { #[account(mut)] - pub dao: Account<'info, Dao>, + pub dao: Box>, pub position_authority: Signer<'info>, #[account( mut,