Skip to content

Commit

Permalink
Veto before proposal passes feature (early veto)
Browse files Browse the repository at this point in the history
Allows for the vetoer to veto a proposal before it passes is
veto_before_passed is set to true. Some DAOs may want this to be able to
speed up their process.
  • Loading branch information
JakeHartnell committed Nov 6, 2023
1 parent bf5c85b commit 25031f1
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 5 deletions.
64 changes: 62 additions & 2 deletions contracts/proposal/dao-proposal-single/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,63 @@ pub fn execute_veto(
.may_load(deps.storage, proposal_id)?
.ok_or(ContractError::NoSuchProposal { id: proposal_id })?;

// TODO refactor for brevity and better code reuse
match prop.status {
Status::Open => {
match prop.timelock {
Some(ref timelock) => {
// Check sender is vetoer
timelock.check_is_vetoer(&info)?;

// Veto prop only if veto_before_passed is true
timelock.check_veto_before_passed_enabled()?;

let old_status = prop.status;

// Update proposal status to vetoed
prop.status = Status::Vetoed;
PROPOSALS.save(deps.storage, proposal_id, &prop)?;

let hooks = proposal_status_changed_hooks(
PROPOSAL_HOOKS,
deps.storage,
proposal_id,
old_status.to_string(),
prop.status.to_string(),
)?;

// TODO refactor into proposal_creation_policy_hooks?
// Add prepropose / deposit module hook which will handle deposit refunds.
let proposal_creation_policy = CREATION_POLICY.load(deps.storage)?;
let hooks = match proposal_creation_policy {
ProposalCreationPolicy::Anyone {} => hooks,
ProposalCreationPolicy::Module { addr } => {
let msg = to_binary(&PreProposeHookMsg::ProposalCompletedHook {
proposal_id,
new_status: prop.status,
})?;
let mut hooks = hooks;
hooks.push(SubMsg::reply_on_error(
WasmMsg::Execute {
contract_addr: addr.into_string(),
msg,
funds: vec![],
},
failed_pre_propose_module_hook_id(),
));
hooks
}
};

Ok(Response::new()
.add_attribute("action", "veto")
.add_attribute("proposal_id", proposal_id.to_string())
.add_submessages(hooks))
}
// If timelock is not configured throw error. This should never happen.
None => Err(ContractError::TimelockError(TimelockError::NoTimelock {})),
}
}
Status::Timelocked { expires } => {
match prop.timelock {
Some(ref timelock) => {
Expand Down Expand Up @@ -324,8 +380,12 @@ pub fn execute_veto(
None => Err(ContractError::TimelockError(TimelockError::NoTimelock {})),
}
}
// Error if the proposal hasn't passed
_ => Err(ContractError::NotPassed {}),
// Error if the proposal has any other status
_ => Err(ContractError::TimelockError(
TimelockError::InvalidProposalStatus {
status: prop.status.to_string(),
},
)),
}
}

Expand Down
2 changes: 1 addition & 1 deletion contracts/proposal/dao-proposal-single/src/proposal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ pub struct SingleChoiceProposal {
/// Whether or not revoting is enabled. If revoting is enabled, a proposal
/// cannot pass until the voting period has elapsed.
pub allow_revoting: bool,
/// Timelock info, if configured
/// Timelock info, if configured enables veto
pub timelock: Option<Timelock>,
}

Expand Down
89 changes: 87 additions & 2 deletions contracts/proposal/dao-proposal-single/src/testing/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@ fn test_proposal_message_timelock_execution() {
delay: Timestamp::from_seconds(100),
vetoer: "oversight".to_string(),
early_execute: false,
veto_before_passed: false,
});
let core_addr = instantiate_with_staked_balances_governance(
&mut app,
Expand Down Expand Up @@ -511,6 +512,7 @@ fn test_proposal_message_timelock_veto() {
delay: Timestamp::from_seconds(100),
vetoer: "oversight".to_string(),
early_execute: false,
veto_before_passed: false,
});
let core_addr = instantiate_with_staked_balances_governance(
&mut app,
Expand Down Expand Up @@ -562,7 +564,10 @@ fn test_proposal_message_timelock_veto() {
.unwrap_err()
.downcast()
.unwrap();
assert_eq!(err, ContractError::NotPassed {});
assert_eq!(
err,
ContractError::TimelockError(TimelockError::NoVetoBeforePassed {})
);

// Vote on proposal to pass it
vote_on_proposal(
Expand Down Expand Up @@ -622,6 +627,7 @@ fn test_proposal_message_timelock_early_execution() {
delay: Timestamp::from_seconds(100),
vetoer: "oversight".to_string(),
early_execute: true,
veto_before_passed: false,
});
let core_addr = instantiate_with_staked_balances_governance(
&mut app,
Expand Down Expand Up @@ -693,6 +699,83 @@ fn test_proposal_message_timelock_early_execution() {
assert_eq!(proposal.proposal.status, Status::Executed);
}

#[test]
fn test_proposal_message_timelock_veto_before_passed() {
let mut app = App::default();
let mut instantiate = get_default_token_dao_proposal_module_instantiate(&mut app);
instantiate.close_proposal_on_execution_failure = false;
instantiate.timelock = Some(Timelock {
delay: Timestamp::from_seconds(100),
vetoer: "oversight".to_string(),
early_execute: false,
veto_before_passed: true,
});
let core_addr = instantiate_with_staked_balances_governance(
&mut app,
instantiate,
Some(vec![
Cw20Coin {
address: "oversight".to_string(),
amount: Uint128::new(15),
},
Cw20Coin {
address: CREATOR_ADDR.to_string(),
amount: Uint128::new(85),
},
]),
);
let proposal_module = query_single_proposal_module(&app, &core_addr);
let gov_token = query_dao_token(&app, &core_addr);

mint_cw20s(&mut app, &gov_token, &core_addr, CREATOR_ADDR, 10_000_000);
let proposal_id = make_proposal(
&mut app,
&proposal_module,
CREATOR_ADDR,
vec![
WasmMsg::Execute {
contract_addr: gov_token.to_string(),
msg: to_binary(&cw20::Cw20ExecuteMsg::Mint {
recipient: CREATOR_ADDR.to_string(),
amount: Uint128::new(10_000_000),
})
.unwrap(),
funds: vec![],
}
.into(),
BankMsg::Send {
to_address: CREATOR_ADDR.to_string(),
amount: coins(10, "ujuno"),
}
.into(),
],
);

let proposal = query_proposal(&app, &proposal_module, proposal_id);

// Proposal is open for voting
assert_eq!(proposal.proposal.status, Status::Open);

// Oversite vetos prop
app.execute_contract(
Addr::unchecked("oversight"),
proposal_module.clone(),
&ExecuteMsg::Veto { proposal_id },
&[],
)
.unwrap();

let proposal = query_proposal(&app, &proposal_module, proposal_id);
assert_eq!(proposal.proposal.status, Status::Vetoed);

// mint_natives(&mut app, core_addr.as_str(), coins(10, "ujuno"));

// // Proposal can be executed early by vetoer
// execute_proposal(&mut app, &proposal_module, "oversight", proposal_id);
// let proposal = query_proposal(&app, &proposal_module, proposal_id);
// assert_eq!(proposal.proposal.status, Status::Executed);
}

#[test]
fn test_proposal_close_after_expiry() {
let CommonTest {
Expand Down Expand Up @@ -897,6 +980,7 @@ fn test_update_config() {
delay: Timestamp::from_seconds(100),
vetoer: CREATOR_ADDR.to_string(),
early_execute: false,
veto_before_passed: false,
}),
threshold: Threshold::AbsoluteCount {
threshold: Uint128::new(10_000),
Expand Down Expand Up @@ -929,7 +1013,8 @@ fn test_update_config() {
timelock: Some(Timelock {
delay: Timestamp::from_seconds(100),
vetoer: CREATOR_ADDR.to_string(),
early_execute: false
early_execute: false,
veto_before_passed: false,
}),
threshold: Threshold::AbsoluteCount {
threshold: Uint128::new(10_000)
Expand Down
17 changes: 17 additions & 0 deletions packages/dao-voting/src/timelock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,18 @@ pub enum TimelockError {
#[error("{0}")]
Std(#[from] StdError),

#[error("Proposal is {status}, this proposal status is unable to be vetoed.")]
InvalidProposalStatus { status: String },

#[error("Early execution for timelocked proposals is not enabled. Proposal can not be executed before the timelock delay has expired.")]
NoEarlyExecute {},

#[error("Timelock is not configured for this contract. Veto not enabled.")]
NoTimelock {},

#[error("Vetoing before a proposal passes is not enabled.")]
NoVetoBeforePassed {},

#[error("The proposal is time locked and cannot be executed.")]
Timelocked {},

Expand All @@ -32,6 +38,8 @@ pub struct Timelock {
/// Whether or not the vetoer can excute a proposal early before the
/// timelock duration has expired
pub early_execute: bool,
/// Whether or not the vetoer can veto a proposal before it passes.
pub veto_before_passed: bool,
}

impl Timelock {
Expand Down Expand Up @@ -82,4 +90,13 @@ impl Timelock {
Err(TimelockError::Unauthorized {})
}
}

/// Checks whether veto_before_passed is enabled, errors if not
pub fn check_veto_before_passed_enabled(&self) -> Result<(), TimelockError> {
if self.veto_before_passed {
Ok(())
} else {
Err(TimelockError::NoVetoBeforePassed {})
}
}
}

0 comments on commit 25031f1

Please sign in to comment.