Mini-instructions (MVP) - #477
Conversation
…fields, errors, events
|
Repository Guard
Repository GuardCargo dependency pinning
Cross-program Anchor/Solana version consistency
solana-program crate pin
Anchor.toml solana_version
Crate minimum age
Yarn package.json pinning
npm minimum age
Workflow toolchain consistency
GitHub Action SHA pinning
Sensitive program / config changes
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. |
| // 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 | ||
| ); | ||
|
|
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
| // required signer. Enqueueing is the only capability the liquidator | ||
| // gains: the approve leg stays permissionless and execution is | ||
| // ordinary top-level Squads execution. |
There was a problem hiding this comment.
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.
| position.liquidity = 0; | ||
| { | ||
| let mut data = amm_position.try_borrow_mut_data()?; | ||
| let mut writer: &mut [u8] = &mut data; | ||
| position.try_serialize(&mut writer)?; | ||
| } |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
Do we not want to internally call the withdraw liquidity instruction?
There was a problem hiding this comment.
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.
| dao.last_failed_takeover_at = clock.unix_timestamp | ||
| } | ||
| ProposalAction::HostileLiquidate { .. } => { | ||
| dao.last_failed_liquidation_at = clock.unix_timestamp |
There was a problem hiding this comment.
We want to prevent hostile proposals from being retried immediately, as they are "out of our control" in terms of us vetoing them.
There was a problem hiding this comment.
Added a comment describing this.
| } | ||
|
|
||
| pub fn handle(ctx: Context<Self>, args: InitializeHostileLiquidateProposalArgs) -> Result<()> { | ||
| let create = &mut ctx.accounts.create; |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
We may want to add a memo instruction that's like transfer IP back to team
There was a problem hiding this comment.
Agreed, will add.
| pub create: TypedCreateAccounts<'info>, | ||
| } | ||
|
|
||
| impl InitializeLargeSpendProposal<'_> { |
There was a problem hiding this comment.
I think this is one that the team should only be able to create
There was a problem hiding this comment.
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<'_> { |
There was a problem hiding this comment.
Same here, I think this should be only team
| /// 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>>>, |
|
Initial pass completed I think we should have some inline docs at this point in lib.rs to explain everything |
Replaces futarchy v0.6's one-size-fits-all proposals with a fixed catalog of typed governance actions. For every type except the
execute_arbitrarycatch-all, futarchy builds the Squads vault transaction itself from a per-type template at create.The catalog
large_spend(amount)mint_tokens(amount, recipient)spending_limit_change(config)execute_arbitraryhostile_takeover(new_team, limit_action)hostile_liquidate(liquidator)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 (replacesexecute_spending_limit_change, which bypassed the record)sync_spending_limit— permissionless dirty-gated crank projecting the record onto the SquadsSpendingLimit(futarchy signs as config authority)apply_liquidation— vault-signed and kind-checked (requires a passedHostileLiquidateproposal, so liquidation can't rideexecute_arbitrary): installs the liquidator, zeroes the limit record, sweeps the treasury's own AMM positionProposalActiononProposal(borsh tag doubles as the kind) + snapshotted signed threshold and blockable flag;Daogainsliquidator: Option<Pubkey>, per-hostile last-failed timestamps,spending_limit_dirty, andinitial_spending_limitpromoted to the authoritative spending-limit recordModified
initialize_proposalis now theexecute_arbitrarypath (externally supplied, unvalidated, at the stricter uniform 10d/+10%)launch_proposal— kind-aware sponsorship and cooldown checks alongside the DAO's existing anti-grief gatefinalize_proposal— uses the proposal's snapshotted threshold (not DAO config) and stamps hostile failure timestampsadmin_cancel_proposal— respects the blockable flag (council can cancel routine types mid-market, never hostile ones)update_dao— spending-limit-neutral;team_addressonly gates sponsorshipwithdraw_liquiditystays open); liquidated DAOs execute via a liquidator-gated enqueue → permissionless approve → ordinary Squads executionpass_threshold_bps/seconds_per_proposal/team_sponsored_pass_threshold_bpsbecome vestigialRemoved
initiate_vault_spend_optimistic_proposal,finalize_optimistic_proposal, and their special cases in create/launch/admin pathsexecute_spending_limit_changeMigration
resize_dao/resize_proposalcranks; legacy proposals snapshot toExecuteArbitraryfrom the vestigial per-DAO fields, optimistic values clearedSDK
v0.6.0, newv0.6.1IDL snapshot added alongside itFutarchyClientmethods and PDA helpers for every new instruction, including packingsync_spending_limitinto execution so the stale-limit window is zero in the normal pathGreptile Summary
Introduces typed futarchy governance actions and their fixed market parameters, replacing the previous one-size-fits-all proposal flow.
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
Reviews (1): Last reviewed commit: "update idl, reduce cycles in tests" | Re-trigger Greptile