Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
10 changes: 10 additions & 0 deletions programs/futarchy/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,4 +152,14 @@ pub enum FutarchyError {
TeamSponsorshipForbidden,
#[msg("Squads proposal must be in Approved status to be cancelled")]
SquadsProposalNotApproved,
#[msg("This DAO has not opted into typed proposals")]
TypedProposalsDisabled,
#[msg("Typed proposals cannot be disabled")]
TypedProposalsCannotBeDisabled,
#[msg("Address lookup tables referenced by the vault transaction must be frozen")]
UnfrozenAddressLookupTable,
#[msg("Lookup table accounts must match the vault transaction's address table lookups")]
InvalidAddressLookupTable,
#[msg("Expected the proposal's Squads vault transaction as the first launch account")]
InvalidSquadsVaultTransaction,
}
5 changes: 5 additions & 0 deletions programs/futarchy/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ pub struct InitializeDaoEvent {
pub squads_multisig_vault: Pubkey,
pub team_sponsored_pass_threshold_bps: i16,
pub team_address: Pubkey,
pub typed_proposals_enabled: bool,
}

#[event]
Expand All @@ -70,6 +71,7 @@ pub struct UpdateDaoEvent {
pub team_sponsored_pass_threshold_bps: i16,
pub team_address: Pubkey,
pub is_optimistic_governance_enabled: bool,
pub typed_proposals_enabled: bool,
}

#[event]
Expand Down Expand Up @@ -116,6 +118,9 @@ pub struct LaunchProposalEvent {
pub timestamp_enqueued: i64,
pub total_staked: u64,
pub post_amm_state: FutarchyAmm,
/// The terms the market opened with, as written by launch.
pub duration_in_seconds: u32,
pub pass_threshold_bps: i16,
}

#[event]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ impl AdminUpdateProposalParams<'_> {
// The same comparison `launch_proposal` makes
require_gt!(
duration_in_seconds,
self.proposal.action.params().twap_start_delay_seconds,
self.proposal
.action
.params_for(&self.dao, false)
.twap_start_delay_seconds,
FutarchyError::ProposalDurationTooShort
);
}
Expand Down Expand Up @@ -91,6 +94,11 @@ impl AdminUpdateProposalParams<'_> {
proposal.pass_threshold_bps = pass_threshold_bps;
}

// `launch_proposal` keeps these values instead of writing its own.
Comment thread
greptile-apps[bot] marked this conversation as resolved.
// It is assumed that a change to the proposal params applies to both,
// even when only one is changed.
proposal.params_overridden = true;

dao.seq_num += 1;
let clock = Clock::get()?;

Expand Down
7 changes: 2 additions & 5 deletions programs/futarchy/src/instructions/finalize_proposal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,11 +185,8 @@ impl FinalizeProposal<'_> {
dao.liquidator = Some(*liquidator);

// The spending limit must be zeroed so that the estate can be swept.
// Otherwise a still-live limit member could drain the estate.
if dao.initial_spending_limit.is_some() {
dao.initial_spending_limit = None;
dao.spending_limit_dirty = true;
}
dao.initial_spending_limit = None;
dao.spending_limit_dirty = true;
}
}

Expand Down
2 changes: 2 additions & 0 deletions programs/futarchy/src/instructions/initialize_dao.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ impl InitializeDao<'_> {
last_failed_liquidation_at: 0,
spending_limit_dirty: false,
last_buyback_finalized_at: 0,
typed_proposals_enabled: true,
});

dao.invariant()?;
Expand All @@ -250,6 +251,7 @@ impl InitializeDao<'_> {
squads_multisig_vault: dao.squads_multisig_vault,
team_sponsored_pass_threshold_bps: dao.team_sponsored_pass_threshold_bps,
team_address: dao.team_address,
typed_proposals_enabled: dao.typed_proposals_enabled,
});

Ok(())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ pub struct InitializeHostileTakeoverProposal<'info> {

impl InitializeHostileTakeoverProposal<'_> {
pub fn validate(&self, args: &InitializeHostileTakeoverProposalArgs) -> Result<()> {
// Hostile takeovers are switched off in production for now.
if cfg!(feature = "production") {
return err!(FutarchyError::InvalidProposalKind);
}

self.typed_initialize_accounts.validate()?;

require_keys_neq!(
Expand Down Expand Up @@ -59,6 +64,7 @@ impl InitializeHostileTakeoverProposal<'_> {
base_to_stake: None,
team_sponsored_pass_threshold_bps: None,
team_address: Some(args.new_team_address),
typed_proposals_enabled: None,
},
}
.data(),
Expand Down
24 changes: 21 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> InitializeProposal<'info> {
pub fn validate(&self, remaining_accounts: &'info [AccountInfo<'info>]) -> Result<()> {
require!(self.dao.liquidator.is_none(), FutarchyError::DaoLiquidated);

require_eq!(
Expand All @@ -61,6 +72,10 @@ impl InitializeProposal<'_> {
self.squads_multisig.stale_transaction_index
);

// Every lookup table the payload resolves through must be frozen, so
// the accounts the market prices are the ones that execute.
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());

