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
10 changes: 10 additions & 0 deletions programs/futarchy/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,14 @@ pub enum FutarchyError {
InvalidSpendingLimitMint,
#[msg("No active optimistic proposal")]
NoActiveOptimisticProposal,
#[msg("Invalid MetaDAO approver")]
InvalidApprover,
#[msg("Proposal has already been approved by MetaDAO")]
ProposalAlreadyApproved,
#[msg("base_to_supermajority must be 0 (disabled) or >= base_to_stake")]
InvalidSupermajorityThreshold,
#[msg("Proposal lacks enough approval points to launch")]
InsufficientApprovalToLaunch,
#[msg("Proposal validation is not enabled for this DAO")]
ProposalValidationDisabled,
}
12 changes: 12 additions & 0 deletions programs/futarchy/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ pub struct InitializeDaoEvent {
pub squads_multisig_vault: Pubkey,
pub team_sponsored_pass_threshold_bps: i16,
pub team_address: Pubkey,
pub base_to_supermajority: u64,
pub is_proposal_validation_enabled: bool,
}

#[event]
Expand All @@ -70,6 +72,8 @@ pub struct UpdateDaoEvent {
pub team_sponsored_pass_threshold_bps: i16,
pub team_address: Pubkey,
pub is_optimistic_governance_enabled: bool,
pub base_to_supermajority: u64,
pub is_proposal_validation_enabled: bool,
}

#[event]
Expand Down Expand Up @@ -192,6 +196,14 @@ pub struct SponsorProposalEvent {
pub team_address: Pubkey,
}

#[event]
pub struct ApproveProposalEvent {
pub common: CommonFields,
pub proposal: Pubkey,
pub dao: Pubkey,
pub approver: Pubkey,
}

#[event]
pub struct RemoveProposalEvent {
pub common: CommonFields,
Expand Down
70 changes: 70 additions & 0 deletions programs/futarchy/src/instructions/approve_proposal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use super::*;

mod metadao_approver {
use anchor_lang::prelude::declare_id;
declare_id!("6awyHMshBGVjJ3ozdSJdyyDE1CTAXUwrpNMaRGMsb4sf");

Check warning on line 5 in programs/futarchy/src/instructions/approve_proposal.rs

View workflow job for this annotation

GitHub Actions / repository-guard

declare_id! literal change; Hardcoded Solana address literal: + declare_id!("6awyHMshBGVjJ3ozdSJdyyDE1CTAXUwrpNMaRGMsb4sf");
}

#[derive(Accounts)]
#[event_cpi]
pub struct ApproveProposal<'info> {
#[account(mut, has_one = dao)]
pub proposal: Box<Account<'info, Proposal>>,
#[account(mut)]
pub dao: Box<Account<'info, Dao>>,
pub approver: Signer<'info>,
}

impl ApproveProposal<'_> {
pub fn validate(&self) -> Result<()> {
require!(
self.dao.is_proposal_validation_enabled,
FutarchyError::ProposalValidationDisabled
);

#[cfg(feature = "production")]
require_keys_eq!(
self.approver.key(),
metadao_approver::ID,
FutarchyError::InvalidApprover
);

require!(
matches!(self.proposal.state, ProposalState::Draft { .. }),
FutarchyError::ProposalNotInDraftState
);

require_neq!(
self.proposal.is_metadao_approved,
true,
FutarchyError::ProposalAlreadyApproved
);

Ok(())
}

pub fn handle(ctx: Context<Self>) -> Result<()> {
let Self {
proposal,
dao,
approver,
event_authority: _,
program: _,
} = ctx.accounts;

proposal.is_metadao_approved = true;

dao.seq_num += 1;

let clock = Clock::get()?;

emit_cpi!(ApproveProposalEvent {
common: CommonFields::new(&clock, dao.seq_num),
proposal: proposal.key(),
dao: dao.key(),
approver: approver.key(),
});

Ok(())
}
}
8 changes: 8 additions & 0 deletions programs/futarchy/src/instructions/initialize_dao.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ pub struct InitializeDaoParams {
pub initial_spending_limit: Option<InitialSpendingLimit>,
pub team_sponsored_pass_threshold_bps: i16,
pub team_address: Pubkey,
pub base_to_supermajority: u64,
pub is_proposal_validation_enabled: bool,
}

#[derive(Accounts)]
Expand Down Expand Up @@ -93,6 +95,8 @@ impl InitializeDao<'_> {
initial_spending_limit,
team_sponsored_pass_threshold_bps,
team_address,
base_to_supermajority,
is_proposal_validation_enabled,
} = params;

let dao = &mut ctx.accounts.dao;
Expand Down Expand Up @@ -223,6 +227,8 @@ impl InitializeDao<'_> {
team_address,
optimistic_proposal: None,
is_optimistic_governance_enabled: false,
base_to_supermajority,
is_proposal_validation_enabled,
});

dao.invariant()?;
Expand All @@ -248,6 +254,8 @@ 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,
base_to_supermajority: dao.base_to_supermajority,
is_proposal_validation_enabled: dao.is_proposal_validation_enabled,
});

Ok(())
Expand Down
1 change: 1 addition & 0 deletions programs/futarchy/src/instructions/initialize_proposal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ impl InitializeProposal<'_> {
pass_quote_mint: quote_vault.conditional_token_mints[1],
fail_quote_mint: quote_vault.conditional_token_mints[0],
is_team_sponsored: false,
is_metadao_approved: false,
});

dao.seq_num += 1;
Expand Down
47 changes: 38 additions & 9 deletions programs/futarchy/src/instructions/launch_proposal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,44 @@ impl LaunchProposal<'_> {

require_keys_eq!(self.proposal.dao, self.dao.key());

// If the proposal is not team sponsored, check if sufficient stake has been accumulated
if !self.proposal.is_team_sponsored {
if let ProposalState::Draft { amount_staked } = self.proposal.state {
require_gte!(
amount_staked,
self.dao.base_to_stake,
FutarchyError::InsufficientStakeToLaunch
);
}
let amount_staked = match self.proposal.state {
ProposalState::Draft { amount_staked } => amount_staked,
_ => unreachable!(), // Draft already asserted above
};

if self.dao.is_proposal_validation_enabled {
// A proposal that challenges an active optimistic proposal is team-authored by design:
// the team initiated *this exact* squads_proposal optimistically (the match is also enforced
// below in validate), so it carries the team point — mirroring `handle`, which already flips
// `is_team_sponsored = true` for the pass threshold.
let is_optimistic_challenge = matches!(
&self.dao.optimistic_proposal,
Some(op) if op.squads_proposal == self.proposal.squads_proposal
);

// Approval points: launch needs >= 2 of 3 {token-holder, team, MetaDAO}
let points = [
amount_staked >= self.dao.base_to_stake, // token-holder point
self.proposal.is_team_sponsored || is_optimistic_challenge, // team point (auto for optimistic challenges)
self.proposal.is_metadao_approved, // MetaDAO point
]
.into_iter()
.filter(|&p| p)
.count();

// Supermajority: stake alone reaches the per-DAO bar (0 disables this path)
let supermajority_reached = self.dao.base_to_supermajority > 0
&& amount_staked >= self.dao.base_to_supermajority;

require!(
points >= LAUNCH_APPROVAL_POINTS_REQUIRED || supermajority_reached,
FutarchyError::InsufficientApprovalToLaunch
);
} else {
require!(
self.proposal.is_team_sponsored || amount_staked >= self.dao.base_to_stake,
FutarchyError::InsufficientStakeToLaunch
);
}

// If there is an active optimistic proposal, it must be for the same squads proposal
Expand Down
4 changes: 4 additions & 0 deletions programs/futarchy/src/instructions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub mod admin_cancel_proposal;
pub mod admin_enqueue_multisig_proposal_approval;
pub mod admin_execute_multisig_proposal;
pub mod admin_remove_proposal;
pub mod approve_proposal;
pub mod collect_fees;
pub mod collect_meteora_damm_fees;
pub mod conditional_swap;
Expand All @@ -17,6 +18,7 @@ pub mod initiate_vault_spend_optimistic_proposal;
pub mod launch_proposal;
pub mod provide_liquidity;
pub mod resize_dao;
pub mod resize_proposal;
pub mod sponsor_proposal;
pub mod spot_swap;
pub mod stake_to_proposal;
Expand All @@ -28,6 +30,7 @@ pub use admin_cancel_proposal::*;
pub use admin_enqueue_multisig_proposal_approval::*;
pub use admin_execute_multisig_proposal::*;
pub use admin_remove_proposal::*;
pub use approve_proposal::*;
pub use collect_fees::*;
pub use collect_meteora_damm_fees::*;
pub use conditional_swap::*;
Expand All @@ -41,6 +44,7 @@ pub use initiate_vault_spend_optimistic_proposal::*;
pub use launch_proposal::*;
pub use provide_liquidity::*;
pub use resize_dao::*;
pub use resize_proposal::*;
pub use sponsor_proposal::*;
pub use spot_swap::*;
pub use stake_to_proposal::*;
Expand Down
13 changes: 9 additions & 4 deletions programs/futarchy/src/instructions/resize_dao.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ impl ResizeDao<'_> {
require_eq!(is_discriminator_correct, true);

const AFTER_REALLOC_SIZE: usize = Dao::INIT_SPACE + 8;
// 42 bytes: 1 (Option discriminant) + 32 (Pubkey) + 8 (i64) + 1 (bool)
const BEFORE_REALLOC_SIZE: usize = AFTER_REALLOC_SIZE - 42;
// 9 bytes: base_to_supermajority (u64) + is_proposal_validation_enabled (bool)
const BEFORE_REALLOC_SIZE: usize = AFTER_REALLOC_SIZE - 9;

if dao.data_len() != BEFORE_REALLOC_SIZE {
// already realloced
Expand All @@ -32,6 +32,9 @@ impl ResizeDao<'_> {

let old_dao_data = OldDao::deserialize(&mut &dao.try_borrow_data().unwrap()[8..])?;

// Opt-in defaults: existing DAOs migrate in with the validation gate OFF and the
// supermajority path disabled, so their launch behavior is unchanged until they
// explicitly opt in via update_dao.
let new_dao_data = Dao {
amm: old_dao_data.amm,
nonce: old_dao_data.nonce,
Expand All @@ -55,8 +58,10 @@ impl ResizeDao<'_> {
initial_spending_limit: old_dao_data.initial_spending_limit,
team_sponsored_pass_threshold_bps: old_dao_data.team_sponsored_pass_threshold_bps,
team_address: old_dao_data.team_address,
optimistic_proposal: None,
is_optimistic_governance_enabled: false,
optimistic_proposal: old_dao_data.optimistic_proposal,
is_optimistic_governance_enabled: old_dao_data.is_optimistic_governance_enabled,
base_to_supermajority: 0,
is_proposal_validation_enabled: false,
};

dao.realloc(AFTER_REALLOC_SIZE, true)?;
Expand Down
78 changes: 78 additions & 0 deletions programs/futarchy/src/instructions/resize_proposal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
use anchor_lang::{system_program, Discriminator};

use super::*;

#[derive(Accounts)]
pub struct ResizeProposal<'info> {
/// CHECK: we check the discriminator
#[account(mut)]
pub proposal: UncheckedAccount<'info>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
}

impl ResizeProposal<'_> {
pub fn handle(ctx: Context<Self>) -> Result<()> {
let proposal = &ctx.accounts.proposal;

require_eq!(proposal.owner, &crate::ID);
let is_discriminator_correct =
proposal.try_borrow_data().unwrap()[..8] == Proposal::discriminator();
require_eq!(is_discriminator_correct, true);

const AFTER_REALLOC_SIZE: usize = Proposal::INIT_SPACE + 8;
// 1 byte: is_metadao_approved (bool)
const BEFORE_REALLOC_SIZE: usize = AFTER_REALLOC_SIZE - 1;

if proposal.data_len() != BEFORE_REALLOC_SIZE {
// already realloced
require_eq!(proposal.data_len(), AFTER_REALLOC_SIZE);
return Ok(());
}

let old_proposal_data =
OldProposal::deserialize(&mut &proposal.try_borrow_data().unwrap()[8..])?;

let new_proposal_data = Proposal {
number: old_proposal_data.number,
proposer: old_proposal_data.proposer,
timestamp_enqueued: old_proposal_data.timestamp_enqueued,
state: old_proposal_data.state,
base_vault: old_proposal_data.base_vault,
quote_vault: old_proposal_data.quote_vault,
dao: old_proposal_data.dao,
pda_bump: old_proposal_data.pda_bump,
question: old_proposal_data.question,
duration_in_seconds: old_proposal_data.duration_in_seconds,
squads_proposal: old_proposal_data.squads_proposal,
pass_base_mint: old_proposal_data.pass_base_mint,
pass_quote_mint: old_proposal_data.pass_quote_mint,
fail_base_mint: old_proposal_data.fail_base_mint,
fail_quote_mint: old_proposal_data.fail_quote_mint,
is_team_sponsored: old_proposal_data.is_team_sponsored,
is_metadao_approved: false,
};

proposal.realloc(AFTER_REALLOC_SIZE, true)?;

let lamports_needed = Rent::get()?.minimum_balance(AFTER_REALLOC_SIZE);

if lamports_needed > proposal.lamports() {
system_program::transfer(
CpiContext::new(
ctx.accounts.system_program.to_account_info(),
system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: proposal.to_account_info(),
},
),
lamports_needed - proposal.lamports(),
)?;
}

new_proposal_data.serialize(&mut &mut proposal.try_borrow_mut_data().unwrap()[8..])?;

Ok(())
}
}
10 changes: 10 additions & 0 deletions programs/futarchy/src/instructions/update_dao.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ pub struct UpdateDaoParams {
pub team_sponsored_pass_threshold_bps: Option<i16>,
pub team_address: Option<Pubkey>,
pub is_optimistic_governance_enabled: Option<bool>,
pub base_to_supermajority: Option<u64>,
pub is_proposal_validation_enabled: Option<bool>,
}

#[derive(Accounts)]
Expand Down Expand Up @@ -83,6 +85,12 @@ impl UpdateDao<'_> {
is_optimistic_governance_enabled: dao_params
.is_optimistic_governance_enabled
.unwrap_or(dao.is_optimistic_governance_enabled),
base_to_supermajority: dao_params
.base_to_supermajority
.unwrap_or(dao.base_to_supermajority),
is_proposal_validation_enabled: dao_params
.is_proposal_validation_enabled
.unwrap_or(dao.is_proposal_validation_enabled),
});

dao.seq_num += 1;
Expand All @@ -104,6 +112,8 @@ impl UpdateDao<'_> {
team_sponsored_pass_threshold_bps: dao.team_sponsored_pass_threshold_bps,
team_address: dao.team_address,
is_optimistic_governance_enabled: dao.is_optimistic_governance_enabled,
base_to_supermajority: dao.base_to_supermajority,
is_proposal_validation_enabled: dao.is_proposal_validation_enabled,
});

Ok(())
Expand Down
Loading
Loading