Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d7fc89a
close_bid_wall should return funds to authority always
pileks Jan 21, 2026
9d2ff64
set bid wall fee recipient to metadao multisig vault everywhere
pileks Jan 21, 2026
e9ce2e2
add min_amount_out to bid wall sell instruction
pileks Jan 21, 2026
6800e45
disallow zero output amount after fees
pileks Jan 21, 2026
e271fd0
Merge remote-tracking branch 'origin/develop' into pileks/met-75-fixes
pileks Jan 23, 2026
8748a38
prevent LP position hijack/freeze
pileks Jan 23, 2026
d356561
prevent supplying wrong multisig when initializing proposal
pileks Jan 23, 2026
3e4b4e7
prevent overcharging by 1 atom in provide_liquidity
pileks Jan 24, 2026
f01a1ba
apply min_liquidity slippage parameter to both first and subsequent l…
pileks Jan 24, 2026
c9db7a9
replace unreachable with proper errors
pileks Jan 24, 2026
f72bdba
change comment to point to correct implementation reference
pileks Jan 26, 2026
11408ba
prevent launch front-running by merging instructions into single tran…
pileks Jan 28, 2026
efaca01
remove MAX_PREMINE from launchpad v7
pileks Jan 28, 2026
3030762
Disallow oracle changes while in Unlocking state to prevent TWAP corr…
pileks Jan 28, 2026
46d9613
adjust liquidity provision logic when position is a new account
pileks Feb 3, 2026
e8c014e
When a Proposal is Rejected by the Market The Squads Proposal Should …
pileks Feb 3, 2026
66e4a02
prevent dao parameters being upadted during active futarchy markets
pileks Feb 4, 2026
3865e80
split_tokens and merge_tokens should verify question is unresolved
pileks Feb 4, 2026
b8068e6
Merge remote-tracking branch 'origin/develop' into pileks/met-75-fixes
pileks Feb 4, 2026
99be7ea
Merge remote-tracking branch 'origin/develop' into pileks/met-75-fixes
pileks Feb 5, 2026
9d35ae8
bid wall quote amount debits rounding
pileks Feb 5, 2026
5ae1419
minor rename
pileks Feb 5, 2026
a1beb2f
slight rename for internal consistency
pileks Feb 5, 2026
6e8c5aa
disallow empty spending limit members and duplicate spending limit me…
pileks Feb 6, 2026
c7e641c
Merge remote-tracking branch 'origin/develop' into pileks/met-75-fixes
pileks Feb 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions jup-sdk/src/futarchy_amm.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions programs/bid_wall/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ pub enum BidWallError {
InvalidInputAmount,
#[msg("Invalid crank address")]
InvalidCrankAddress,
#[msg("Insufficient output amount")]
InsufficientOutputAmount,
}
5 changes: 3 additions & 2 deletions programs/bid_wall/src/instructions/cancel_bid_wall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand All @@ -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)]
Expand Down
9 changes: 5 additions & 4 deletions programs/bid_wall/src/instructions/close_bid_wall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand All @@ -13,7 +14,7 @@ use crate::{
pub struct CloseBidWall<'info> {
#[account(
mut,
close=payer,
close=authority,
has_one = authority
)]
pub bid_wall: Account<'info, BidWall>,
Expand All @@ -22,11 +23,11 @@ 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
#[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)]
Expand Down
8 changes: 1 addition & 7 deletions programs/bid_wall/src/instructions/collect_fees.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
6 changes: 4 additions & 2 deletions programs/bid_wall/src/instructions/initialize_bid_wall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use anchor_spl::{

use crate::{
events::{BidWallInitializedEvent, CommonFields},
metadao_multisig_vault,
state::BidWall,
usdc_mint,
};
Expand Down Expand Up @@ -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>,
Expand Down
33 changes: 28 additions & 5 deletions programs/bid_wall/src/instructions/sell_tokens.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -85,7 +86,10 @@ impl SellTokens<'_> {
}

pub fn handle(ctx: Context<Self>, 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
Expand All @@ -103,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(
Expand Down Expand Up @@ -147,8 +157,21 @@ impl SellTokens<'_> {
amount_out_after_fee,
)?;

// 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;
require_gte!(
amount_out_after_fee,
min_amount_out,
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 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.
Expand Down
7 changes: 7 additions & 0 deletions programs/bid_wall/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = 300;

pub const TOKEN_SCALE: u64 = 1_000_000;
Expand Down
8 changes: 8 additions & 0 deletions programs/conditional_vault/src/instructions/merge_tokens.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
8 changes: 8 additions & 0 deletions programs/conditional_vault/src/instructions/split_tokens.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
2 changes: 2 additions & 0 deletions programs/conditional_vault/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,15 @@ 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,
) -> Result<()> {
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions programs/futarchy/src/instructions/finalize_proposal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion programs/futarchy/src/instructions/initialize_proposal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pub struct InitializeProposal<'info> {
pub proposal: Box<Account<'info, Proposal>>,
pub squads_proposal: Box<Account<'info, squads_multisig_program::Proposal>>,
pub squads_multisig: Box<Account<'info, squads_multisig_program::Multisig>>,
#[account(mut)]
#[account(mut, has_one = squads_multisig)]
pub dao: Box<Account<'info, Dao>>,
#[account(
constraint = question.oracle == proposal.key()
Expand Down
42 changes: 31 additions & 11 deletions programs/futarchy/src/instructions/provide_liquidity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,12 @@ impl ProvideLiquidity<'_> {
quote_amount,
max_base_amount,
min_liquidity,
position_authority: _,
position_authority,
} = params;

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 {
Expand All @@ -95,8 +94,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)?;

Expand All @@ -122,17 +124,35 @@ 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)
};

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,
});
// 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 {
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;

Expand Down Expand Up @@ -168,7 +188,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,
Expand Down
8 changes: 8 additions & 0 deletions programs/futarchy/src/instructions/update_dao.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self>, dao_params: UpdateDaoParams) -> Result<()> {
let dao = &mut ctx.accounts.dao;

Expand Down
Loading
Loading