Skip to content

Mini-instructions (MVP) - #477

Open
metapileks wants to merge 30 commits into
developfrom
pileks/met-543-mini-instructions
Open

Mini-instructions (MVP)#477
metapileks wants to merge 30 commits into
developfrom
pileks/met-543-mini-instructions

Conversation

@metapileks

@metapileks metapileks commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Replaces futarchy v0.6's one-size-fits-all proposals with a fixed catalog of typed governance actions. For every type except the execute_arbitrary catch-all, futarchy builds the Squads vault transaction itself from a per-type template at create.

The catalog

Action Who can propose Market Threshold Council can block Cooldown on failure
large_spend(amount) Team (via sponsorship) 1.5 days −10% Yes
mint_tokens(amount, recipient) Anyone 5 days +5% Yes
spending_limit_change(config) Team (via sponsorship) 5 days +5% Yes
execute_arbitrary Anyone 10 days +10% Yes
hostile_takeover(new_team, limit_action) Anyone 20 days +10% No 20 days
hostile_liquidate(liquidator) Anyone 10 days +25% No 10 days

Program changes

Added

  • initialize_*_proposal — permissionless creates that build the vault tx from the type's template (typed_create.rs).
  • set_spending_limit — vault-signed writer of the on-DAO spending-limit record (replaces execute_spending_limit_change, which bypassed the record)
  • sync_spending_limit — permissionless dirty-gated crank projecting the record onto the Squads SpendingLimit (futarchy signs as config authority)
  • apply_liquidation — vault-signed and kind-checked (requires a passed HostileLiquidate proposal, so liquidation can't ride execute_arbitrary): installs the liquidator, zeroes the limit record, sweeps the treasury's own AMM position
  • State: typed ProposalAction on Proposal (borsh tag doubles as the kind) + snapshotted signed threshold and blockable flag; Dao gains liquidator: Option<Pubkey>, per-hostile last-failed timestamps, spending_limit_dirty, and initial_spending_limit promoted to the authoritative spending-limit record

Modified

  • initialize_proposal is now the execute_arbitrary path (externally supplied, unvalidated, at the stricter uniform 10d/+10%)
  • launch_proposal — kind-aware sponsorship and cooldown checks alongside the DAO's existing anti-grief gate
  • finalize_proposal — uses the proposal's snapshotted threshold (not DAO config) and stamps hostile failure timestamps
  • admin_cancel_proposal — respects the blockable flag (council can cancel routine types mid-market, never hostile ones)
  • update_dao — spending-limit-neutral; team_address only gates sponsorship
  • Liquidated guards on state-mutating instructions (third-party withdraw_liquidity stays open); liquidated DAOs execute via a liquidator-gated enqueue → permissionless approve → ordinary Squads execution
  • Per-DAO pass_threshold_bps / seconds_per_proposal / team_sponsored_pass_threshold_bps become vestigial

Removed

  • Optimistic proposal machinery: initiate_vault_spend_optimistic_proposal, finalize_optimistic_proposal, and their special cases in create/launch/admin paths
  • execute_spending_limit_change

Migration

  • resize_dao / resize_proposal cranks; legacy proposals snapshot to ExecuteArbitrary from the vestigial per-DAO fields, optimistic values cleared

SDK

  • Crate 0.6.1 → 0.6.2; previous IDL frozen as v0.6.0, new v0.6.1 IDL snapshot added alongside it
  • FutarchyClient methods and PDA helpers for every new instruction, including packing sync_spending_limit into execution so the stale-limit window is zero in the normal path

Greptile Summary

Introduces typed futarchy governance actions and their fixed market parameters, replacing the previous one-size-fits-all proposal flow.

  • Builds action-specific Squads vault transactions during proposal creation.
  • Adds authoritative spending-limit state with permissionless synchronization to Squads.
  • Adds hostile takeover and liquidation flows, including terminal liquidated-DAO guards and liquidator-controlled execution.
  • Adds migration cranks, SDK support, IDL snapshots, and extensive unit and integration coverage.

Confidence Score: 5/5

The PR appears safe to merge based on the reviewed typed-action bindings, migration behavior, liquidation lifecycle, and spending-limit synchronization.

The changed proposal templates bind action payloads and target accounts into their Squads transactions, liquidation is constrained to passed hostile-liquidation proposals with terminal replay protection, and spending-limit synchronization preserves atomic state through Solana transaction rollback.

Important Files Changed

Filename Overview
programs/futarchy/src/instructions/typed_create.rs Centralizes permissionless typed proposal creation and binds generated Squads transactions to proposal action snapshots.
programs/futarchy/src/instructions/apply_liquidation.rs Applies passed hostile liquidation proposals, installs the liquidator, disables spending rights, and sweeps the treasury-owned AMM position.
programs/futarchy/src/instructions/sync_spending_limit.rs Dirty-gates projection of the authoritative DAO spending-limit record onto the corresponding Squads account.
programs/futarchy/src/instructions/launch_proposal.rs Adds action-specific sponsorship and hostile cooldown checks while retaining liquidity and Squads-state validation.
programs/futarchy/src/instructions/finalize_proposal.rs Finalizes markets using snapshotted thresholds and records failed hostile-action cooldown timestamps.
programs/futarchy/src/instructions/resize_proposal.rs Migrates legacy proposals with their existing threshold semantics and classifies them as blockable arbitrary-execution proposals.
programs/futarchy/src/state/proposal_action.rs Defines the typed governance action catalog and fixed action-specific governance parameters.
sdk/src/futarchy/v0.6/FutarchyClient.ts Adds SDK builders and account derivations for typed proposal, spending-limit, liquidation, and migration operations.

Reviews (1): Last reviewed commit: "update idl, reduce cycles in tests" | Re-trigger Greptile

@metapileks metapileks self-assigned this Jul 22, 2026
@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Repository Guard

  • Cargo.lock: pass
  • yarn.lock (root): pass
  • yarn.lock (sdk): pass
  • Repo guard: pass

Repository Guard

Cargo dependency pinning

  • Status: pass
  • Every programs/*/Cargo.toml dep uses =x.y.z, a path = .. workspace ref, or a git dep with a 40-char rev.

Cross-program Anchor/Solana version consistency

  • Status: pass
  • anchor-lang and anchor-spl are pinned to the version declared in repo-guard.toml across every program.

solana-program crate pin

  • Status: pass
  • Every solana-program = "=X" declaration is =1.17.14 (locked to match Cargo.lock).

Anchor.toml solana_version

  • Status: pass
  • Anchor.toml declares solana_version = "1.17.34" (local-dev install for anchor test).

Crate minimum age

  • Status: pass
  • All Cargo deps changed by this PR are at least 14 days old on crates.io.

Yarn package.json pinning

  • Status: pass
  • All package.json deps use exact versions (no ^, ~, ranges).

npm minimum age

  • Status: pass
  • All npm deps changed by this PR are at least 14 days old.

Workflow toolchain consistency

  • Status: pass
  • Every workflow declares anchor-version: 0.29.0.
  • Per-file solana-cli-version values match [toolchain.workflow_solana_cli] in repo-guard.toml.

GitHub Action SHA pinning

  • Status: pass
  • Every third-party action is pinned to a SHA in [actions.sha_allowlist].

Sensitive program / config changes

  • Status: warn
  • Review hint only (CODEOWNERS is the merge gate). Lines below match heuristics for security-sensitive changes:
  • High-sensitivity files touched: Anchor.toml, Cargo.lock
  • programs/futarchy/src/instructions/initialize_hostile_liquidate_proposal.rs:45 Program ID constant or variable change -> + program_id: crate::ID,
  • programs/futarchy/src/instructions/initialize_hostile_takeover_proposal.rs:40 Program ID constant or variable change -> + program_id: crate::ID,
  • programs/futarchy/src/instructions/initialize_hostile_takeover_proposal.rs:80 Program ID constant or variable change -> + program_id: crate::ID,
  • programs/futarchy/src/instructions/initialize_mint_tokens_proposal.rs:99 Program ID constant or variable change -> + program_id: mint_governor::ID,
  • programs/futarchy/src/instructions/initialize_spending_limit_change_proposal.rs:43 Program ID constant or variable change -> + program_id: crate::ID,
  • tests/futarchy/integration/liquidationEndToEnd.test.ts:306 Program ID constant or variable change -> + squadsMultisigProgram: multisig.PROGRAM_ID,
  • tests/futarchy/unit/initializeHostileLiquidateProposal.test.ts:98 Hardcoded Solana address literal -> + programId: new PublicKey("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"),
  • tests/futarchy/unit/liquidatorPath.test.ts:279 Program ID constant or variable change -> + squadsMultisigProgram: multisig.PROGRAM_ID,
  • tests/futarchy/unit/setSpendingLimit.test.ts:90 Program ID constant or variable change -> + squadsMultisigProgram: multisig.PROGRAM_ID,
  • tests/futarchy/unit/syncSpendingLimit.test.ts:128 Program ID constant or variable change -> + squadsMultisigProgram: multisig.PROGRAM_ID,

Overall status: pass

Lockfile freshness (Cargo.lock + yarn.lock) is checked by the workflow directly and cannot be bypassed. The sensitive-diff section is a review hint - CODEOWNERS handles the actual merge gate.

@metapileks metapileks changed the title Mini instructions Mini-instructions (MVP) Jul 23, 2026
@metapileks
metapileks marked this pull request as ready for review July 23, 2026 23:01
Comment on lines +72 to +79
// 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.
require!(
self.proposal.council_can_block,
FutarchyError::InvalidProposalKind
);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like admin should be more for security stuff and we can have a different one for council blocking proposals, maybe? Not a strong opinion

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. Mainly holding this here until we make the decision on whether we're merging in #469 so that we know whether that will be the council (one of the parties in the multisig), or we will have a separate council that can cancel a proposal at any time.

Comment on lines +66 to +68
// required signer. Enqueueing is the only capability the liquidator
// gains: the approve leg stays permissionless and execution is
// ordinary top-level Squads execution.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this work?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We reuse our existing system of enqueueing and executing an admin-controller Squads proposal approval, but now whoever is the liquidator can do the enqueueing of the approval. Running the approval itself is always permissionless.
The reason for it having two instructions (enqueue approval, execute approval) is because that way we can enqueue using another Squads multisig then permissionlessly approve, thus avoiding reentrancy into Squads (if it were a single instruction).
Squads (liquidator/admin) -> Futarchy -> Squads (DAO) is the reentrancy path we're avoiding.

Comment on lines +146 to +151
position.liquidity = 0;
{
let mut data = amm_position.try_borrow_mut_data()?;
let mut writer: &mut [u8] = &mut data;
position.try_serialize(&mut writer)?;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feels janky

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me know if it feels a bit less jank now that there's an explicit comment and less code around it.

AmmPosition::try_deserialize(&mut &data[..])?
};

if position.liquidity > 0 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we not want to internally call the withdraw liquidity instruction?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First reason is that Anchor's serialization approach, by design, doesn't catch changes that happen in inner instructions. We have to manually call .reload() on all accounts that we need to touch after the CPI, and any accounts we touch before the CPI would not properly reflect within it, as their changes are not yet written to account data.

Second reason is that we'd see two events - one "regular" WithdrawLiquidityEvent and later another ApplyLiquidationEvent, instead of just the ApplyLiquidationEvent.

I've now changed it so that it uses a withdraw_from_position function shared between withdraw_liquidity and apply_liquidation, whose purpose is to run the withdrawal logic (including token transfers). This way we still emit a single event per instruction, and we contain (most) of the account updates. The only exception is the UncheckedAccount for the position in apply_liquidation, but that is a simple addition and now it's properly documented.

Comment on lines +166 to +169
dao.last_failed_takeover_at = clock.unix_timestamp
}
ProposalAction::HostileLiquidate { .. } => {
dao.last_failed_liquidation_at = clock.unix_timestamp

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the point of these?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want to prevent hostile proposals from being retried immediately, as they are "out of our control" in terms of us vetoing them.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a comment describing this.

}

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is this object?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feels a little smelly

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May just need a better name

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to TypedInitializeAccounts and typed_initialize_accounts respectively, as it will be clearer that we're talking about a typed initialization and its associated common accounts, as opposed to a "generic" initialization. Didn't like common for this, as it's not descriptive enough.

data: crate::instruction::ApplyLiquidation.data(),
};

let event = create.create_proposal(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may want to add a memo instruction that's like transfer IP back to team

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, will add.

pub create: TypedCreateAccounts<'info>,
}

impl InitializeLargeSpendProposal<'_> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is one that the team should only be able to create

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So you are thinking both create AND launch should be gated? Right now anyone can create these, but only the team can approve launching these, as they need to sponsor them.

pub mint_authority: Option<Box<Account<'info, mint_governor::MintAuthority>>>,
}

impl InitializeMintTokensProposal<'_> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, I think this should be only team

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +19 to +23
/// Only for governed mints (v0.8 launches): the `MintGovernor` holding the
/// base mint's authority.
pub mint_governor: Option<Box<Account<'info, mint_governor::MintGovernor>>>,
/// Only for governed mints: the vault's minting rights on `mint_governor`.
pub mint_authority: Option<Box<Account<'info, mint_governor::MintAuthority>>>,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Smart

@metaproph3t

metaproph3t commented Aug 1, 2026

Copy link
Copy Markdown
Member

Initial pass completed

I think we should have some inline docs at this point in lib.rs to explain everything

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants