From c9e8cdf37e2dc7cc833a8a75520d65bc3764a3f6 Mon Sep 17 00:00:00 2001 From: bekauz Date: Fri, 17 Nov 2023 21:35:28 +0100 Subject: [PATCH] unit tests wip --- .../dao-proposal-single/src/contract.rs | 27 +- .../dao-proposal-single/src/testing/tests.rs | 255 +++++++++++++++++- packages/dao-voting/src/status.rs | 6 +- packages/dao-voting/src/timelock.rs | 16 +- 4 files changed, 270 insertions(+), 34 deletions(-) diff --git a/contracts/proposal/dao-proposal-single/src/contract.rs b/contracts/proposal/dao-proposal-single/src/contract.rs index d1ae144bc..b8dbe5218 100644 --- a/contracts/proposal/dao-proposal-single/src/contract.rs +++ b/contracts/proposal/dao-proposal-single/src/contract.rs @@ -62,8 +62,6 @@ pub fn instantiate( .pre_propose_info .into_initial_policy_and_messages(dao.clone())?; - // TODO: validate timelock? - let config = Config { threshold: msg.threshold, max_voting_period, @@ -319,7 +317,9 @@ pub fn execute_veto( // vetoer can veto the proposal iff the timelock is active/not expired if expiration.is_expired(&env.block) { - return Err(ContractError::TimelockError(TimelockError::TimelockExpired { })) + return Err(ContractError::TimelockError( + TimelockError::TimelockExpired {}, + )); } let old_status = prop.status; @@ -375,6 +375,7 @@ pub fn execute_execute( .ok_or(ContractError::NoSuchProposal { id: proposal_id })?; let config = CONFIG.load(deps.storage)?; + // TODO: add exception for vetoer execution if config.only_members_execute { let power = get_voting_power( deps.as_ref(), @@ -387,9 +388,9 @@ pub fn execute_execute( } } - // Check here that the proposal is passed. Allow it to be executed - // even if it is expired so long as it passed during its voting - // period. + // Check here that the proposal is passed or timelocked. + // Allow it to be executed even if it is expired so long + // as it passed during its voting period. prop.update_status(&env.block); let old_status = prop.status; match prop.status { @@ -397,13 +398,13 @@ pub fn execute_execute( Status::Timelocked { expiration } => { if let Some(ref timelock) = prop.timelock { // Check if the sender is the vetoer - if timelock.check_is_vetoer(&info).is_ok() { - // check if they can execute early - timelock.check_early_execute_enabled()?; - } else if !expiration.is_expired(&env.block) { - // anyone can execute the proposal iff timelock - // expiration is due. otherwise we error. - return Err(ContractError::TimelockError(TimelockError::Timelocked {})) + match timelock.vetoer == info.sender { + // if sender is the vetoer we validate the early exec flag + true => timelock.check_early_execute_enabled()?, + // otherwise timelock must be expired in order to execute + false => if !expiration.is_expired(&env.block) { + return Err(ContractError::TimelockError(TimelockError::Timelocked {})); + } } } } diff --git a/contracts/proposal/dao-proposal-single/src/testing/tests.rs b/contracts/proposal/dao-proposal-single/src/testing/tests.rs index 95ceab31e..72172d89f 100644 --- a/contracts/proposal/dao-proposal-single/src/testing/tests.rs +++ b/contracts/proposal/dao-proposal-single/src/testing/tests.rs @@ -439,7 +439,7 @@ fn test_proposal_message_timelock_execution() { let native_balance = query_balance_native(&app, CREATOR_ADDR, "ujuno"); assert_eq!(cw20_balance, Uint128::zero()); assert_eq!(native_balance, Uint128::zero()); - + vote_on_proposal( &mut app, &proposal_module, @@ -448,7 +448,7 @@ fn test_proposal_message_timelock_execution() { Vote::Yes, ); let proposal = query_proposal(&app, &proposal_module, proposal_id); - + // Proposal is timelocked to the moment of prop passing + timelock delay assert_eq!( proposal.proposal.status, @@ -459,8 +459,8 @@ fn test_proposal_message_timelock_execution() { mint_natives(&mut app, core_addr.as_str(), coins(10, "ujuno")); - // Test even oversite can't execute when Timelocked and early execute is - // not enabled. + // vetoer can't execute when timelock is active and + // early execute not enabled. let err: ContractError = app .execute_contract( Addr::unchecked("oversight"), @@ -504,6 +504,253 @@ fn test_proposal_message_timelock_execution() { assert_eq!(proposal.proposal.status, Status::Executed); } +// only the authorized vetoer can veto an open proposal +#[test] +fn test_open_proposal_veto_unauthorized() { + let mut app = App::default(); + let mut instantiate = get_default_token_dao_proposal_module_instantiate(&mut app); + instantiate.close_proposal_on_execution_failure = false; + let timelock = Timelock { + delay: Duration::Time(100), + vetoer: "oversight".to_string(), + early_execute: false, + veto_before_passed: true, + }; + instantiate.timelock = Some(timelock.clone()); + let core_addr = instantiate_with_staked_balances_governance( + &mut app, + instantiate, + Some(vec![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(), + ], + ); + + // only the vetoer can veto + let err: ContractError = app + .execute_contract( + Addr::unchecked("not-oversight"), + proposal_module.clone(), + &ExecuteMsg::Veto { proposal_id }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + ContractError::TimelockError(TimelockError::Unauthorized {}) + ); +} + +// open proposal can only be vetoed if `veto_before_passed` flag is enabled +#[test] +fn test_open_proposal_veto_with_early_veto_flag_disabled() { + let mut app = App::default(); + let mut instantiate = get_default_token_dao_proposal_module_instantiate(&mut app); + instantiate.close_proposal_on_execution_failure = false; + let timelock = Timelock { + delay: Duration::Time(100), + vetoer: "oversight".to_string(), + early_execute: false, + veto_before_passed: false, + }; + instantiate.timelock = Some(timelock.clone()); + let core_addr = instantiate_with_staked_balances_governance( + &mut app, + instantiate, + Some(vec![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 err: ContractError = app + .execute_contract( + Addr::unchecked("oversight"), + proposal_module.clone(), + &ExecuteMsg::Veto { proposal_id }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + ContractError::TimelockError(TimelockError::NoVetoBeforePassed {}) + ); +} + +#[test] +fn test_open_proposal_veto_early() { + let mut app = App::default(); + let mut instantiate = get_default_token_dao_proposal_module_instantiate(&mut app); + instantiate.close_proposal_on_execution_failure = false; + let timelock = Timelock { + delay: Duration::Time(100), + vetoer: "oversight".to_string(), + early_execute: false, + veto_before_passed: true, + }; + instantiate.timelock = Some(timelock.clone()); + let core_addr = instantiate_with_staked_balances_governance( + &mut app, + instantiate, + Some(vec![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(), + ], + ); + + 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 {} + ); +} + +// only the vetoer can veto during timelock period +#[test] +fn test_timelocked_proposal_veto_unauthorized() { + todo!() +} + +// vetoer can only veto the proposal before the timelock expires +#[test] +fn test_timelocked_proposal_veto_expired_timelock() { + todo!() +} + +#[test] +fn test_timelocked_proposal_veto_no_timelock_config() { + todo!() + // what +} + +// vetoer can only exec timelocked prop if the early exec flag is enabled +#[test] +fn test_timelocked_proposal_execute_no_early_exec() { + todo!() +} + +#[test] +fn test_timelocked_proposal_execute_early() { + todo!() +} + +// only vetoer can exec timelocked prop early +#[test] +fn test_timelocked_proposal_execute_active_timelock_unauthorized() { + todo!() +} + +// anyone can exec the prop after the timelock expires +#[test] +fn test_timelocked_proposal_execute_expired_timelock_not_vetoer() { + todo!() +} + +#[test] +fn test_timelocked_proposal_no_votes_accepted() { + todo!() +} + +#[test] +fn test_vetoed_proposal_no_votes_accepted() { + todo!() +} + +#[test] +fn test_update_vetoer_address() { + todo!() +} + #[test] fn test_proposal_message_timelock_veto() { let mut app = App::default(); diff --git a/packages/dao-voting/src/status.rs b/packages/dao-voting/src/status.rs index 4fffc20e3..7d25052c0 100644 --- a/packages/dao-voting/src/status.rs +++ b/packages/dao-voting/src/status.rs @@ -17,8 +17,8 @@ pub enum Status { Closed, /// The proposal's execution failed. ExecutionFailed, - /// Proposal is timelocked and can not be until the timelock expires - /// During this time the proposal may be vetoed. + /// The proposal is timelocked. Only the configured vetoer + /// can execute or veto until the timelock expires. Timelocked { expiration: Expiration }, /// The proposal has been vetoed. Vetoed, @@ -33,7 +33,7 @@ impl std::fmt::Display for Status { Status::Executed => write!(f, "executed"), Status::Closed => write!(f, "closed"), Status::ExecutionFailed => write!(f, "execution_failed"), - Status::Timelocked { expiration } => write!(f, "timelocked {:?}", expiration), + Status::Timelocked { expiration } => write!(f, "timelocked_until {:?}", expiration), Status::Vetoed => write!(f, "vetoed"), } } diff --git a/packages/dao-voting/src/timelock.rs b/packages/dao-voting/src/timelock.rs index f3513a0e3..6099fd978 100644 --- a/packages/dao-voting/src/timelock.rs +++ b/packages/dao-voting/src/timelock.rs @@ -1,6 +1,6 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{MessageInfo, StdError, Timestamp, BlockInfo}; -use cw_utils::{Duration, Expiration}; +use cosmwasm_std::{MessageInfo, StdError}; +use cw_utils::Duration; use thiserror::Error; #[derive(Error, Debug, PartialEq)] @@ -53,18 +53,6 @@ impl Timelock { } } - pub fn check_is_expired( - &self, - current_time: Timestamp, - expires: Timestamp, - ) -> Result<(), TimelockError> { - if expires.seconds() > current_time.seconds() { - Ok(()) - } else { - Err(TimelockError::Timelocked {}) - } - } - /// Checks whether the message sender is the vetoer. pub fn check_is_vetoer(&self, info: &MessageInfo) -> Result<(), TimelockError> { if self.vetoer == info.sender.to_string() {