Expand All @@ -75,6 +90,7 @@ impl InitializeProposal<'_> {
proposal,
squads_proposal,
squads_multisig: _,
squads_vault_transaction: _,
dao,
proposer,
payer: _,
Expand All @@ -88,7 +104,8 @@ impl InitializeProposal<'_> {
dao.proposal_count += 1;

let action = ProposalAction::ExecuteArbitrary;
let params = action.params();
// A preview: launch writes the terms from the configuration in force later.
let params = action.params_for(dao, false);

proposal.set_inner(Proposal {
number: dao.proposal_count,
Expand All @@ -110,6 +127,7 @@ impl InitializeProposal<'_> {
pass_threshold_bps: params.pass_threshold_bps,
council_can_block: params.council_can_block,
action,
params_overridden: false,
});

dao.seq_num += 1;
Expand Down
33 changes: 24 additions & 9 deletions programs/futarchy/src/instructions/launch_proposal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,16 @@ impl<'info> LaunchProposal<'info> {

// Kind gates, checked at launch rather than create so that pre-created
// drafts can't bypass them
let params = self.proposal.action.params();

// Typed kinds need the DAO typed proposals enabled.
if !matches!(self.proposal.action, ProposalAction::ExecuteArbitrary) {
require!(
self.dao.typed_proposals_enabled,
FutarchyError::TypedProposalsDisabled
);
}

let params = self.proposal.launch_params(&self.dao);

if params.team_sponsorship_policy == TeamSponsorshipPolicy::Required {
require!(is_team_sponsored, FutarchyError::ProposalNotTeamSponsored);
Expand All @@ -74,7 +83,7 @@ impl<'info> LaunchProposal<'info> {
// with an empty aggregator, and `MarketsTooYoung` blocks finalize.
// Strict, because finalize needs the last update past that boundary.
require_gt!(
self.proposal.duration_in_seconds,
params.duration_seconds,
params.twap_start_delay_seconds,
FutarchyError::ProposalDurationTooShort
);
Expand Down Expand Up @@ -109,9 +118,11 @@ impl<'info> LaunchProposal<'info> {
self.squads_multisig.stale_transaction_index
);

self.proposal
.action
.verify_launch_accounts(&self.dao, remaining_accounts)?;
self.proposal.action.verify_launch_accounts(
&self.dao,
self.squads_proposal.transaction_index,
remaining_accounts,
)?;

Ok(())
}
Expand Down Expand Up @@ -176,8 +187,10 @@ impl<'info> LaunchProposal<'info> {

let clock = Clock::get()?;

// Per-kind, not per-DAO: `dao.twap_start_delay_seconds` is vestigial.
let twap_start_delay_seconds = proposal.action.params().twap_start_delay_seconds;
// Write the terms in force now; the draft only carried a preview.
let params = proposal.launch_params(dao);
proposal.duration_in_seconds = params.duration_seconds;
proposal.pass_threshold_bps = params.pass_threshold_bps;

dao.amm.state = PoolState::Futarchy {
spot,
Expand All @@ -190,7 +203,7 @@ impl<'info> LaunchProposal<'info> {
clock.unix_timestamp,
dao.twap_initial_observation,
dao.twap_max_observation_change_per_update,
twap_start_delay_seconds,
params.twap_start_delay_seconds,
),
},
fail: Pool {
Expand All @@ -202,7 +215,7 @@ impl<'info> LaunchProposal<'info> {
clock.unix_timestamp,
dao.twap_initial_observation,
dao.twap_max_observation_change_per_update,
twap_start_delay_seconds,
params.twap_start_delay_seconds,
),
},
};
Expand All @@ -220,6 +233,8 @@ impl<'info> LaunchProposal<'info> {
timestamp_enqueued: proposal.timestamp_enqueued,
total_staked,
post_amm_state: dao.amm.clone(),
duration_in_seconds: proposal.duration_in_seconds,
pass_threshold_bps: proposal.pass_threshold_bps,
});

Ok(())
Expand Down
11 changes: 7 additions & 4 deletions programs/futarchy/src/instructions/resize_dao.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ impl ResizeDao<'_> {
require_eq!(is_discriminator_correct, true);

const AFTER_REALLOC_SIZE: usize = Dao::MIGRATED_SIZE;
// 58 bytes: 33 (Option<Pubkey> liquidator) + 8 (i64) + 8 (i64) + 1 (bool) + 8 (i64)
const BEFORE_REALLOC_SIZE: usize = AFTER_REALLOC_SIZE - 58;
// 59 bytes: 33 (Option<Pubkey> liquidator) + 8 (i64) + 8 (i64) + 1 (bool)
// + 8 (i64) + 1 (bool typed_proposals_enabled)
const BEFORE_REALLOC_SIZE: usize = AFTER_REALLOC_SIZE - 59;

if dao.data_len() != BEFORE_REALLOC_SIZE {
// already realloced
Expand Down Expand Up @@ -75,8 +76,9 @@ impl ResizeDao<'_> {
twap_max_observation_change_per_update: old_dao_data
.twap_max_observation_change_per_update,
twap_start_delay_seconds: old_dao_data.twap_start_delay_seconds,
min_quote_futarchic_liquidity: old_dao_data.min_quote_futarchic_liquidity,
min_base_futarchic_liquidity: old_dao_data.min_base_futarchic_liquidity,
// A zero minimum fails `Dao::invariant`; 1 is the launchpads' value.
min_quote_futarchic_liquidity: old_dao_data.min_quote_futarchic_liquidity.max(1),
min_base_futarchic_liquidity: old_dao_data.min_base_futarchic_liquidity.max(1),
base_to_stake: old_dao_data.base_to_stake,
seq_num: old_dao_data.seq_num,
initial_spending_limit: live_spending_limit,
Expand All @@ -92,6 +94,7 @@ impl ResizeDao<'_> {
last_failed_liquidation_at: 0,
spending_limit_dirty: false,
last_buyback_finalized_at: 0,
typed_proposals_enabled: false,
};

dao.realloc(AFTER_REALLOC_SIZE, true)?;
Expand Down
12 changes: 6 additions & 6 deletions programs/futarchy/src/instructions/resize_proposal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ impl ResizeProposal<'_> {
require_eq!(is_discriminator_correct, true);

const AFTER_REALLOC_SIZE: usize = Proposal::MIGRATED_SIZE;
// 401 bytes: 32 (Option<Pubkey> sponsored_by replacing the bool)
// 402 bytes: 32 (Option<Pubkey> sponsored_by replacing the bool)
// + 2 (i16 pass_threshold_bps) + 1 (bool council_can_block)
// + 366 (ProposalAction)
const BEFORE_REALLOC_SIZE: usize = AFTER_REALLOC_SIZE - 401;
// + 366 (ProposalAction) + 1 (bool params_overridden)
const BEFORE_REALLOC_SIZE: usize = AFTER_REALLOC_SIZE - 402;

if proposal.data_len() != BEFORE_REALLOC_SIZE {
// already realloced
Expand All @@ -44,11 +44,10 @@ impl ResizeProposal<'_> {

let action = ProposalAction::ExecuteArbitrary;

// Draft proposals take the kind's catalog params like any new proposal.
// Launched proposals keep the rules they were launched under.
// Drafts preview what launch would write today; launched proposals keep their rules.
let (pass_threshold_bps, duration_in_seconds) =
if matches!(old_proposal_data.state, ProposalState::Draft { .. }) {
let params = action.params();
let params = action.params_for(dao, old_proposal_data.is_team_sponsored);
(params.pass_threshold_bps, params.duration_seconds)
} else {
let pass_threshold_bps = if old_proposal_data.is_team_sponsored {
Expand Down Expand Up @@ -87,6 +86,7 @@ impl ResizeProposal<'_> {
pass_threshold_bps,
council_can_block: true,
action,
params_overridden: false,
};

proposal.realloc(AFTER_REALLOC_SIZE, true)?;
Expand Down
7 changes: 7 additions & 0 deletions programs/futarchy/src/instructions/typed_initialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ impl TypedInitializeAccounts<'_> {
pub fn validate(&self) -> Result<()> {
require!(self.dao.liquidator.is_none(), FutarchyError::DaoLiquidated);

// The catalog is opt-in per DAO.
require!(
self.dao.typed_proposals_enabled,
FutarchyError::TypedProposalsDisabled
);

require_eq!(
self.question.num_outcomes(),
2,
Expand Down Expand Up @@ -150,6 +156,7 @@ impl TypedInitializeAccounts<'_> {
pass_threshold_bps: params.pass_threshold_bps,
council_can_block: params.council_can_block,
action,
params_overridden: false,
};
self.proposal.set_inner(proposal);

Expand Down
Loading