Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
271de9a
immediately mark dao as liquidated on proposal finalization
metapileks Aug 14, 2026
aa4b238
dao resize should include spending limit
metapileks Aug 14, 2026
0382481
force zeroing of spending limit before dao liquidation, if spending l…
metapileks Aug 14, 2026
cfc6196
prevent large spend from launching with stale data
metapileks Aug 15, 2026
01d4e4c
proposal migration should hande draft and live proposals differently
metapileks Aug 15, 2026
66d1ee0
remove LP withdrawal from direct liquidation path - allow liquidation…
metapileks Aug 16, 2026
d084bff
disallow finalization and cancellation of unmigrated proposals
metapileks Aug 16, 2026
2e0006d
prevent pre-migration DAO from running instructions with wrong state
metapileks Aug 16, 2026
ed7331c
minor refactor
metapileks Aug 16, 2026
f5974e1
further spending limit validation
metapileks Aug 16, 2026
695272b
buyback proposal kind - track AMM quote reserves by observation
metapileks Aug 17, 2026
c3d39e7
adjust buyback for jup trigger order
metapileks Aug 17, 2026
eee0ab8
allow spot swap for a liquidated dao
metapileks Aug 17, 2026
a26af32
lock liquidator to metadao multisig vault
metapileks Aug 21, 2026
84036b8
prevent hostile takeover from accepting new team same as old team
metapileks Aug 26, 2026
24d738e
fix(futarchy): forbid team sponsorship of hostile proposals
metapileks Aug 26, 2026
e680091
chore(futarchy): drop unused AlreadyLiquidated error
metapileks Aug 26, 2026
6029616
test(liquidation): dedupe second mint in refund test
metapileks Aug 26, 2026
fcf9612
style(futarchy): cargo fmt
metapileks Aug 27, 2026
8a7b224
fix(futarchy): add liquidation guard to sponsor_proposal
metapileks Aug 28, 2026
f7d5cf3
chore(futarchy): add comment regarding double-liquidation being impos…
metapileks Aug 31, 2026
84eb3be
fix(futarchy): replace is_team_sponsored with sponsored_by for granul…
metapileks Sep 2, 2026
96d5771
fix(futarchy): disable optimistic governance flag on migration
metapileks Sep 2, 2026
a7262e3
feat(futarchy): admin proposal cancellation
metapileks Sep 2, 2026
eb88cd5
chore(futarchy): expose admin approve in SDK, refactor tests
metapileks Sep 2, 2026
20443bf
Merge remote-tracking branch 'origin/develop' into pileks/mini-instru…
metapileks Sep 3, 2026
85dd92c
Merge remote-tracking branch 'origin/develop' into pileks/mini-instru…
metapileks Sep 4, 2026
05f8a5c
Merge remote-tracking branch 'origin/develop' into pileks/mini-instru…
metapileks Sep 4, 2026
658c342
Merge remote-tracking branch 'origin/pileks/met-543-mini-instructions…
metapileks Sep 15, 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
28 changes: 24 additions & 4 deletions programs/futarchy/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,6 @@ pub enum FutarchyError {
SpendingLimitNotDirty,
#[msg("Wrong proposal kind for this instruction")]
InvalidProposalKind,
#[msg("This DAO has already been liquidated")]
AlreadyLiquidated,
#[msg("A spending limit can have at most 10 members")]
TooManySpendingLimitMembers,
#[msg("Invalid liquidator")]
Expand All @@ -118,18 +116,40 @@ pub enum FutarchyError {
EmptyProposalParamsUpdate,
#[msg("Buyback amount exceeds 25% of the treasury")]
BuybackCapExceeded,
#[msg("The total must be an exact multiple of the non-zero per-cycle amount, at least twice over")]
#[msg("Buyback total must be non-zero")]
InvalidBuybackAmount,
#[msg("Cycle frequency must be between 60 seconds and 1 year")]
InvalidBuybackCycleFrequency,
#[msg("Start delay must be at most 30 days")]
InvalidBuybackStartDelay,
#[msg("min_price must be no greater than max_price")]
InvalidBuybackPriceBand,
#[msg("A treasury account is neither a vault-owned quote account nor the treasury's AMM position")]
#[msg(
"A treasury account is neither a vault-owned quote account nor the treasury's AMM position"
)]
InvalidTreasuryAccount,
#[msg("Treasury accounts must be in strictly ascending key order")]
TreasuryAccountsNotSorted,
#[msg("This proposal kind's launch takes no extra accounts")]
UnexpectedLaunchAccounts,
#[msg("Spending limit account is not the canonical spending-limit PDA")]
InvalidSpendingLimitAccount,
#[msg("The DAO's team has changed since this draft was created")]
StaleTeamAddress,
#[msg("Account is not migrated to latest layout")]
AccountNotMigrated,
#[msg("A spending limit's monthly amount must be non-zero")]
InvalidSpendingLimitAmount,
#[msg("A spending limit must have at least one member")]
EmptySpendingLimitMembers,
#[msg("A spending limit's members must be unique")]
DuplicateSpendingLimitMember,
#[msg("A buyback must run at least two cycles")]
InvalidBuybackCycleCount,
#[msg("Invalid team address")]
InvalidTeamAddress,
#[msg("This proposal kind cannot be team-sponsored")]
TeamSponsorshipForbidden,
#[msg("Squads proposal must be in Approved status to be cancelled")]
SquadsProposalNotApproved,
}
11 changes: 0 additions & 11 deletions programs/futarchy/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,14 +262,3 @@ pub struct SyncSpendingLimitEvent {
/// `None` = no limit (removed or never existed).
pub config: Option<InitialSpendingLimit>,
}

