Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions programs/futarchy/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,6 @@ pub enum FutarchyError {
InvalidSpendingLimitMint,
#[msg("No active optimistic proposal")]
NoActiveOptimisticProposal,
#[msg("Address lookup tables referenced by the vault transaction must be frozen")]
UnfrozenAddressLookupTable,
}
22 changes: 19 additions & 3 deletions programs/futarchy/src/instructions/initialize_proposal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ pub struct InitializeProposal<'info> {
pub proposal: Box<Account<'info, Proposal>>,
pub squads_proposal: Box<Account<'info, squads_multisig_program::Proposal>>,
pub squads_multisig: Box<Account<'info, squads_multisig_program::Multisig>>,
#[account(
seeds = [
squads_multisig_program::SEED_PREFIX,
squads_multisig.key().as_ref(),
squads_multisig_program::SEED_TRANSACTION,
squads_proposal.transaction_index.to_le_bytes().as_ref(),
],
bump,
seeds::program = squads_multisig_program::ID,
)]
pub squads_vault_transaction: Box<Account<'info, squads_multisig_program::VaultTransaction>>,
#[account(mut, has_one = squads_multisig)]
pub dao: Box<Account<'info, Dao>>,
#[account(
Expand All @@ -35,8 +46,8 @@ pub struct InitializeProposal<'info> {
pub system_program: Program<'info, System>,
}

