-
Notifications
You must be signed in to change notification settings - Fork 85
Mini-instructions (MVP) #477
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
ba8e57f
3e81a70
0ccd17b
ba791c3
0d6ab0b
d3dcea3
759a8a1
b26d280
fc4bb82
ef2fbf7
9bf7aa5
cd1afde
9515959
59e57c6
d8a8b0c
e9a8d78
13db286
ee5bb42
94166c0
7f42131
21a61a2
44bfa45
8d25d31
686f9d5
a8110ca
a298864
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -62,17 +62,28 @@ pub struct AdminEnqueueMultisigProposalApproval<'info> { | |
|
|
||
| impl AdminEnqueueMultisigProposalApproval<'_> { | ||
| pub fn validate(&self, _args: &AdminEnqueueMultisigProposalApprovalArgs) -> Result<()> { | ||
| #[cfg(feature = "production")] | ||
| require_keys_eq!(self.admin.key(), admin::ID, FutarchyError::InvalidAdmin); | ||
| // 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 | ||
| // ordinary top-level Squads execution. | ||
|
Comment on lines
+66
to
+68
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How does this work? |
||
| 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); | ||
| } | ||
| } | ||
|
|
||
| if !matches!(self.dao.amm.state, PoolState::Spot { .. }) { | ||
| return Err(FutarchyError::PoolNotInSpotState.into()); | ||
| } | ||
|
|
||
| if self.dao.optimistic_proposal.is_some() { | ||
| return Err(FutarchyError::ActiveOptimisticProposalAlreadyEnqueued.into()); | ||
| } | ||
|
|
||
| validate_squads_proposal( | ||
| &self.squads_multisig_proposal, | ||
| &self.squads_multisig, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,197 @@ | ||
| use super::*; | ||
|
|
||
| #[derive(Accounts)] | ||
| #[event_cpi] | ||
| pub struct ApplyLiquidation<'info> { | ||
| /// The linked liquidation proposal, baked into the payload at create. | ||
| #[account(has_one = dao)] | ||
| pub proposal: Box<Account<'info, Proposal>>, | ||
| #[account(mut, has_one = squads_multisig_vault)] | ||
| pub dao: Box<Account<'info, Dao>>, | ||
| /// The vault's signature is only obtainable through a Squads vault | ||
| /// transaction execution, so the caller is a passed proposal's payload. | ||
| pub squads_multisig_vault: Signer<'info>, | ||
| /// CHECK: the treasury's own LP position. The address is pinned by the | ||
| /// seeds, but whether the account exists at execution is unknowable at | ||
| /// create, so it is parsed manually — a passed liquidation must never | ||
| /// brick on treasury shape. | ||
| #[account( | ||
| mut, | ||
| seeds = [SEED_AMM_POSITION, dao.key().as_ref(), squads_multisig_vault.key().as_ref()], | ||
| bump, | ||
| )] | ||
| pub amm_position: UncheckedAccount<'info>, | ||
| #[account( | ||
| mut, | ||
| associated_token::mint = dao.base_mint, | ||
| associated_token::authority = dao, | ||
| )] | ||
| pub amm_base_vault: Account<'info, TokenAccount>, | ||
| #[account( | ||
| mut, | ||
| associated_token::mint = dao.quote_mint, | ||
| associated_token::authority = dao, | ||
| )] | ||
| pub amm_quote_vault: Account<'info, TokenAccount>, | ||
| #[account( | ||
| mut, | ||
| associated_token::mint = dao.base_mint, | ||
| associated_token::authority = squads_multisig_vault, | ||
| )] | ||
| pub vault_base_account: Account<'info, TokenAccount>, | ||
| #[account( | ||
| mut, | ||
| associated_token::mint = dao.quote_mint, | ||
| associated_token::authority = squads_multisig_vault, | ||
| )] | ||
| pub vault_quote_account: Account<'info, TokenAccount>, | ||
| pub token_program: Program<'info, Token>, | ||
| } | ||
|
|
||
| impl ApplyLiquidation<'_> { | ||
| pub fn validate(&self) -> Result<()> { | ||
| // Like every payload instruction that mutates the DAO, only lands in | ||
| // Spot — the sweep always computes against a whole spot pool. | ||
| require!( | ||
| matches!(self.dao.amm.state, PoolState::Spot { .. }), | ||
| FutarchyError::PoolNotInSpotState | ||
| ); | ||
|
|
||
| require!( | ||
| self.proposal.state == ProposalState::Passed, | ||
| FutarchyError::ProposalNotPassed | ||
| ); | ||
|
|
||
| // Execution is permissionless and a second passed liquidation can | ||
| // exist, so replay must be refused, not double-applied. | ||
| require!( | ||
| self.dao.liquidator.is_none(), | ||
| FutarchyError::AlreadyLiquidated | ||
| ); | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| pub fn handle(ctx: Context<Self>) -> Result<()> { | ||
| let Self { | ||
| proposal, | ||
| dao, | ||
| squads_multisig_vault: _, | ||
| amm_position, | ||
| amm_base_vault, | ||
| amm_quote_vault, | ||
| vault_base_account, | ||
| vault_quote_account, | ||
| token_program, | ||
| event_authority: _, | ||
| program: _, | ||
| } = ctx.accounts; | ||
|
|
||
| // The destructure is the kind check: the vault's signature alone is | ||
| // kind-blind, so without it an execute_arbitrary payload could invoke | ||
| // liquidation at a different duration/threshold. | ||
| let ProposalAction::HostileLiquidate { liquidator } = &proposal.action else { | ||
| return err!(FutarchyError::InvalidProposalKind); | ||
| }; | ||
| let liquidator = *liquidator; | ||
|
|
||
| // `Some` is the liquidated flag, and it is terminal. | ||
| dao.liquidator = Some(liquidator); | ||
|
|
||
| // Zero the record; the next sync removes the Squads-side limit, so | ||
| // the outgoing team's pull rights end. | ||
| dao.initial_spending_limit = None; | ||
| dao.spending_limit_dirty = true; | ||
|
|
||
| // Sweep the treasury's own AMM position pro-rata into the vault's | ||
| // token accounts. Third-party positions are untouched — they exit on | ||
| // their own schedule via withdraw_liquidity. A missing or empty | ||
| // position is skipped, never a failure. | ||
| let mut base_swept = 0u64; | ||
| let mut quote_swept = 0u64; | ||
|
|
||
| if !amm_position.data_is_empty() { | ||
| require_keys_eq!( | ||
| *amm_position.owner, | ||
| crate::ID, | ||
| anchor_lang::error::ErrorCode::AccountOwnedByWrongProgram | ||
| ); | ||
|
|
||
| let mut position: AmmPosition = { | ||
| let data = amm_position.try_borrow_data()?; | ||
| AmmPosition::try_deserialize(&mut &data[..])? | ||
| }; | ||
|
|
||
| if position.liquidity > 0 { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we not want to internally call the withdraw liquidity instruction? |
||
| let liquidity_to_sweep = position.liquidity; | ||
| let total_liquidity = dao.amm.total_liquidity; | ||
| require_gt!(total_liquidity, 0, FutarchyError::AssertFailed); | ||
|
|
||
| { | ||
| let PoolState::Spot { ref mut spot } = dao.amm.state else { | ||
| return err!(FutarchyError::PoolNotInSpotState); | ||
| }; | ||
|
|
||
| let (base_to_sweep, quote_to_sweep) = | ||
| spot.get_base_and_quote_withdrawable(liquidity_to_sweep, total_liquidity); | ||
| spot.base_reserves -= base_to_sweep; | ||
| spot.quote_reserves -= quote_to_sweep; | ||
|
|
||
| base_swept = base_to_sweep; | ||
| quote_swept = quote_to_sweep; | ||
| } | ||
|
|
||
| dao.amm.total_liquidity -= liquidity_to_sweep; | ||
|
|
||
| position.liquidity = 0; | ||
| { | ||
| let mut data = amm_position.try_borrow_mut_data()?; | ||
| let mut writer: &mut [u8] = &mut data; | ||
| position.try_serialize(&mut writer)?; | ||
| } | ||
|
Comment on lines
+146
to
+151
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Feels janky |
||
|
|
||
| let dao_creator = dao.dao_creator; | ||
| let nonce = dao.nonce.to_le_bytes(); | ||
| let signer_seeds = &[ | ||
| SEED_DAO, | ||
| dao_creator.as_ref(), | ||
| nonce.as_ref(), | ||
| &[dao.pda_bump], | ||
| ]; | ||
|
|
||
| for (amount_to_sweep, from, to) in [ | ||
| (base_swept, amm_base_vault, vault_base_account), | ||
| (quote_swept, amm_quote_vault, vault_quote_account), | ||
| ] { | ||
| token::transfer( | ||
| CpiContext::new_with_signer( | ||
| token_program.to_account_info(), | ||
| Transfer { | ||
| from: from.to_account_info(), | ||
| to: to.to_account_info(), | ||
| authority: dao.to_account_info(), | ||
| }, | ||
| &[&signer_seeds[..]], | ||
| ), | ||
| amount_to_sweep, | ||
| )?; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| dao.seq_num += 1; | ||
|
|
||
| let clock = Clock::get()?; | ||
| emit_cpi!(ApplyLiquidationEvent { | ||
| common: CommonFields::new(&clock, dao.seq_num), | ||
| dao: dao.key(), | ||
| proposal: proposal.key(), | ||
| liquidator, | ||
| base_swept, | ||
| quote_swept, | ||
| post_amm_state: dao.amm.clone(), | ||
| }); | ||
|
|
||
| Ok(()) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.