From a6ae9e7e0d1a520f10d07c842ca7a7f1ae0f1675 Mon Sep 17 00:00:00 2001 From: Noah Saso Date: Thu, 17 Oct 2024 01:32:02 -0400 Subject: [PATCH] added basic suite tests for delegations --- .../dao-vote-delegation/src/contract.rs | 7 + .../delegation/dao-vote-delegation/src/lib.rs | 4 +- .../dao-vote-delegation/src/testing/mod.rs | 1 + .../dao-vote-delegation/src/testing/tests.rs | 174 ++++++++++++++++++ packages/dao-testing/src/suite/base.rs | 86 ++++++++- 5 files changed, 266 insertions(+), 6 deletions(-) create mode 100644 contracts/delegation/dao-vote-delegation/src/testing/mod.rs create mode 100644 contracts/delegation/dao-vote-delegation/src/testing/tests.rs diff --git a/contracts/delegation/dao-vote-delegation/src/contract.rs b/contracts/delegation/dao-vote-delegation/src/contract.rs index 846384b94..037ebea14 100644 --- a/contracts/delegation/dao-vote-delegation/src/contract.rs +++ b/contracts/delegation/dao-vote-delegation/src/contract.rs @@ -75,6 +75,13 @@ pub fn instantiate( )?; VP_CAP_PERCENT.save(deps.storage, &msg.vp_cap_percent, env.block.height)?; + // initialize voting power changed hook callers + if let Some(vp_hook_callers) = msg.vp_hook_callers { + for caller in vp_hook_callers { + VOTING_POWER_HOOK_CALLERS.save(deps.storage, deps.api.addr_validate(&caller)?, &())?; + } + } + // sync proposal modules with no limit if not disabled. this should succeed // for most DAOs as the query will not run out of gas with only a few // proposal modules. diff --git a/contracts/delegation/dao-vote-delegation/src/lib.rs b/contracts/delegation/dao-vote-delegation/src/lib.rs index 5e7eaa5cc..167195f08 100644 --- a/contracts/delegation/dao-vote-delegation/src/lib.rs +++ b/contracts/delegation/dao-vote-delegation/src/lib.rs @@ -7,7 +7,7 @@ mod hooks; pub mod msg; pub mod state; -// #[cfg(test)] -// mod testing; +#[cfg(test)] +mod testing; pub use crate::error::ContractError; diff --git a/contracts/delegation/dao-vote-delegation/src/testing/mod.rs b/contracts/delegation/dao-vote-delegation/src/testing/mod.rs new file mode 100644 index 000000000..15ab56057 --- /dev/null +++ b/contracts/delegation/dao-vote-delegation/src/testing/mod.rs @@ -0,0 +1 @@ +pub mod tests; diff --git a/contracts/delegation/dao-vote-delegation/src/testing/tests.rs b/contracts/delegation/dao-vote-delegation/src/testing/tests.rs new file mode 100644 index 000000000..55f2c875e --- /dev/null +++ b/contracts/delegation/dao-vote-delegation/src/testing/tests.rs @@ -0,0 +1,174 @@ +use cosmwasm_std::{Addr, Decimal, Empty, Uint128}; +use cw_multi_test::{Contract, ContractWrapper}; +use dao_testing::{DaoTestingSuite, DaoTestingSuiteBase, Executor, ADDR0, ADDR1, ADDR2}; +use dao_voting::delegation::{DelegationResponse, DelegationsResponse}; + +pub fn dao_vote_delegation_contract() -> Box> { + let contract = ContractWrapper::new( + crate::contract::execute, + crate::contract::instantiate, + crate::contract::query, + ) + .with_migrate(crate::contract::migrate); + Box::new(contract) +} + +#[test] +fn test_setup() { + let mut base = DaoTestingSuiteBase::base(); + let mut suite = base.cw4(); + let dao = suite.dao(); + + let code_id = suite.base.app.store_code(dao_vote_delegation_contract()); + let delegation_addr = suite + .base + .app + .instantiate_contract( + code_id, + dao.core_addr.clone(), + &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, + ) + .unwrap(); + + // register addr0 as a delegate + suite + .base + .app + .execute_contract( + Addr::unchecked(ADDR0), + delegation_addr.clone(), + &crate::msg::ExecuteMsg::Register {}, + &[], + ) + .unwrap(); + + // delegate 100% of addr1's voting power to addr0 + suite + .base + .app + .execute_contract( + Addr::unchecked(ADDR1), + delegation_addr.clone(), + &crate::msg::ExecuteMsg::Delegate { + delegate: ADDR0.to_string(), + percent: Decimal::percent(100), + }, + &[], + ) + .unwrap(); + + // delegations take effect on the next block + suite.base.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, + } + ); + + // propose a proposal + let (proposal_module, id1, p1) = + dao.propose_single_choice(&mut suite.base.app, ADDR0, "test proposal 1", 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) + ); + + // set delegation to 50% + suite + .base + .app + .execute_contract( + Addr::unchecked(ADDR1), + delegation_addr.clone(), + &crate::msg::ExecuteMsg::Delegate { + delegate: ADDR0.to_string(), + percent: Decimal::percent(50), + }, + &[], + ) + .unwrap(); + + // delegations take effect on the next block + suite.base.advance_block(); + + // propose a proposal + let (_, id2, p2) = + dao.propose_single_choice(&mut suite.base.app, ADDR2, "test proposal 2", vec![]); + + // 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(); + assert_eq!( + udvp.effective, + Uint128::from((suite.members[1].weight / 2) as u128) + ); + + // 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(); + assert_eq!( + udvp.effective, + Uint128::from(suite.members[1].weight as u128) + ); +} diff --git a/packages/dao-testing/src/suite/base.rs b/packages/dao-testing/src/suite/base.rs index 85ec6d559..cadac5517 100644 --- a/packages/dao-testing/src/suite/base.rs +++ b/packages/dao-testing/src/suite/base.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{to_json_binary, Addr, Empty, QuerierWrapper, Timestamp}; +use cosmwasm_std::{to_json_binary, Addr, CosmosMsg, Empty, QuerierWrapper, Timestamp}; use cw20::Cw20Coin; use cw_multi_test::{App, Executor}; use cw_utils::Duration; @@ -10,10 +10,65 @@ use crate::contracts::*; pub struct TestDao { pub core_addr: Addr, pub voting_module_addr: Addr, - pub proposal_modules: Vec, + /// proposal modules in the form (pre-propose module, proposal module). if + /// the pre-propose module is None, then it does not exist. + pub proposal_modules: Vec<(Option, Addr)>, 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, + app: &mut App, + 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]; + + app.execute_contract( + Addr::unchecked(proposer.into()), + pre_propose_module.as_ref().unwrap().clone(), + &pre_propose_msg, + &[], + ) + .unwrap(); + + let proposal_id: u64 = app + .wrap() + .query_wasm_smart( + proposal_module.clone(), + &dao_proposal_single::msg::QueryMsg::ProposalCount {}, + ) + .unwrap(); + + let res: dao_proposal_single::query::ProposalResponse = app + .wrap() + .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, @@ -97,6 +152,7 @@ pub trait DaoTestingSuite { }, close_proposal_on_execution_failure: true, veto: None, + delegation_module: None, }) .unwrap(), admin: Some(dao_interface::state::Admin::CoreModule {}), @@ -134,6 +190,7 @@ pub trait DaoTestingSuite { }, close_proposal_on_execution_failure: true, veto: None, + delegation_module: None, }) .unwrap(), admin: Some(dao_interface::state::Admin::CoreModule {}), @@ -300,7 +357,7 @@ impl DaoTestingSuiteBase { let core = Addr::unchecked(instantiate_event.attributes[0].value.clone()); // get voting module address - let voting_module: Addr = self + let voting_module_addr: Addr = self .app .wrap() .query_wasm_smart(&core, &dao_interface::msg::QueryMsg::VotingModule {}) @@ -319,9 +376,30 @@ impl DaoTestingSuiteBase { ) .unwrap(); + let proposal_modules = proposal_modules + .into_iter() + .map(|p| -> (Option, Addr) { + let pre_propose_module: dao_voting::pre_propose::ProposalCreationPolicy = self + .app + .wrap() + .query_wasm_smart( + &p.address, + &dao_proposal_single::msg::QueryMsg::ProposalCreationPolicy {}, + ) + .unwrap(); + + match pre_propose_module { + dao_voting::pre_propose::ProposalCreationPolicy::Anyone {} => (None, p.address), + dao_voting::pre_propose::ProposalCreationPolicy::Module { addr } => { + (Some(addr), p.address) + } + } + }) + .collect::>(); + TestDao { core_addr: core, - voting_module_addr: voting_module, + voting_module_addr, proposal_modules, x: Empty::default(), }