#[event]
pub struct ApplyLiquidationEvent {
pub common: CommonFields,
pub dao: Pubkey,
pub proposal: Pubkey,
pub liquidator: Pubkey,
pub base_swept: u64,
pub quote_swept: u64,
pub post_amm_state: FutarchyAmm,
}
4 changes: 4 additions & 0 deletions programs/futarchy/src/instructions/admin_cancel_proposal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ pub struct AdminCancelProposal<'info> {

impl AdminCancelProposal<'_> {
pub fn validate(&self) -> Result<()> {
// Ensure the proposal and DAO are migrated.
Proposal::assert_migrated(&self.proposal.to_account_info())?;
Dao::assert_migrated(&self.dao.to_account_info())?;

// Unblockable proposals are censorship-proof once live: nobody, including
// the council, can cancel them. Reads the create-time snapshot so a
// live proposal keeps the flag it launched with.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ pub struct AdminEnqueueMultisigProposalApproval<'info> {

impl AdminEnqueueMultisigProposalApproval<'_> {
pub fn validate(&self, _args: &AdminEnqueueMultisigProposalApprovalArgs) -> Result<()> {
// Ensure the DAO is migrated before reading `liquidator`.
Dao::assert_migrated(&self.dao.to_account_info())?;

// On a liquidated DAO the liquidator replaces the admin id as the
// required signer. Enqueueing is the only capability the liquidator
// gains: the approve leg stays permissionless and execution is
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
use super::*;

mod admin {
use anchor_lang::prelude::declare_id;

// MetaDAO ops multisig — the same signer as the approval enqueue
declare_id!("6awyHMshBGVjJ3ozdSJdyyDE1CTAXUwrpNMaRGMsb4sf");

Check warning on line 7 in programs/futarchy/src/instructions/admin_enqueue_multisig_proposal_cancellation.rs

View workflow job for this annotation

GitHub Actions / repository-guard

declare_id! literal change; Hardcoded Solana address literal: + declare_id!("6awyHMshBGVjJ3ozdSJdyyDE1CTAXUwrpNMaRGMsb4sf");
}

#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)]
pub struct AdminEnqueueMultisigProposalCancellationArgs {
pub transaction_index: u64,
}

#[derive(Accounts)]
#[instruction(args: AdminEnqueueMultisigProposalCancellationArgs)]
pub struct AdminEnqueueMultisigProposalCancellation<'info> {
#[account(has_one = squads_multisig)]
pub dao: Account<'info, Dao>,

#[account(mut)]
pub admin: Signer<'info>,

#[account(
seeds = [
squads_multisig_program::SEED_PREFIX,
squads_multisig_program::SEED_MULTISIG,
dao.key().as_ref(),
],
bump,
seeds::program = squads_multisig_program::ID,
)]
pub squads_multisig: Account<'info, squads_multisig_program::Multisig>,

#[account(
seeds = [
squads_multisig_program::SEED_PREFIX,
squads_multisig.key().as_ref(),
squads_multisig_program::SEED_TRANSACTION,
args.transaction_index.to_le_bytes().as_ref(),
squads_multisig_program::SEED_PROPOSAL,
],
bump,
seeds::program = squads_multisig_program::ID,
)]
pub squads_multisig_proposal: Account<'info, squads_multisig_program::Proposal>,

#[account(
init,
payer = admin,
space = 8 + EnqueuedMultisigProposalCancellation::INIT_SPACE,
seeds = [
SEED_ENQUEUED_MULTISIG_PROPOSAL_CANCELLATION,
dao.key().as_ref(),
args.transaction_index.to_le_bytes().as_ref(),
],
bump,
)]
pub enqueued_cancellation: Account<'info, EnqueuedMultisigProposalCancellation>,

pub system_program: Program<'info, System>,
}

impl AdminEnqueueMultisigProposalCancellation<'_> {
pub fn validate(&self, _args: &AdminEnqueueMultisigProposalCancellationArgs) -> Result<()> {
// Ensure the DAO is migrated before reading `liquidator`.
Dao::assert_migrated(&self.dao.to_account_info())?;

// On a liquidated DAO the liquidator replaces the admin id as the
// required signer. Enqueueing is the only capability the liquidator
// gains: the cancel leg stays permissionless.
match self.dao.liquidator {
Some(liquidator) => {
require_keys_eq!(
self.admin.key(),
liquidator,
FutarchyError::InvalidLiquidator
);
}
None => {
#[cfg(feature = "production")]
require_keys_eq!(self.admin.key(), admin::ID, FutarchyError::InvalidAdmin);
}
}

validate_squads_proposal_for_cancellation(
&self.squads_multisig_proposal,
&self.dao.squads_multisig,
)?;

Ok(())
}

pub fn handle(
ctx: Context<Self>,
args: AdminEnqueueMultisigProposalCancellationArgs,
) -> Result<()> {
let enqueued = &mut ctx.accounts.enqueued_cancellation;

enqueued.dao = ctx.accounts.dao.key();
enqueued.transaction_index = args.transaction_index;
enqueued.pda_bump = ctx.bumps.enqueued_cancellation;

Ok(())
}
}

/// A cancellation targets an `Approved` proposal. Squads permits cancelling a
/// stale proposal, so there is no stale-index check here.
pub fn validate_squads_proposal_for_cancellation(
squads_proposal: &squads_multisig_program::Proposal,
dao_multisig_key: &Pubkey,
) -> Result<()> {
require_keys_eq!(squads_proposal.multisig, *dao_multisig_key);

require!(
matches!(
squads_proposal.status,
squads_multisig_program::ProposalStatus::Approved { .. }
),
FutarchyError::SquadsProposalNotApproved
);

Ok(())
}
163 changes: 0 additions & 163 deletions programs/futarchy/src/instructions/apply_liquidation.rs

This file was deleted.

Loading
Loading