impl InitializeProposal<'_> {
pub fn validate(&self) -> Result<()> {
impl<'info, 'c: 'info> InitializeProposal<'info> {
pub fn validate(&self, remaining_accounts: &[AccountInfo<'info>]) -> Result<()> {
// If we're trying to challenge an optimistic proposal that has already passed due to age, we should error
// In the case of an already-optimistically-passed proposal, the optimistic proposal can be cleared
// from the DAO state by finalizing the optimistic proposal (finalize_optimistic_proposal)
Expand Down Expand Up @@ -72,20 +83,25 @@ impl InitializeProposal<'_> {
self.squads_multisig.stale_transaction_index
);

// Any address lookup table the vault transaction references must be frozen, so the
// addresses the market evaluates can't change between approval and execution
validate_address_lookup_tables(&self.squads_vault_transaction.message, remaining_accounts)?;

// Should never be the case because the oracle is the proposal account, and you can't re-initialize a proposal
assert!(!self.question.is_resolved());

Ok(())
}

pub fn handle(ctx: Context<Self>) -> Result<()> {
pub fn handle(ctx: Context<'_, '_, 'c, 'info, Self>) -> Result<()> {
let Self {
base_vault,
quote_vault,
question,
proposal,
squads_proposal,
squads_multisig: _,
squads_vault_transaction: _,
dao,
proposer,
payer: _,
Expand Down
22 changes: 19 additions & 3 deletions programs/futarchy/src/instructions/launch_proposal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,24 @@ pub struct LaunchProposal<'info> {
pub squads_multisig: Account<'info, squads_multisig_program::Multisig>,
#[account(owner = squads_multisig_program::ID)]
pub squads_proposal: Account<'info, squads_multisig_program::Proposal>,
#[account(
seeds = [
squads_multisig_program::SEED_PREFIX,
squads_multisig.key().as_ref(),
squads_multisig_program::SEED_TRANSACTION,
squads_proposal.transaction_index.to_le_bytes().as_ref(),
],
bump,
seeds::program = squads_multisig_program::ID,
)]
pub squads_vault_transaction: Box<Account<'info, squads_multisig_program::VaultTransaction>>,
pub system_program: Program<'info, System>,
pub token_program: Program<'info, Token>,
pub associated_token_program: Program<'info, AssociatedToken>,
}

impl LaunchProposal<'_> {
pub fn validate(&self) -> Result<()> {
impl<'info, 'c: 'info> LaunchProposal<'info> {
pub fn validate(&self, remaining_accounts: &[AccountInfo<'info>]) -> Result<()> {
msg!("proposal state: {:?}", self.proposal.state);
require!(
matches!(self.proposal.state, ProposalState::Draft { .. }),
Expand Down Expand Up @@ -91,10 +102,14 @@ impl LaunchProposal<'_> {
self.squads_multisig.stale_transaction_index
);

// Any address lookup table the vault transaction references must be frozen, so the
// addresses the market evaluates can't change between approval and execution.
validate_address_lookup_tables(&self.squads_vault_transaction.message, remaining_accounts)?;

Ok(())
}

pub fn handle(ctx: Context<Self>) -> Result<()> {
pub fn handle(ctx: Context<'_, '_, 'c, 'info, Self>) -> Result<()> {
let Self {
proposal,
dao,
Expand All @@ -114,6 +129,7 @@ impl LaunchProposal<'_> {
amm_fail_quote_vault: _,
squads_multisig: _,
squads_proposal: _,
squads_vault_transaction: _,
system_program: _,
token_program: _,
associated_token_program: _,
Expand Down
12 changes: 8 additions & 4 deletions programs/futarchy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,10 @@ pub mod futarchy {
InitializeDao::handle(ctx, params)
}

#[access_control(ctx.accounts.validate())]
pub fn initialize_proposal(ctx: Context<InitializeProposal>) -> Result<()> {
#[access_control(ctx.accounts.validate(ctx.remaining_accounts))]
pub fn initialize_proposal<'c: 'info, 'info>(
ctx: Context<'_, '_, 'c, 'info, InitializeProposal<'info>>,
) -> Result<()> {
InitializeProposal::handle(ctx)
}

Expand All @@ -90,8 +92,10 @@ pub mod futarchy {
UnstakeFromProposal::handle(ctx, params)
}

#[access_control(ctx.accounts.validate())]
pub fn launch_proposal(ctx: Context<LaunchProposal>) -> Result<()> {
#[access_control(ctx.accounts.validate(ctx.remaining_accounts))]
pub fn launch_proposal<'c: 'info, 'info>(
ctx: Context<'_, '_, 'c, 'info, LaunchProposal<'info>>,
) -> Result<()> {
LaunchProposal::handle(ctx)
}

Expand Down
60 changes: 60 additions & 0 deletions programs/futarchy/src/squads.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,69 @@
use anchor_lang::prelude::*;
use anchor_lang::solana_program::address_lookup_table::{self, state::AddressLookupTable};

use std::collections::BTreeMap;

use crate::FutarchyError;

/// Validates that every Address Lookup Table referenced by a vault transaction message is
/// frozen (`authority` permanently `None`, so its contents can never change) and that every
/// index the message references already exists in the table. `remaining_accounts` must hold
/// exactly one ALT account per `message.address_table_lookups` entry, in the same order —
/// the same convention Squads' own `vault_transaction_execute` uses.
pub fn validate_address_lookup_tables<'info>(
message: &squads_multisig_program::VaultTransactionMessage,
remaining_accounts: &[AccountInfo<'info>],
) -> Result<()> {
require_eq!(
remaining_accounts.len(),
message.address_table_lookups.len(),
FutarchyError::InvalidTransaction
);

for (lookup, alt_account_info) in message
.address_table_lookups
.iter()
.zip(remaining_accounts.iter())
{
require_keys_eq!(
*alt_account_info.key,
lookup.account_key,
FutarchyError::InvalidTransaction
);
require_keys_eq!(
*alt_account_info.owner,
address_lookup_table::program::ID,
FutarchyError::InvalidTransaction
);

let alt_data = alt_account_info.try_borrow_data()?;
let alt_state = AddressLookupTable::deserialize(&alt_data)
.map_err(|_| FutarchyError::InvalidTransaction)?;

require!(
alt_state.meta.authority.is_none(),
FutarchyError::UnfrozenAddressLookupTable
);

// A frozen table's length is final: an out-of-range index can never be filled,
// so the proposal could pass its market but never execute. Reject it upfront.
if let Some(max_index) = lookup
.writable_indexes
.iter()
.chain(lookup.readonly_indexes.iter())
.max()
{
require_gt!(
alt_state.addresses.len(),
*max_index as usize,
FutarchyError::InvalidTransaction
);
}
}

Ok(())
}

/// Compiles a Solana instruction into a Squads TransactionMessage format.
/// This is necessary because Solana's Message::serialize() uses a different header format
/// (num_readonly_signed_accounts, num_readonly_unsigned_accounts) than Squads expects
Expand Down
75 changes: 73 additions & 2 deletions sdk/src/futarchy/v0.6/FutarchyClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,18 +276,57 @@ export class FutarchyClient {
});
}

/**
* Fetches the Squads vault transaction linked to a Squads proposal and returns its
* address along with the address lookup table accounts its message references. The
* lookup tables must be passed as remaining accounts to `initializeProposal` and
* `launchProposal`, which verify each one is frozen.
*/
async getSquadsVaultTransactionAccounts(squadsProposal: PublicKey): Promise<{
squadsVaultTransaction: PublicKey;
lookupTableKeys: PublicKey[];
}> {
const squadsProposalAccount =
await multisig.accounts.Proposal.fromAccountAddress(
this.provider.connection,
squadsProposal,
);

const [squadsVaultTransaction] = multisig.getTransactionPda({
multisigPda: squadsProposalAccount.multisig,
index: BigInt(squadsProposalAccount.transactionIndex.toString()),
});

const vaultTransactionAccount =
await multisig.accounts.VaultTransaction.fromAccountAddress(
this.provider.connection,
squadsVaultTransaction,
);

const lookupTableKeys =
vaultTransactionAccount.message.addressTableLookups.map(
(lookup) => lookup.accountKey,
);

return { squadsVaultTransaction, lookupTableKeys };
}

launchProposalIx({
proposal,
dao,
baseMint,
quoteMint,
squadsProposal,
squadsVaultTransaction,
lookupTables = [],
}: {
proposal: PublicKey;
dao: PublicKey;
baseMint: PublicKey;
quoteMint: PublicKey;
squadsProposal: PublicKey;
squadsVaultTransaction: PublicKey;
lookupTables?: PublicKey[];
}) {
const {
baseVault,
Expand Down Expand Up @@ -333,8 +372,16 @@ export class FutarchyClient {
),
squadsMultisig,
squadsProposal,
squadsVaultTransaction,
payer: this.provider.publicKey,
})
.remainingAccounts(
lookupTables.map((pubkey) => ({
pubkey,
isSigner: false,
isWritable: false,
})),
)
.preInstructions([
ComputeBudgetProgram.setComputeUnitLimit({ units: 300_000 }),
]);
Expand Down Expand Up @@ -590,7 +637,11 @@ export class FutarchyClient {
instructions: TransactionInstruction[];
transactionIndex: bigint;
payer?: PublicKey;
}): { tx: Transaction; squadsProposal: PublicKey } {
}): {
tx: Transaction;
squadsProposal: PublicKey;
squadsVaultTransaction: PublicKey;
} {
const multisigPda = multisig.getMultisigPda({ createKey: dao })[0];

const transactionMessage = new TransactionMessage({
Expand Down Expand Up @@ -621,9 +672,14 @@ export class FutarchyClient {
transactionIndex: transactionIndex,
});

const [squadsVaultTransaction] = multisig.getTransactionPda({
multisigPda,
index: transactionIndex,
});

const tx = new Transaction().add(vaultTxCreate, proposalCreate);

return { tx, squadsProposal };
return { tx, squadsProposal, squadsVaultTransaction };
}

async initializeProposal(
Expand Down Expand Up @@ -657,12 +713,17 @@ export class FutarchyClient {
)
.rpc();

const { squadsVaultTransaction, lookupTableKeys } =
await this.getSquadsVaultTransactionAccounts(squadsProposal);

await this.initializeProposalIx(
squadsProposal,
dao,
storedDao.baseMint,
storedDao.quoteMint,
question,
squadsVaultTransaction,
lookupTableKeys,
)
.preInstructions([
ComputeBudgetProgram.setComputeUnitLimit({ units: 300_000 }),
Expand All @@ -678,6 +739,8 @@ export class FutarchyClient {
baseMint: PublicKey,
quoteMint: PublicKey,
question: PublicKey,
squadsVaultTransaction: PublicKey,
lookupTables: PublicKey[] = [],
proposer: PublicKey = this.provider.publicKey,
) {
let [proposal] = getProposalAddr(this.futarchy.programId, squadsProposal);
Expand All @@ -703,12 +766,20 @@ export class FutarchyClient {
question,
proposal,
squadsProposal,
squadsVaultTransaction,
dao,
baseVault,
quoteVault,
proposer,
squadsMultisig,
})
.remainingAccounts(
lookupTables.map((pubkey) => ({
pubkey,
isSigner: false,
isWritable: false,
})),
)
.preInstructions([
createAssociatedTokenAccountIdempotentInstruction(
this.provider.publicKey,
Expand Down
Loading
Loading