Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 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
3a6d474
ensure conditional liquidities are always greater than zero
pileks Feb 5, 2026
d062aa8
ensure max_base_amount is a positive nonzero integer when providing l…
pileks Feb 5, 2026
a213eda
short-circuit arbitrage when it doesn't yield results
pileks Feb 6, 2026
d1cd0e6
refactor seeds and invariants into constants
pileks Feb 6, 2026
00c2da7
explicitly prevent silent truncation in pass thresholds
pileks Feb 6, 2026
83d2079
ensure max twap change is nonzero
pileks Feb 6, 2026
75b8d71
rename variable & comments to reflect actual situation
pileks Feb 6, 2026
6e8c5aa
disallow empty spending limit members and duplicate spending limit me…
pileks Feb 6, 2026
25f986f
Merge branch 'pileks/met-75-fixes' into pileks/met-100-address-issues
pileks Feb 6, 2026
0c0e29f
Merge remote-tracking branch 'origin/develop' into pileks/met-100-add…
pileks Feb 11, 2026
056755d
box dao account where applicable
pileks Feb 11, 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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions programs/futarchy/src/instructions/collect_fees.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub mod metadao_admin {
#[event_cpi]
pub struct CollectFees<'info> {
#[account(mut)]
pub dao: Account<'info, Dao>,
pub dao: Box<Account<'info, Dao>>,
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>,
Expand Down Expand Up @@ -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],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
2 changes: 1 addition & 1 deletion programs/futarchy/src/instructions/conditional_swap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
7 changes: 4 additions & 3 deletions programs/futarchy/src/instructions/finalize_proposal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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],
];
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -175,7 +176,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 {
Expand Down
4 changes: 2 additions & 2 deletions programs/futarchy/src/instructions/initialize_dao.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)]
Expand Down Expand Up @@ -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],
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 @@ -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<Account<'info, Proposal>>,
Expand Down
5 changes: 5 additions & 0 deletions programs/futarchy/src/instructions/launch_proposal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
3 changes: 2 additions & 1 deletion programs/futarchy/src/instructions/provide_liquidity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)]
Expand Down Expand Up @@ -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;

Expand Down
6 changes: 1 addition & 5 deletions programs/futarchy/src/instructions/spot_swap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion programs/futarchy/src/instructions/stake_to_proposal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)]
Expand Down
4 changes: 2 additions & 2 deletions programs/futarchy/src/instructions/unstake_from_proposal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ pub struct UnstakeFromProposal<'info> {
pub proposal_base_account: Box<Account<'info, TokenAccount>>,
#[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<Account<'info, StakeAccount>>,
Expand Down Expand Up @@ -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],
];
Expand Down
2 changes: 1 addition & 1 deletion programs/futarchy/src/instructions/update_dao.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Account<'info, Dao>>,
pub squads_multisig_vault: Signer<'info>,
}

Expand Down
6 changes: 3 additions & 3 deletions programs/futarchy/src/instructions/withdraw_liquidity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub struct WithdrawLiquidityParams {
#[event_cpi]
pub struct WithdrawLiquidity<'info> {
#[account(mut)]
pub dao: Account<'info, Dao>,
pub dao: Box<Account<'info, Dao>>,
pub position_authority: Signer<'info>,
#[account(
mut,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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],
Expand Down
2 changes: 2 additions & 0 deletions programs/futarchy/src/state/amm_position.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use anchor_lang::prelude::*;

pub const SEED_AMM_POSITION: &[u8] = b"amm_position";

#[account]
#[derive(InitSpace)]
pub struct AmmPosition {
Expand Down
36 changes: 31 additions & 5 deletions programs/futarchy/src/state/dao.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -69,34 +77,52 @@ 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
);

require_gt!(
self.min_base_futarchic_liquidity,
0,
FutarchyError::InsufficientLiquidity
);

require_gt!(
self.min_quote_futarchic_liquidity,
0,
FutarchyError::InsufficientLiquidity
);

require_gt!(
self.twap_max_observation_change_per_update,
0u128,
FutarchyError::InvalidMaxObservationChange
);

Ok(())
}
}
32 changes: 25 additions & 7 deletions programs/futarchy/src/state/futarchy_amm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
};
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
2 changes: 2 additions & 0 deletions programs/futarchy/src/state/proposal.rs
Original file line number Diff line number Diff line change
@@ -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 },
Expand Down
2 changes: 2 additions & 0 deletions programs/futarchy/src/state/stake_account.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use super::*;

pub const SEED_STAKE: &[u8] = b"stake";

#[account]
#[derive(InitSpace)]
pub struct StakeAccount {
Expand Down
4 changes: 2 additions & 2 deletions programs/v06_launchpad/src/instructions/complete_launch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading