From d46af0962e0b1cb6f64e530a724c1b4fefe8625a Mon Sep 17 00:00:00 2001 From: Noah Saso Date: Thu, 17 Oct 2024 19:53:46 -0400 Subject: [PATCH] added more delegation tests --- Cargo.lock | 1 + .../delegation/dao-vote-delegation/Cargo.toml | 1 + .../dao-vote-delegation/src/testing/mod.rs | 3 + .../dao-vote-delegation/src/testing/suite.rs | 401 +++++++++++++ .../dao-vote-delegation/src/testing/tests.rs | 560 ++++++++++++++---- packages/dao-interface/src/helpers.rs | 2 +- packages/dao-testing/src/suite/base.rs | 147 ++--- packages/dao-testing/src/suite/cw20_suite.rs | 8 +- packages/dao-testing/src/suite/cw4_suite.rs | 4 +- packages/dao-testing/src/suite/cw721_suite.rs | 8 +- packages/dao-testing/src/suite/token_suite.rs | 8 +- 11 files changed, 944 insertions(+), 199 deletions(-) create mode 100644 contracts/delegation/dao-vote-delegation/src/testing/suite.rs diff --git a/Cargo.lock b/Cargo.lock index f1e405157..6bae64b4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2370,6 +2370,7 @@ dependencies = [ "cw721-base 0.18.0", "dao-hooks 2.5.0", "dao-interface 2.5.0", + "dao-proposal-sudo", "dao-testing", "dao-voting 2.5.0", "dao-voting-cw20-staked", diff --git a/contracts/delegation/dao-vote-delegation/Cargo.toml b/contracts/delegation/dao-vote-delegation/Cargo.toml index 0fb0d7957..086f9561d 100644 --- a/contracts/delegation/dao-vote-delegation/Cargo.toml +++ b/contracts/delegation/dao-vote-delegation/Cargo.toml @@ -45,3 +45,4 @@ dao-voting-cw4 = { workspace = true, features = ["library"] } dao-voting-token-staked = { workspace = true, features = ["library"] } dao-voting-cw721-staked = { workspace = true, features = ["library"] } dao-testing = { workspace = true } +dao-proposal-sudo = { workspace = true } diff --git a/contracts/delegation/dao-vote-delegation/src/testing/mod.rs b/contracts/delegation/dao-vote-delegation/src/testing/mod.rs index 15ab56057..591459a01 100644 --- a/contracts/delegation/dao-vote-delegation/src/testing/mod.rs +++ b/contracts/delegation/dao-vote-delegation/src/testing/mod.rs @@ -1 +1,4 @@ +pub mod suite; pub mod tests; + +pub use suite::*; diff --git a/contracts/delegation/dao-vote-delegation/src/testing/suite.rs b/contracts/delegation/dao-vote-delegation/src/testing/suite.rs new file mode 100644 index 000000000..447e5c90a --- /dev/null +++ b/contracts/delegation/dao-vote-delegation/src/testing/suite.rs @@ -0,0 +1,401 @@ +use std::ops::{Deref, DerefMut}; + +use cosmwasm_std::{Addr, Decimal, Uint128}; +use dao_interface::helpers::{OptionalUpdate, Update}; +use dao_testing::{Cw4TestDao, DaoTestingSuite, DaoTestingSuiteBase}; + +use super::tests::dao_vote_delegation_contract; + +pub struct DaoVoteDelegationTestingSuite { + /// base testing suite that we're extending + pub base: DaoTestingSuiteBase, + + // initial config + vp_cap_percent: Option, + delegation_validity_blocks: Option, + + /// cw4-group voting DAO + pub dao: Cw4TestDao, + /// members of the DAO + pub members: Vec, + + /// delegation code ID + pub delegation_code_id: u64, + /// delegation contract address + pub delegation_addr: Addr, +} + +// allow direct access to base testing suite methods +impl Deref for DaoVoteDelegationTestingSuite { + type Target = DaoTestingSuiteBase; + + fn deref(&self) -> &Self::Target { + &self.base + } +} + +// allow direct access to base testing suite methods +impl DerefMut for DaoVoteDelegationTestingSuite { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.base + } +} + +// CONSTRUCTOR +impl DaoVoteDelegationTestingSuite { + pub fn new() -> Self { + let mut base = DaoTestingSuiteBase::base(); + let mut suite = base.cw4(); + + let members = suite.members.clone(); + let dao = suite.dao(); + + let delegation_code_id = suite.store(dao_vote_delegation_contract); + + Self { + base, + + vp_cap_percent: None, + delegation_validity_blocks: None, + + dao, + members, + + delegation_code_id, + delegation_addr: Addr::unchecked(""), + } + } + + pub fn with_vp_cap_percent(mut self, vp_cap_percent: Decimal) -> Self { + self.vp_cap_percent = Some(vp_cap_percent); + self + } + + pub fn with_delegation_validity_blocks(mut self, delegation_validity_blocks: u64) -> Self { + self.delegation_validity_blocks = Some(delegation_validity_blocks); + self + } + + pub fn build(mut self) -> Self { + let code_id = self.delegation_code_id; + let core_addr = self.dao.core_addr.clone(); + let group_addr = self.dao.x.group_addr.to_string(); + let vp_cap_percent = self.vp_cap_percent; + let delegation_validity_blocks = self.delegation_validity_blocks; + + self.delegation_addr = self.instantiate( + code_id, + &core_addr, + &crate::msg::InstantiateMsg { + dao: None, + vp_hook_callers: Some(vec![group_addr]), + no_sync_proposal_modules: None, + vp_cap_percent, + delegation_validity_blocks, + }, + &[], + "delegation", + None, + ); + + self + } +} + +// EXECUTIONS +impl DaoVoteDelegationTestingSuite { + /// register a user as a delegate + pub fn register(&mut self, delegate: impl Into) { + let delegation_addr = self.delegation_addr.clone(); + self.execute_smart_ok( + delegate, + delegation_addr, + &crate::msg::ExecuteMsg::Register {}, + &[], + ); + } + + /// unregister a delegate + pub fn unregister(&mut self, delegate: impl Into) { + let delegation_addr = self.delegation_addr.clone(); + self.execute_smart_ok( + delegate, + delegation_addr, + &crate::msg::ExecuteMsg::Unregister {}, + &[], + ); + } + + /// create or update a delegation + pub fn delegate( + &mut self, + delegator: impl Into, + delegate: impl Into, + percent: Decimal, + ) { + let delegation_addr = self.delegation_addr.clone(); + self.execute_smart_ok( + delegator, + delegation_addr, + &crate::msg::ExecuteMsg::Delegate { + delegate: delegate.into(), + percent, + }, + &[], + ); + } + + /// revoke a delegation + pub fn undelegate(&mut self, delegator: impl Into, delegate: impl Into) { + let delegation_addr = self.delegation_addr.clone(); + self.execute_smart_ok( + delegator, + delegation_addr, + &crate::msg::ExecuteMsg::Undelegate { + delegate: delegate.into(), + }, + &[], + ); + } + + /// update voting power hook callers + pub fn update_voting_power_hook_callers( + &mut self, + add: Option>, + remove: Option>, + ) { + let core_addr = self.dao.core_addr.clone(); + let delegation_addr = self.delegation_addr.clone(); + self.execute_smart_ok( + core_addr, + delegation_addr, + &crate::msg::ExecuteMsg::UpdateVotingPowerHookCallers { add, remove }, + &[], + ); + } + + /// sync proposal modules + pub fn sync_proposal_modules(&mut self, start_after: Option, limit: Option) { + let core_addr = self.dao.core_addr.clone(); + let delegation_addr = self.delegation_addr.clone(); + self.execute_smart_ok( + core_addr, + delegation_addr, + &crate::msg::ExecuteMsg::SyncProposalModules { start_after, limit }, + &[], + ); + } + + /// update VP cap percent + pub fn update_vp_cap_percent(&mut self, vp_cap_percent: Option) { + let core_addr = self.dao.core_addr.clone(); + let delegation_addr = self.delegation_addr.clone(); + self.execute_smart_ok( + core_addr, + delegation_addr, + &crate::msg::ExecuteMsg::UpdateConfig { + vp_cap_percent: OptionalUpdate(Some( + vp_cap_percent.map_or(Update::Clear, Update::Set), + )), + delegation_validity_blocks: OptionalUpdate(None), + }, + &[], + ); + } + + /// update delegation validity blocks + pub fn update_delegation_validity_blocks(&mut self, delegation_validity_blocks: Option) { + let core_addr = self.dao.core_addr.clone(); + let delegation_addr = self.delegation_addr.clone(); + self.execute_smart_ok( + core_addr, + delegation_addr, + &crate::msg::ExecuteMsg::UpdateConfig { + vp_cap_percent: OptionalUpdate(None), + delegation_validity_blocks: OptionalUpdate(Some( + delegation_validity_blocks.map_or(Update::Clear, Update::Set), + )), + }, + &[], + ); + } +} + +/// QUERIES +impl DaoVoteDelegationTestingSuite { + /// get the delegates + pub fn delegates( + &self, + start_after: Option, + limit: Option, + ) -> Vec { + self.querier() + .query_wasm_smart::( + &self.delegation_addr, + &crate::msg::QueryMsg::Delegates { start_after, limit }, + ) + .unwrap() + .delegates + } + + /// get the delegations + pub fn delegations( + &self, + delegator: impl Into, + height: Option, + offset: Option, + limit: Option, + ) -> dao_voting::delegation::DelegationsResponse { + self.querier() + .query_wasm_smart( + &self.delegation_addr, + &crate::msg::QueryMsg::Delegations { + delegator: delegator.into(), + height, + offset, + limit, + }, + ) + .unwrap() + } + + /// get the unvoted delegated voting power for a proposal + pub fn unvoted_delegated_voting_power( + &self, + delegate: impl Into, + proposal_module: impl Into, + proposal_id: u64, + start_height: u64, + ) -> dao_voting::delegation::UnvotedDelegatedVotingPowerResponse { + self.querier() + .query_wasm_smart( + &self.delegation_addr, + &crate::msg::QueryMsg::UnvotedDelegatedVotingPower { + delegate: delegate.into(), + proposal_module: proposal_module.into(), + proposal_id, + height: start_height, + }, + ) + .unwrap() + } + + /// get the proposal modules + pub fn proposal_modules(&self, start_after: Option, limit: Option) -> Vec { + self.querier() + .query_wasm_smart( + &self.delegation_addr, + &crate::msg::QueryMsg::ProposalModules { start_after, limit }, + ) + .unwrap() + } + + /// get the voting power hook callers + pub fn voting_power_hook_callers( + &self, + start_after: Option, + limit: Option, + ) -> Vec { + self.querier() + .query_wasm_smart( + &self.delegation_addr, + &crate::msg::QueryMsg::VotingPowerHookCallers { start_after, limit }, + ) + .unwrap() + } +} + +/// ASSERTIONS +impl DaoVoteDelegationTestingSuite { + /// assert that there are N delegations + pub fn assert_delegations_count(&self, delegator: impl Into, count: u32) { + let delegations = self.delegations(delegator, None, None, None); + assert_eq!(delegations.delegations.len() as u32, count); + } + + /// assert that there are N active delegations + pub fn assert_active_delegations_count(&self, delegator: impl Into, count: u32) { + let delegations = self.delegations(delegator, None, None, None); + assert_eq!( + delegations.delegations.iter().filter(|d| d.active).count() as u32, + count + ); + } + + /// assert that an active delegation exists + pub fn assert_delegation( + &self, + delegator: impl Into, + delegate: impl Into + Copy, + percent: Decimal, + ) { + let delegations = self.delegations(delegator, None, None, None); + assert!(delegations + .delegations + .iter() + .any(|d| d.delegate == delegate.into() && d.percent == percent && d.active)); + } + + /// assert that there are N delegates + pub fn assert_delegates_count(&self, count: u32) { + let delegates = self.delegates(None, None); + assert_eq!(delegates.len() as u32, count); + } + + /// assert a delegate is registered + pub fn assert_registered(&self, delegate: impl Into + Copy) { + let delegates = self.delegates(None, None); + assert!(delegates.iter().any(|d| d.delegate == delegate.into())); + } + + /// assert a delegate's total delegated voting power + pub fn assert_delegate_total_delegated_vp( + &self, + delegate: impl Into + Copy, + expected_total: impl Into, + ) { + let delegate_total = self + .delegates(None, None) + .into_iter() + .find(|d| d.delegate == delegate.into()) + .unwrap() + .power; + assert_eq!(delegate_total, expected_total.into()); + } + + /// assert a delegate's total UDVP on a proposal + pub fn assert_total_udvp( + &self, + delegate: impl Into, + proposal_module: impl Into, + proposal_id: u64, + start_height: u64, + total: impl Into, + ) { + let udvp = self.unvoted_delegated_voting_power( + delegate, + proposal_module, + proposal_id, + start_height, + ); + assert_eq!(udvp.total, total.into()); + } + + /// assert a delegate's effective UDVP on a proposal + pub fn assert_effective_udvp( + &self, + delegate: impl Into, + proposal_module: impl Into, + proposal_id: u64, + start_height: u64, + effective: impl Into, + ) { + let udvp = self.unvoted_delegated_voting_power( + delegate, + proposal_module, + proposal_id, + start_height, + ); + assert_eq!(udvp.effective, effective.into()); + } +} diff --git a/contracts/delegation/dao-vote-delegation/src/testing/tests.rs b/contracts/delegation/dao-vote-delegation/src/testing/tests.rs index 9743cb017..243c4e330 100644 --- a/contracts/delegation/dao-vote-delegation/src/testing/tests.rs +++ b/contracts/delegation/dao-vote-delegation/src/testing/tests.rs @@ -1,7 +1,8 @@ -use cosmwasm_std::{Addr, Decimal, Empty, Uint128}; +use cosmwasm_std::{to_json_binary, Addr, Decimal, Empty, Uint128}; use cw_multi_test::{Contract, ContractWrapper}; -use dao_testing::{DaoTestingSuite, DaoTestingSuiteBase, ADDR0, ADDR1, ADDR2}; -use dao_voting::delegation::{DelegationResponse, DelegationsResponse}; +use dao_testing::{ADDR0, ADDR1, ADDR2}; + +use super::*; pub fn dao_vote_delegation_contract() -> Box> { let contract = ContractWrapper::new( @@ -14,144 +15,475 @@ pub fn dao_vote_delegation_contract() -> Box> { } #[test] -fn test_setup() { - let mut base = DaoTestingSuiteBase::base(); - let mut suite = base.cw4(); - let dao = suite.dao(); - - let code_id = suite.store(dao_vote_delegation_contract); - let delegation_addr = suite.instantiate( - code_id, - &dao.core_addr, - &crate::msg::InstantiateMsg { - dao: None, - vp_hook_callers: Some(vec![dao.x.group_addr.to_string()]), - no_sync_proposal_modules: None, - vp_cap_percent: Some(Decimal::percent(50)), - delegation_validity_blocks: Some(100), - }, - &[], - "delegation", - None, +fn test_simple() { + let mut suite = DaoVoteDelegationTestingSuite::new() + .with_vp_cap_percent(Decimal::percent(50)) + .with_delegation_validity_blocks(10) + .build(); + let dao = suite.dao.clone(); + + // ensure set up correctly + assert_eq!( + suite.voting_power_hook_callers(None, None), + vec![dao.x.group_addr.clone()] + ); + assert_eq!( + suite.proposal_modules(None, None), + dao.proposal_modules + .iter() + .map(|p| p.1.clone()) + .collect::>() ); - // register addr0 as a delegate - suite.execute_smart( + // register ADDR0 as a delegate + suite.register(ADDR0); + suite.assert_delegates_count(1); + suite.assert_registered(ADDR0); + + // delegate 100% of addr1's voting power to ADDR0 + suite.delegate(ADDR1, ADDR0, Decimal::percent(100)); + + // delegations take effect on the next block + suite.advance_block(); + + suite.assert_delegations_count(ADDR1, 1); + suite.assert_delegation(ADDR1, ADDR0, Decimal::percent(100)); + suite.assert_delegate_total_delegated_vp(ADDR0, suite.members[1].weight); + + // propose a proposal + let (proposal_module, id1, p1) = + suite.propose_single_choice(&dao, ADDR0, "test proposal 1", vec![]); + + // ensure delegation is correctly applied to proposal + suite.assert_effective_udvp( ADDR0, - &delegation_addr, - &crate::msg::ExecuteMsg::Register {}, - &[], + &proposal_module, + id1, + p1.start_height, + suite.members[1].weight, + ); + suite.assert_total_udvp( + ADDR0, + &proposal_module, + id1, + p1.start_height, + suite.members[1].weight, ); - // delegate 100% of addr1's voting power to addr0 - suite.execute_smart( - ADDR1, - &delegation_addr, - &crate::msg::ExecuteMsg::Delegate { - delegate: ADDR0.to_string(), - percent: Decimal::percent(100), - }, - &[], + // set delegation to 50% + suite.delegate(ADDR1, ADDR0, Decimal::percent(50)); + + // delegations take effect on the next block + suite.advance_block(); + + suite.assert_delegation(ADDR1, ADDR0, Decimal::percent(50)); + suite.assert_delegate_total_delegated_vp(ADDR0, suite.members[1].weight / 2); + + // propose another proposal + let (_, id2, p2) = suite.propose_single_choice(&dao, ADDR2, "test proposal 2", vec![]); + + // ensure delegation is correctly applied to new proposal + suite.assert_effective_udvp( + ADDR0, + &proposal_module, + id2, + p2.start_height, + suite.members[1].weight / 2, + ); + + // ensure old delegation is still applied to old proposal + suite.assert_effective_udvp( + ADDR0, + &proposal_module, + id1, + p1.start_height, + suite.members[1].weight, ); + // revoke delegation + suite.undelegate(ADDR1, ADDR0); + // delegations take effect on the next block suite.advance_block(); - let delegations: DelegationsResponse = suite - .querier() - .query_wasm_smart( - &delegation_addr, - &crate::msg::QueryMsg::Delegations { - delegator: ADDR1.to_string(), - height: None, - offset: None, - limit: None, - }, - ) - .unwrap(); - - assert_eq!(delegations.delegations.len(), 1); - assert_eq!( - delegations.delegations[0], - DelegationResponse { - delegate: Addr::unchecked(ADDR0), - percent: Decimal::percent(100), - active: true, + suite.assert_delegations_count(ADDR1, 0); + suite.assert_delegate_total_delegated_vp(ADDR0, 0u128); + + // propose another proposal + let (_, id3, p3) = suite.propose_single_choice(&dao, ADDR2, "test proposal 3", vec![]); + + // ensure delegation is removed from new proposal + suite.assert_effective_udvp(ADDR0, &proposal_module, id3, p3.start_height, 0u128); + suite.assert_total_udvp(ADDR0, &proposal_module, id3, p3.start_height, 0u128); + + // delegate 100% of every other member's voting power to ADDR0 + for member in suite.members.clone() { + if member.addr != ADDR0 { + suite.delegate(member.addr, ADDR0, Decimal::percent(100)); } + } + + // delegations take effect on the next block + suite.advance_block(); + + let total_vp_except_addr0 = suite + .members + .iter() + .map(|m| if m.addr == ADDR0 { 0 } else { m.weight as u128 }) + .sum::(); + suite.assert_delegate_total_delegated_vp(ADDR0, total_vp_except_addr0); + + // propose another proposal + let (_, id4, p4) = suite.propose_single_choice(&dao, ADDR0, "test proposal 4", vec![]); + + // ensure delegation is correctly applied to proposal and that VP cap is + // applied correctly. effective should be 50% of total voting power, and + // total should be everything that's delegated to ADDR0 + suite.assert_effective_udvp( + ADDR0, + &proposal_module, + id4, + p4.start_height, + // VP cap is set to 50% of total voting power + Uint128::from(suite.members.iter().map(|m| m.weight as u128).sum::()) + .mul_floor(Decimal::percent(50)), + ); + suite.assert_total_udvp( + ADDR0, + &proposal_module, + id4, + p4.start_height, + total_vp_except_addr0, ); + // advance 10 blocks to expire all delegations + suite.advance_blocks(10); + + suite.assert_delegate_total_delegated_vp(ADDR0, 0u128); + + // propose another proposal + let (_, id5, p5) = suite.propose_single_choice(&dao, ADDR0, "test proposal 5", vec![]); + + suite.assert_effective_udvp(ADDR0, &proposal_module, id5, p5.start_height, 0u128); + suite.assert_total_udvp(ADDR0, &proposal_module, id5, p5.start_height, 0u128); + + // delegate 100% of every other member's voting power to ADDR0 again + for member in suite.members.clone() { + if member.addr != ADDR0 { + suite.delegate(member.addr, ADDR0, Decimal::percent(100)); + } + } + + // delegations take effect on the next block + suite.advance_block(); + + suite.assert_delegate_total_delegated_vp(ADDR0, total_vp_except_addr0); + + // unregister ADDR0 as a delegate + suite.unregister(ADDR0); + + // delegations take effect on the next block + suite.advance_block(); + + suite.assert_delegates_count(0); + + // propose another proposal + let (_, id6, p6) = suite.propose_single_choice(&dao, ADDR0, "test proposal 6", vec![]); + + suite.assert_effective_udvp(ADDR0, &proposal_module, id6, p6.start_height, 0u128); + suite.assert_total_udvp(ADDR0, &proposal_module, id6, p6.start_height, 0u128); + + // ensure that ADDR1 has 1 delegation but 0 active delegations since their + // delegate unregistered + suite.assert_delegations_count(ADDR1, 1); + suite.assert_active_delegations_count(ADDR1, 0); +} + +#[test] +fn test_vp_cap_update() { + let mut suite = DaoVoteDelegationTestingSuite::new() + .with_vp_cap_percent(Decimal::percent(50)) + .with_delegation_validity_blocks(10) + .build(); + let dao = suite.dao.clone(); + + // register ADDR0 as a delegate + suite.register(ADDR0); + + // delegate 100% of every other member's voting power to ADDR0 + for member in suite.members.clone() { + if member.addr != ADDR0 { + suite.delegate(member.addr, ADDR0, Decimal::percent(100)); + } + } + + // delegations take effect on the next block + suite.advance_block(); + + let total_vp_except_addr0 = suite + .members + .iter() + .map(|m| if m.addr == ADDR0 { 0 } else { m.weight as u128 }) + .sum::(); + suite.assert_delegate_total_delegated_vp(ADDR0, total_vp_except_addr0); + // propose a proposal let (proposal_module, id1, p1) = - dao.propose_single_choice(&mut suite, ADDR0, "test proposal 1", vec![]); + suite.propose_single_choice(&dao, ADDR0, "test proposal", vec![]); - // ensure delegation is correctly applied to proposal - let udvp: dao_voting::delegation::UnvotedDelegatedVotingPowerResponse = suite - .querier() - .query_wasm_smart( - &delegation_addr, - &crate::msg::QueryMsg::UnvotedDelegatedVotingPower { - delegate: ADDR0.to_string(), - proposal_module: proposal_module.to_string(), - proposal_id: id1, - height: p1.start_height, - }, - ) - .unwrap(); - assert_eq!( - udvp.effective, - Uint128::from(suite.members[1].weight as u128) + // ensure delegation is correctly applied to proposal and that VP cap is + // applied correctly. effective should be 50% of total voting power, and + // total should be everything that's delegated to ADDR0 + suite.assert_effective_udvp( + ADDR0, + &proposal_module, + id1, + p1.start_height, + // VP cap is set to 50% of total voting power + Uint128::from(suite.members.iter().map(|m| m.weight as u128).sum::()) + .mul_floor(Decimal::percent(50)), + ); + suite.assert_total_udvp( + ADDR0, + &proposal_module, + id1, + p1.start_height, + total_vp_except_addr0, ); - // set delegation to 50% - suite.execute_smart( - ADDR1, - &delegation_addr, - &crate::msg::ExecuteMsg::Delegate { - delegate: ADDR0.to_string(), - percent: Decimal::percent(50), - }, - &[], + // change VP cap to 30% of total + suite.update_vp_cap_percent(Some(Decimal::percent(30))); + // updates take effect on the next block + suite.advance_block(); + + // propose another proposal + let (_, id2, p2) = suite.propose_single_choice(&dao, ADDR0, "test proposal", vec![]); + + // ensure delegation is correctly applied to proposal and that VP cap is + // applied correctly. effective should be 30% of total voting power, and + // total should still be everything that's delegated to ADDR0 + suite.assert_effective_udvp( + ADDR0, + &proposal_module, + id2, + p2.start_height, + // VP cap is set to 30% of total voting power + Uint128::from(suite.members.iter().map(|m| m.weight as u128).sum::()) + .mul_floor(Decimal::percent(30)), + ); + suite.assert_total_udvp( + ADDR0, + &proposal_module, + id2, + p2.start_height, + total_vp_except_addr0, + ); + + // old proposal should still use old VP cap + suite.assert_effective_udvp( + ADDR0, + &proposal_module, + id1, + p1.start_height, + // VP cap is set to 50% of total voting power + Uint128::from(suite.members.iter().map(|m| m.weight as u128).sum::()) + .mul_floor(Decimal::percent(50)), + ); + suite.assert_total_udvp( + ADDR0, + &proposal_module, + id1, + p1.start_height, + total_vp_except_addr0, ); + // remove VP cap + suite.update_vp_cap_percent(None); + // updates take effect on the next block + suite.advance_block(); + + // propose another proposal + let (_, id3, p3) = suite.propose_single_choice(&dao, ADDR0, "test proposal", vec![]); + + // effective should now be equal to total since there is no cap + suite.assert_effective_udvp( + ADDR0, + &proposal_module, + id3, + p3.start_height, + total_vp_except_addr0, + ); + suite.assert_total_udvp( + ADDR0, + &proposal_module, + id3, + p3.start_height, + total_vp_except_addr0, + ); + + // old proposals should still use old VP caps + suite.assert_effective_udvp( + ADDR0, + &proposal_module, + id2, + p2.start_height, + // VP cap is set to 30% of total voting power + Uint128::from(suite.members.iter().map(|m| m.weight as u128).sum::()) + .mul_floor(Decimal::percent(30)), + ); + suite.assert_total_udvp( + ADDR0, + &proposal_module, + id2, + p2.start_height, + total_vp_except_addr0, + ); + suite.assert_effective_udvp( + ADDR0, + &proposal_module, + id1, + p1.start_height, + // VP cap is set to 50% of total voting power + Uint128::from(suite.members.iter().map(|m| m.weight as u128).sum::()) + .mul_floor(Decimal::percent(50)), + ); + suite.assert_total_udvp( + ADDR0, + &proposal_module, + id1, + p1.start_height, + total_vp_except_addr0, + ); +} + +#[test] +fn test_expiration_update() { + let mut suite = DaoVoteDelegationTestingSuite::new() + .with_delegation_validity_blocks(10) + .build(); + + // register ADDR0 as a delegate + suite.register(ADDR0); + + // delegate to ADDR0 + suite.delegate(ADDR1, ADDR0, Decimal::percent(100)); // delegations take effect on the next block suite.advance_block(); - // propose a proposal - let (_, id2, p2) = dao.propose_single_choice(&mut suite, ADDR2, "test proposal 2", vec![]); + suite.assert_delegations_count(ADDR1, 1); + suite.assert_delegation(ADDR1, ADDR0, Decimal::percent(100)); + suite.assert_delegate_total_delegated_vp(ADDR0, suite.members[1].weight); - // ensure delegation is correctly applied to new proposal - let udvp: dao_voting::delegation::UnvotedDelegatedVotingPowerResponse = suite - .querier() - .query_wasm_smart( - &delegation_addr, - &crate::msg::QueryMsg::UnvotedDelegatedVotingPower { - delegate: ADDR0.to_string(), - proposal_module: proposal_module.to_string(), - proposal_id: id2, - height: p2.start_height, - }, - ) - .unwrap(); + // update delegation validity blocks to 50 + suite.update_delegation_validity_blocks(Some(50)); + + // move 10 blocks into the future + suite.advance_blocks(10); + + // delegation should be expired after 10 blocks since update happened after + suite.assert_delegations_count(ADDR1, 0); + suite.assert_delegate_total_delegated_vp(ADDR0, 0u128); + + // delegate to ADDR0 + suite.delegate(ADDR1, ADDR0, Decimal::percent(100)); + // delegations take effect on the next block + suite.advance_block(); + + // move 10 blocks into the future + suite.advance_blocks(10); + + // delegation should still be active + suite.assert_delegations_count(ADDR1, 1); + suite.assert_delegation(ADDR1, ADDR0, Decimal::percent(100)); + suite.assert_delegate_total_delegated_vp(ADDR0, suite.members[1].weight); + + // move 40 blocks into the future + suite.advance_blocks(40); + + // delegation should be expired + suite.assert_delegations_count(ADDR1, 0); + suite.assert_delegate_total_delegated_vp(ADDR0, 0u128); + + suite.advance_block(); + + // remove expiration + suite.update_delegation_validity_blocks(None); + + // delegate to ADDR0 + suite.delegate(ADDR1, ADDR0, Decimal::percent(100)); + // delegations take effect on the next block + suite.advance_block(); + + // move 10 blocks into the future + suite.advance_blocks(10); + + // delegation should still be active + suite.assert_delegations_count(ADDR1, 1); + suite.assert_delegation(ADDR1, ADDR0, Decimal::percent(100)); + suite.assert_delegate_total_delegated_vp(ADDR0, suite.members[1].weight); + + // move 100 blocks into the future + suite.advance_blocks(100); + + // delegation should still be active + suite.assert_delegations_count(ADDR1, 1); + suite.assert_delegation(ADDR1, ADDR0, Decimal::percent(100)); + suite.assert_delegate_total_delegated_vp(ADDR0, suite.members[1].weight); +} + +#[test] +fn test_update_hook_callers() { + let mut suite = DaoVoteDelegationTestingSuite::new().build(); + let dao = suite.dao.clone(); + + // ensure setup correctly assert_eq!( - udvp.effective, - Uint128::from((suite.members[1].weight / 2) as u128) + suite.voting_power_hook_callers(None, None), + vec![dao.x.group_addr.clone()] + ); + assert_eq!( + suite.proposal_modules(None, None), + dao.proposal_modules + .iter() + .map(|p| p.1.clone()) + .collect::>() ); - // ensure old delegation is still applied to old proposal - let udvp: dao_voting::delegation::UnvotedDelegatedVotingPowerResponse = suite - .querier() - .query_wasm_smart( - &delegation_addr, - &crate::msg::QueryMsg::UnvotedDelegatedVotingPower { - delegate: ADDR0.to_string(), - proposal_module: proposal_module.to_string(), - proposal_id: id1, - height: p1.start_height, - }, - ) - .unwrap(); + // add another contract as a voting power hook caller + suite.update_voting_power_hook_callers(Some(vec!["addr".to_string()]), None); + + assert_eq!( + suite.voting_power_hook_callers(None, None), + vec![Addr::unchecked("addr"), dao.x.group_addr.clone()] + ); + + // add another proposal module to the DAO + let proposal_sudo_code_id = suite.proposal_sudo_id; + suite.execute_smart_ok( + &dao.core_addr, + &dao.core_addr, + &dao_interface::msg::ExecuteMsg::UpdateProposalModules { + to_add: vec![dao_interface::state::ModuleInstantiateInfo { + code_id: proposal_sudo_code_id, + msg: to_json_binary(&dao_proposal_sudo::msg::InstantiateMsg { + root: "root".to_string(), + }) + .unwrap(), + admin: None, + label: "sudo".to_string(), + funds: vec![], + }], + to_disable: vec![], + }, + &[], + ); + + // sync proposal modules + suite.sync_proposal_modules(None, None); + + // ensure new proposal module is synced assert_eq!( - udvp.effective, - Uint128::from(suite.members[1].weight as u128) + suite.proposal_modules(None, None).len(), + dao.proposal_modules.len() + 1 ); } diff --git a/packages/dao-interface/src/helpers.rs b/packages/dao-interface/src/helpers.rs index 40ba76c2c..e15d325a5 100644 --- a/packages/dao-interface/src/helpers.rs +++ b/packages/dao-interface/src/helpers.rs @@ -8,7 +8,7 @@ pub enum Update { /// An update type that allows partial updates of optional fields. #[cw_serde] -pub struct OptionalUpdate(Option>); +pub struct OptionalUpdate(pub Option>); impl OptionalUpdate { /// Updates the value if it exists, otherwise does nothing. diff --git a/packages/dao-testing/src/suite/base.rs b/packages/dao-testing/src/suite/base.rs index 8a27e8e99..dac4cf379 100644 --- a/packages/dao-testing/src/suite/base.rs +++ b/packages/dao-testing/src/suite/base.rs @@ -5,7 +5,7 @@ use std::{ use cosmwasm_std::{to_json_binary, Addr, Coin, CosmosMsg, Empty, QuerierWrapper, Timestamp}; use cw20::Cw20Coin; -use cw_multi_test::{App, AppResponse, Contract, Executor}; +use cw_multi_test::{error::AnyResult, App, AppResponse, Contract, Executor}; use cw_utils::Duration; use serde::Serialize; @@ -22,58 +22,6 @@ pub struct TestDao { pub x: Extra, } -impl TestDao { - /// propose a single choice proposal and return the proposal module address, - /// proposal ID, and proposal - pub fn propose_single_choice( - &self, - base: &mut DaoTestingSuiteBase, - proposer: impl Into, - title: impl Into, - msgs: Vec, - ) -> ( - Addr, - u64, - dao_proposal_single::proposal::SingleChoiceProposal, - ) { - let pre_propose_msg = dao_pre_propose_single::ExecuteMsg::Propose { - msg: dao_pre_propose_single::ProposeMessage::Propose { - title: title.into(), - description: "".to_string(), - msgs, - vote: None, - }, - }; - - let (pre_propose_module, proposal_module) = &self.proposal_modules[0]; - - base.execute_smart( - proposer, - pre_propose_module.as_ref().unwrap(), - &pre_propose_msg, - &[], - ); - - let proposal_id: u64 = base - .querier() - .query_wasm_smart( - proposal_module.clone(), - &dao_proposal_single::msg::QueryMsg::ProposalCount {}, - ) - .unwrap(); - - let res: dao_proposal_single::query::ProposalResponse = base - .querier() - .query_wasm_smart( - proposal_module.clone(), - &dao_proposal_single::msg::QueryMsg::Proposal { proposal_id }, - ) - .unwrap(); - - (proposal_module.clone(), proposal_id, res.proposal) - } -} - pub struct DaoTestingSuiteBase { pub app: App, @@ -409,9 +357,14 @@ impl DaoTestingSuiteBase { self.app.wrap() } + /// advance the block height by N + pub fn advance_blocks(&mut self, n: u64) { + self.app.update_block(|b| b.height += n); + } + /// advance the block height by one pub fn advance_block(&mut self) { - self.app.update_block(|b| b.height += 1); + self.advance_blocks(1); } /// store a contract given its maker function and return its code ID @@ -436,26 +389,36 @@ impl DaoTestingSuiteBase { init_msg, send_funds, label.into(), - admin.map(|a| a.into()), + admin, ) .unwrap() } - /// execute a smart contract and expect it to succeed + /// execute a smart contract and return the result pub fn execute_smart( &mut self, sender: impl Into, contract_addr: impl Into, msg: &T, send_funds: &[Coin], + ) -> AnyResult { + self.app.execute_contract( + Addr::unchecked(sender.into()), + Addr::unchecked(contract_addr.into()), + msg, + send_funds, + ) + } + + /// execute a smart contract and expect it to succeed + pub fn execute_smart_ok( + &mut self, + sender: impl Into, + contract_addr: impl Into, + msg: &T, + send_funds: &[Coin], ) -> AppResponse { - self.app - .execute_contract( - Addr::unchecked(sender.into()), - Addr::unchecked(contract_addr.into()), - msg, - send_funds, - ) + self.execute_smart(sender, contract_addr, msg, send_funds) .unwrap() } @@ -467,13 +430,7 @@ impl DaoTestingSuiteBase { msg: &T, send_funds: &[Coin], ) -> E { - self.app - .execute_contract( - Addr::unchecked(sender.into()), - Addr::unchecked(contract_addr.into()), - msg, - send_funds, - ) + self.execute_smart(sender, contract_addr, msg, send_funds) .unwrap_err() .downcast() .unwrap() @@ -497,4 +454,54 @@ impl DaoTestingSuiteBase { None, ) } + + /// propose a single choice proposal and return the proposal module address, + /// proposal ID, and proposal + pub fn propose_single_choice( + &mut self, + dao: &TestDao, + proposer: impl Into, + title: impl Into, + msgs: Vec, + ) -> ( + Addr, + u64, + dao_proposal_single::proposal::SingleChoiceProposal, + ) { + let pre_propose_msg = dao_pre_propose_single::ExecuteMsg::Propose { + msg: dao_pre_propose_single::ProposeMessage::Propose { + title: title.into(), + description: "".to_string(), + msgs, + vote: None, + }, + }; + + let (pre_propose_module, proposal_module) = &dao.proposal_modules[0]; + + self.execute_smart_ok( + proposer, + pre_propose_module.as_ref().unwrap(), + &pre_propose_msg, + &[], + ); + + let proposal_id: u64 = self + .querier() + .query_wasm_smart( + proposal_module.clone(), + &dao_proposal_single::msg::QueryMsg::ProposalCount {}, + ) + .unwrap(); + + let res: dao_proposal_single::query::ProposalResponse = self + .querier() + .query_wasm_smart( + proposal_module.clone(), + &dao_proposal_single::msg::QueryMsg::Proposal { proposal_id }, + ) + .unwrap(); + + (proposal_module.clone(), proposal_id, res.proposal) + } } diff --git a/packages/dao-testing/src/suite/cw20_suite.rs b/packages/dao-testing/src/suite/cw20_suite.rs index 6e2931362..4cbdd6667 100644 --- a/packages/dao-testing/src/suite/cw20_suite.rs +++ b/packages/dao-testing/src/suite/cw20_suite.rs @@ -27,13 +27,13 @@ impl<'a> Deref for DaoTestingSuiteCw20<'a> { type Target = DaoTestingSuiteBase; fn deref(&self) -> &Self::Target { - &self.base + self.base } } impl<'a> DerefMut for DaoTestingSuiteCw20<'a> { fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.base + self.base } } @@ -103,7 +103,7 @@ impl<'a> DaoTestingSuiteCw20<'a> { staker: impl Into, amount: impl Into, ) { - self.execute_smart( + self.execute_smart_ok( staker, &dao.x.cw20_addr, &cw20::Cw20ExecuteMsg::Send { @@ -122,7 +122,7 @@ impl<'a> DaoTestingSuiteCw20<'a> { staker: impl Into, amount: impl Into, ) { - self.execute_smart( + self.execute_smart_ok( staker, &dao.x.staking_addr, &cw20_stake::msg::ExecuteMsg::Unstake { diff --git a/packages/dao-testing/src/suite/cw4_suite.rs b/packages/dao-testing/src/suite/cw4_suite.rs index 2910de7a7..343ac2a83 100644 --- a/packages/dao-testing/src/suite/cw4_suite.rs +++ b/packages/dao-testing/src/suite/cw4_suite.rs @@ -21,13 +21,13 @@ impl<'a> Deref for DaoTestingSuiteCw4<'a> { type Target = DaoTestingSuiteBase; fn deref(&self) -> &Self::Target { - &self.base + self.base } } impl<'a> DerefMut for DaoTestingSuiteCw4<'a> { fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.base + self.base } } diff --git a/packages/dao-testing/src/suite/cw721_suite.rs b/packages/dao-testing/src/suite/cw721_suite.rs index 5ab18de7c..6fab1bbdf 100644 --- a/packages/dao-testing/src/suite/cw721_suite.rs +++ b/packages/dao-testing/src/suite/cw721_suite.rs @@ -31,13 +31,13 @@ impl<'a> Deref for DaoTestingSuiteCw721<'a> { type Target = DaoTestingSuiteBase; fn deref(&self) -> &Self::Target { - &self.base + self.base } } impl<'a> DerefMut for DaoTestingSuiteCw721<'a> { fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.base + self.base } } @@ -98,7 +98,7 @@ impl<'a> DaoTestingSuiteCw721<'a> { staker: impl Into, token_id: impl Into, ) { - self.execute_smart( + self.execute_smart_ok( staker, &dao.x.cw721_addr, &cw721_base::msg::ExecuteMsg::::SendNft { @@ -117,7 +117,7 @@ impl<'a> DaoTestingSuiteCw721<'a> { staker: impl Into, token_id: impl Into, ) { - self.execute_smart( + self.execute_smart_ok( staker, &dao.voting_module_addr, &dao_voting_cw721_staked::msg::ExecuteMsg::Unstake { diff --git a/packages/dao-testing/src/suite/token_suite.rs b/packages/dao-testing/src/suite/token_suite.rs index 7dd0733d8..28513c85a 100644 --- a/packages/dao-testing/src/suite/token_suite.rs +++ b/packages/dao-testing/src/suite/token_suite.rs @@ -26,13 +26,13 @@ impl<'a> Deref for DaoTestingSuiteToken<'a> { type Target = DaoTestingSuiteBase; fn deref(&self) -> &Self::Target { - &self.base + self.base } } impl<'a> DerefMut for DaoTestingSuiteToken<'a> { fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.base + self.base } } @@ -110,7 +110,7 @@ impl<'a> DaoTestingSuiteToken<'a> { staker: impl Into, amount: impl Into, ) { - self.execute_smart( + self.execute_smart_ok( staker, &dao.voting_module_addr, &dao_voting_token_staked::msg::ExecuteMsg::Stake {}, @@ -125,7 +125,7 @@ impl<'a> DaoTestingSuiteToken<'a> { staker: impl Into, amount: impl Into, ) { - self.execute_smart( + self.execute_smart_ok( staker, &dao.voting_module_addr, &dao_voting_token_staked::msg::ExecuteMsg::Unstake {