diff --git a/Cargo.lock b/Cargo.lock index 0f1cd89..91e34cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -925,8 +925,10 @@ dependencies = [ "cw4-group", "dao-dao-macros", "dao-interface", + "dao-proposal-sudo", "dao-utils", "dao-voting-cw4", + "dao-voting-snip20-balance", "query_auth 0.1.0", "schemars 0.8.16", "secret-cosmwasm-std", @@ -1234,6 +1236,21 @@ dependencies = [ "thiserror", ] +[[package]] +name = "dao-proposal-sudo" +version = "2.4.0" +dependencies = [ + "cosmwasm-schema 1.1.11", + "dao-dao-macros", + "dao-interface", + "secret-cosmwasm-std", + "secret-cw2", + "secret-multi-test 0.13.4 (git+https://github.com/securesecrets/secret-plus-utils?branch=main)", + "secret-storage-plus 0.13.4 (git+https://github.com/securesecrets/secret-plus-utils?branch=main)", + "secret-toolkit", + "thiserror", +] + [[package]] name = "dao-snip721-extensions" version = "2.4.0" @@ -1287,6 +1304,7 @@ dependencies = [ "cosmwasm-schema 1.1.11", "cw-hooks", "cw4", + "dao-interface", "dao-snip721-extensions", "dao-voting 2.4.0", "schemars 0.8.16", @@ -1356,6 +1374,24 @@ dependencies = [ "thiserror", ] +[[package]] +name = "dao-voting-snip20-balance" +version = "2.4.0" +dependencies = [ + "cosmwasm-schema 1.1.11", + "dao-dao-macros", + "dao-interface", + "secret-cosmwasm-std", + "secret-cw2", + "secret-multi-test 0.13.4 (git+https://github.com/securesecrets/secret-plus-utils?branch=main)", + "secret-storage-plus 0.13.4 (git+https://github.com/securesecrets/secret-plus-utils?branch=main)", + "secret-toolkit", + "secret-utils 0.13.4 (git+https://github.com/securesecrets/secret-plus-utils?branch=main)", + "shade-protocol", + "snip20-reference-impl", + "thiserror", +] + [[package]] name = "dao-voting-snip20-staked" version = "2.4.0" diff --git a/Cargo.toml b/Cargo.toml index 6156c73..e1b4aec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,9 @@ members = [ "./contracts/pre-propose/*", "./contracts/staking/*", "./contracts/voting/*", - "./packages/*" + "./packages/*", + "./contracts/test/dao-proposal-sudo/", + "./contracts/test/dao-voting-snip20-balance/" ] resolver = "2" @@ -119,7 +121,8 @@ cw20-staked-balance-voting-v1 = { package = "cw20-staked-balance-voting", versio cw4-voting-v1 = { package = "cw4-voting", version = "0.1.0",default-features = false } stake-cw20-v03 = { package = "stake-cw20", version = "0.2.6",default-features = false } voting-v1 = { package = "dao-voting", version = "0.1.0" ,default-features = false} - +dao-proposal-sudo = { path = "./contracts/test/dao-proposal-sudo"} +dao-voting-snip20-balance ={ path = "./contracts/test/dao-voting-snip20-balance"} secret-toolkit = { version = "0.10.0", default-features = false, features = [ "utils", diff --git a/contracts/dao-dao-core/Cargo.toml b/contracts/dao-dao-core/Cargo.toml index 2698d19..d953c97 100644 --- a/contracts/dao-dao-core/Cargo.toml +++ b/contracts/dao-dao-core/Cargo.toml @@ -39,6 +39,8 @@ cw4 ={ workspace = true } query_auth ={ workspace = true } dao-voting-cw4 = { workspace = true } cw4-group = { workspace = true } +dao-proposal-sudo ={ workspace = true } +dao-voting-snip20-balance ={ workspace = true } [dev-dependencies] diff --git a/contracts/dao-dao-core/src/contract.rs b/contracts/dao-dao-core/src/contract.rs index 61adb88..e827352 100644 --- a/contracts/dao-dao-core/src/contract.rs +++ b/contracts/dao-dao-core/src/contract.rs @@ -2,9 +2,10 @@ use cosmwasm_std::entry_point; use cosmwasm_std::{ from_binary, to_binary, Addr, Binary, CosmosMsg, Deps, DepsMut, Empty, Env, MessageInfo, Reply, - Response, StdError, StdResult, SubMsg, SubMsgResult, + Response, StdError, StdResult, SubMsg, SubMsgResult, Uint128, }; use dao_interface::replies::parse_reply_address_from_event; +use dao_interface::state::AnyContractInfo; use dao_interface::ReplyEvent; use dao_interface::{ msg::{ExecuteMsg, InitialItem, InstantiateMsg, MigrateMsg, QueryMsg, Snip20ReceiveMsg}, @@ -20,7 +21,6 @@ use dao_interface::{ }; use dao_utils::msg::GroupContract; use dao_utils::msg::NftRolesContract; -use dao_utils::query::get_contract_code_hash; use secret_cw2::{get_contract_version, set_contract_version, ContractVersion}; use secret_toolkit::utils::InitCallback; use secret_toolkit::{serialization::Json, storage::Keymap, utils::HandleCallback}; @@ -33,8 +33,8 @@ use snip20_reference_impl::msg::ExecuteAnswer; use crate::query_auth_init::QueryAuthInstantiateMsg; use crate::state::{ ACTIVE_PROPOSAL_MODULE_COUNT, ADMIN, CONFIG, ITEMS, NOMINATED_ADMIN, PAUSED, PROPOSAL_MODULES, - REPLY_IDS, SNIP20_LIST, SNIP721_LIST, SUBDAO_LIST, TOKEN_VIEWING_KEY, - TOTAL_PROPOSAL_MODULE_COUNT, VOTING_MODULE, + QUERY_AUTH, REPLY_IDS, SNIP20_CODE_HASH, SNIP20_LIST, SNIP721_CODE_HASH, SNIP721_LIST, + SUBDAO_LIST, TOKEN_VIEWING_KEY, TOTAL_PROPOSAL_MODULE_COUNT, VOTING_MODULE, }; use crate::{error::ContractError, snip20_msg}; @@ -108,6 +108,8 @@ pub fn instantiate( TOTAL_PROPOSAL_MODULE_COUNT.save(deps.storage, &0)?; ACTIVE_PROPOSAL_MODULE_COUNT.save(deps.storage, &0)?; + SNIP20_CODE_HASH.save(deps.storage, &msg.snip20_code_hash)?; + SNIP721_CODE_HASH.save(deps.storage, &msg.snip721_code_hash)?; Ok(Response::new() .add_attribute("action", "instantiate") @@ -138,7 +140,9 @@ pub fn execute( } ExecuteMsg::Pause { duration } => execute_pause(deps, env, info.sender, duration), ExecuteMsg::Receive(msg) => execute_receive_snip20(deps, info.sender, msg), - ExecuteMsg::ReceiveNft { sender, .. } => execute_receive_snip721(deps, sender), + ExecuteMsg::BatchReceiveNft { sender, from, .. } => { + execute_receive_snip721(deps, sender, from) + } ExecuteMsg::RemoveItem { key } => execute_remove_item(deps, env, info.sender, key), ExecuteMsg::SetItem { key, value } => execute_set_item(deps, env, info.sender, key, value), ExecuteMsg::UpdateConfig { config } => { @@ -458,8 +462,8 @@ pub fn execute_update_snip20_list( let viewing_key = TOKEN_VIEWING_KEY .get(deps.storage, addr) .unwrap_or_default(); - let snip20_code_hash = get_contract_code_hash(deps.querier, addr.to_string())?; - let _info: secret_toolkit::snip20::query::Balance = deps.querier.query_wasm_smart( + let snip20_code_hash = SNIP20_CODE_HASH.load(deps.storage)?; + let _info: snip20_reference_impl::msg::QueryAnswer = deps.querier.query_wasm_smart( snip20_code_hash, addr, &secret_toolkit::snip20::QueryMsg::Balance { @@ -483,7 +487,7 @@ pub fn execute_update_snip721_list( return Err(ContractError::Unauthorized {}); } do_update_addr_list(deps, &SNIP721_LIST, to_add, to_remove, |addr, deps| { - let snip721_code_hash = get_contract_code_hash(deps.querier, addr.clone().to_string())?; + let snip721_code_hash = SNIP721_CODE_HASH.load(deps.storage)?; let _info: secret_toolkit::snip721::query::ContractInfo = deps.querier.query_wasm_smart( snip721_code_hash, addr, @@ -563,7 +567,8 @@ pub fn execute_receive_snip20( sender: Addr, _wrapper: Snip20ReceiveMsg, ) -> Result { - let code_hash = get_contract_code_hash(deps.querier, sender.clone().to_string())?; + println!("here"); + let code_hash = SNIP20_CODE_HASH.load(deps.storage)?; let viewing_key = TOKEN_VIEWING_KEY .get(deps.storage, &sender) .unwrap_or_default(); @@ -596,10 +601,16 @@ pub fn execute_receive_snip20( } } -pub fn execute_receive_snip721(deps: DepsMut, sender: Addr) -> Result { +pub fn execute_receive_snip721( + deps: DepsMut, + sender: Addr, + from: Addr, +) -> Result { SNIP721_LIST.insert(deps.storage, &sender.clone(), &Empty {})?; + println!("sender: {}", sender); + println!("from : {}", from); Ok(Response::new() - .add_attribute("action", "receive_cw721") + .add_attribute("action", "receive_snip721") .add_attribute("token", sender)) } @@ -609,11 +620,13 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { QueryMsg::Admin {} => query_admin(deps), QueryMsg::AdminNomination {} => query_admin_nomination(deps), QueryMsg::Config {} => query_config(deps), - QueryMsg::Cw20TokenList { start_after, limit } => query_cw20_list(deps, start_after, limit), - QueryMsg::Cw20Balances { start_after, limit } => { + QueryMsg::Snip20TokenList { start_after, limit } => { + query_cw20_list(deps, start_after, limit) + } + QueryMsg::Snip20Balances { start_after, limit } => { query_cw20_balances(deps, env, start_after, limit) } - QueryMsg::Cw721TokenList { start_after, limit } => { + QueryMsg::Snip721TokenList { start_after, limit } => { query_cw721_list(deps, start_after, limit) } QueryMsg::DumpState {} => query_dump_state(deps, env), @@ -637,6 +650,7 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { query_list_sub_daos(deps, start_after, limit) } QueryMsg::DaoURI {} => query_dao_uri(deps), + QueryMsg::QueryAuthInfo {} => to_binary(&QUERY_AUTH.load(deps.storage)?), } } @@ -679,32 +693,32 @@ pub fn query_proposal_modules( // Even if this does lock up one can determine the existing // proposal modules by looking at past transactions on chain. - // let data = paginate_map_values( - // deps, - // &PROPOSAL_MODULES, - // 0, - // PROPOSAL_MODULES.get_len(deps.storage).unwrap_or_default(), - // )?; - let mut res: Vec = Vec::new(); let mut start = start_after.clone(); let binding = PROPOSAL_MODULES; let iter = binding.iter(deps.storage)?; + for item in iter { let (address, module) = item?; - if let Some(start_after) = &start { + + // Check if we've reached the start_after item + if let Some(ref start_after) = start { if &address == start_after { - // If we found the start point, reset it to start iterating + // Reset start to None to start collecting results start = None; + continue; // Skip adding this item } + continue; // Skip items until we reach start_after } - if start.is_none() { - res.push(module); - if res.len() >= limit.unwrap_or_default() as usize { - break; // Break out of loop if limit reached - } + + // Once start is None, we can start adding items to the result + res.push(module); + if res.len() >= limit.unwrap_or(usize::MAX as u32) as usize { + // Convert u32 limit to usize + break; // Break out of loop if limit is reached } } + to_binary(&res) } @@ -719,22 +733,28 @@ pub fn query_active_proposal_modules( let mut start = start_after.clone(); let binding = &PROPOSAL_MODULES; let iter = binding.iter(deps.storage)?; + for item in iter { let (address, module) = item?; - if let Some(start_after) = &start { + + // Check if we've reached the start_after item + if let Some(ref start_after) = start { if &address == start_after { - // If we found the start point, reset it to start iterating + // Reset start to None to start collecting results start = None; + continue; // Skip adding this item } + continue; // Skip items until we reach start_after } - if start.is_none() { - res.push(module); - if res.len() >= limit.unwrap_or_default() as usize { - break; // Break out of loop if limit reached - } + + // Once start is None, we can start adding items to the result + res.push(module); + if res.len() >= limit.unwrap_or(usize::MAX as u32) as usize { + break; // Break out of loop if limit is reached } } + // Filter for active modules and apply the limit let limit = limit.unwrap_or(res.len() as u32); to_binary::>( @@ -829,26 +849,48 @@ pub fn query_list_items( start_after: Option, limit: Option, ) -> StdResult { - let mut res: Vec<(String, String)> = Vec::new(); // Vector to hold key-value pairs - let mut start = start_after.clone(); + // Early return for limit zero + if limit == Some(0) { + return to_binary(&Vec::<(String, String)>::new()); + } + + let mut res: Vec<(String, String)> = Vec::new(); let binding = ITEMS; let iter = binding.iter(deps.storage)?; - for item in iter { - let (key, value) = item?; - if let Some(start_after) = &start { - if &key == start_after { - // If we found the start point, reset it to start iterating - start = None; + // Convert the limit to usize, defaulting to MAX if None + let limit_value = limit.unwrap_or(usize::MAX as u32) as usize; + + // Collect all items into a vector + let mut items: Vec<(String, String)> = iter.collect::>>()?; + items.sort_by(|a, b| b.0.cmp(&a.0)); // Sort in descending order + + let mut collecting = false; + + for (key, value) in items { + if let Some(ref start_after_key) = start_after { + // If we find the start_after key, begin collecting afterward + if key == *start_after_key { + collecting = true; + continue; // Skip this key } - } - if start.is_none() { - res.push((key.clone(), value.clone())); // Collect the key-value pair - if res.len() >= limit.unwrap_or_default() as usize { - break; // Break out of loop if limit reached + if !collecting { + continue; // Skip until we find start_after } + } else { + collecting = true; // Collect all if no start_after is provided + } + + // Collect the result + res.push((key.clone(), value.clone())); // Collect the key-value pair + + // Stop if we reached the limit + if res.len() >= limit_value { + break; // Break out of loop if limit reached } } + + // Return the results as binary to_binary(&res) } @@ -861,21 +903,27 @@ pub fn query_cw20_list( let mut start = start_after.clone(); let binding = &SNIP20_LIST; let iter = binding.iter(deps.storage)?; + for item in iter { let (addr, _) = item?; - if let Some(start_after) = &start { + + // Check if we've reached the start_after item + if let Some(ref start_after) = start { if &addr == start_after { - // If we found the start point, reset it to start iterating + // Reset start to None to start collecting results start = None; + continue; // Skip adding this item } + continue; // Skip items until we reach start_after } - if start.is_none() { - res.push(addr); - if res.len() >= limit.unwrap_or_default() as usize { - break; // Break out of loop if limit reached - } + + // Once start is None, we can start adding items to the result + res.push(addr); + if res.len() >= limit.unwrap_or(usize::MAX as u32) as usize { + break; // Break out of loop if limit reached } } + to_binary(&res) } @@ -888,21 +936,27 @@ pub fn query_cw721_list( let mut start = start_after.clone(); let binding = &SNIP721_LIST; let iter = binding.iter(deps.storage)?; + for item in iter { let (addr, _) = item?; - if let Some(start_after) = &start { + + // Check if we've reached the start_after item + if let Some(ref start_after) = start { if &addr == start_after { - // If we found the start point, reset it to start iterating + // Reset start to None to start collecting results start = None; + continue; // Skip adding this item } + continue; // Skip items until we reach start_after } - if start.is_none() { - res.push(addr); - if res.len() >= limit.unwrap_or_default() as usize { - break; // Break out of loop if limit reached - } + + // Once start is None, we can start adding items to the result + res.push(addr); + if res.len() >= limit.unwrap_or(usize::MAX as u32) as usize { + break; // Break out of loop if limit reached } } + to_binary(&res) } @@ -916,29 +970,36 @@ pub fn query_cw20_balances( let mut start = start_after.clone(); let binding = &SNIP20_LIST; let iter = binding.iter(deps.storage)?; + for item in iter { let (addr, _) = item?; - if let Some(start_after) = &start { + + // Check if we've reached the start_after item + if let Some(ref start_after) = start { if &addr == start_after { - // If we found the start point, reset it to start iterating + // Reset start to None to start collecting results start = None; + continue; // Skip adding this item } + continue; // Skip items until we reach start_after } - if start.is_none() { - res.push(addr.to_string()); - if res.len() >= limit.unwrap_or_default() as usize { - break; // Break out of loop if limit reached - } + + // Once start is None, we can start adding items to the result + res.push(addr.to_string()); + if res.len() >= limit.unwrap_or(usize::MAX as u32) as usize { + break; // Break out of loop if limit reached } } - let balances = res + + let balances: StdResult> = res .into_iter() .map(|addr| { - let snip20_code_hash = get_contract_code_hash(deps.querier, addr.clone())?; + let snip20_code_hash = SNIP20_CODE_HASH.load(deps.storage)?; let viewing_key = TOKEN_VIEWING_KEY .get(deps.storage, &deps.api.addr_validate(&addr)?) .unwrap_or_default(); - let balance: secret_toolkit::snip20::query::Balance = deps.querier.query_wasm_smart( + let mut balance_amount = Uint128::zero(); + let balance: snip20_reference_impl::msg::QueryAnswer = deps.querier.query_wasm_smart( snip20_code_hash.clone(), addr.clone(), &snip20_reference_impl::msg::QueryMsg::Balance { @@ -946,13 +1007,20 @@ pub fn query_cw20_balances( key: viewing_key, }, )?; + match balance { + snip20_reference_impl::msg::QueryAnswer::Balance { amount } => { + balance_amount = amount; + } + _ => (), + } Ok(Snip20BalanceResponse { addr, - balance: balance.amount, + balance: balance_amount, }) }) - .collect::>>()?; - to_binary(&balances) + .collect(); + + to_binary(&balances?) } pub fn query_list_sub_daos( @@ -969,19 +1037,24 @@ pub fn query_list_sub_daos( let mut start = start_at.clone(); let binding = &SUBDAO_LIST; let iter = binding.iter(deps.storage)?; + for item in iter { let (addr, subdao) = item?; - if let Some(start_at) = &start { + + // Check if we've reached the start_after item + if let Some(ref start_at) = start { if &addr == start_at { - // If we found the start point, reset it to start iterating + // Reset start to None to start collecting results start = None; + continue; // Skip adding this item } + continue; // Skip items until we reach start_after } - if start.is_none() { - subdaos.push((addr, subdao)); - if subdaos.len() >= limit.unwrap_or_default() as usize { - break; // Break out of loop if limit reached - } + + // Once start is None, we can start adding items to the result + subdaos.push((addr, subdao)); + if subdaos.len() >= limit.unwrap_or(usize::MAX as u32) as usize { + break; // Break out of loop if limit reached } } @@ -1157,6 +1230,14 @@ pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result(&info.msg) { + return msg.to_cosmos_msg(Some(admin), info.label, info.code_id, info.code_hash, None); + } + + //Dao voting snip20 balance + if let Ok(msg) = from_binary::(&info.msg) { + return msg.to_cosmos_msg(Some(admin), info.label, info.code_id, info.code_hash, None); + } + // If none of the types matched, return an error Err(StdError::generic_err( "Failed to deserialize data into any known struct", diff --git a/contracts/dao-dao-core/src/snip20_msg.rs b/contracts/dao-dao-core/src/snip20_msg.rs index ffd8820..a76fefe 100644 --- a/contracts/dao-dao-core/src/snip20_msg.rs +++ b/contracts/dao-dao-core/src/snip20_msg.rs @@ -1,17 +1,11 @@ #![allow(clippy::field_reassign_with_default)] // This is triggered in `#[derive(JsonSchema)]` -use cosmwasm_std::{Binary, Uint128}; +use cosmwasm_std::Binary; +use dao_interface::msg::InitialBalance; use schemars::JsonSchema; use secret_toolkit::utils::{HandleCallback, InitCallback}; use serde::{Deserialize, Serialize}; -#[cfg_attr(test, derive(Eq, PartialEq))] -#[derive(Serialize, Deserialize, Clone, JsonSchema)] -pub struct InitialBalance { - pub address: String, - pub amount: Uint128, -} - #[derive(Serialize, Deserialize, JsonSchema)] pub struct InstantiateMsg { pub name: String, diff --git a/contracts/dao-dao-core/src/state.rs b/contracts/dao-dao-core/src/state.rs index 491f04e..493a11d 100644 --- a/contracts/dao-dao-core/src/state.rs +++ b/contracts/dao-dao-core/src/state.rs @@ -1,4 +1,5 @@ use cosmwasm_std::{Addr, Empty}; +use dao_interface::state::AnyContractInfo; use dao_interface::ReplyIds; use dao_interface::{ query::SubDao, @@ -63,3 +64,8 @@ pub const SUBDAO_LIST: Keymap = Keymap::new(b"sub_daos"); pub const TOKEN_VIEWING_KEY: Keymap = Keymap::new(b"token_viewing_key"); pub const REPLY_IDS: ReplyIds = ReplyIds::new(b"reply_ids", b"reply_ids_count"); + +pub const QUERY_AUTH: Item = Item::new("query_auth"); + +pub const SNIP20_CODE_HASH: Item = Item::new("snip20_code_hash"); +pub const SNIP721_CODE_HASH: Item = Item::new("snip721_code_hash"); diff --git a/contracts/dao-dao-core/src/tests.rs b/contracts/dao-dao-core/src/tests.rs index bff995d..b071f28 100644 --- a/contracts/dao-dao-core/src/tests.rs +++ b/contracts/dao-dao-core/src/tests.rs @@ -1,3159 +1,2972 @@ -// use cosmwasm_schema::cw_serde; -// use cosmwasm_std::{ -// from_binary, -// testing::{mock_dependencies, mock_env}, -// to_binary, Addr, ContractInfo, CosmosMsg, Empty, Storage, Uint128, WasmMsg, -// }; -// use cw4::Member; -// use dao_interface::{ -// msg::{ExecuteMsg, InitialItem, InstantiateMsg, MigrateMsg, QueryMsg}, -// query::{ -// AdminNominationResponse, DaoURIResponse, DumpStateResponse, GetItemResponse, -// PauseInfoResponse, ProposalModuleCountResponse, Snip20BalanceResponse, SubDao, -// }, -// state::{Admin, Config, ModuleInstantiateInfo, ProposalModule, ProposalModuleStatus}, -// voting::{InfoResponse, VotingPowerAtHeightResponse}, -// }; -// use secret_cw2::{set_contract_version, ContractVersion}; -// use secret_multi_test::{next_block, App, Contract, ContractInstantiationInfo, ContractWrapper, Executor}; -// use secret_storage_plus::{Item, Map}; -// use secret_utils::{Duration, Expiration}; -// use snip20_reference_impl::msg::InitConfig; - -// use crate::{ -// contract::{derive_proposal_module_prefix, migrate, CONTRACT_NAME, CONTRACT_VERSION}, -// state::PROPOSAL_MODULES, -// ContractError, -// }; - -// const CREATOR_ADDR: &str = "creator"; - -// fn snip20_contract() -> Box> { -// let contract = ContractWrapper::new( -// snip20_reference_impl::contract::execute, -// snip20_reference_impl::contract::instantiate, -// snip20_reference_impl::contract::query, -// ); -// Box::new(contract) -// } - -// fn snip721_contract() -> Box> { -// let contract = ContractWrapper::new( -// snip20_reference_impl::contract::execute, -// snip20_reference_impl::contract::instantiate, -// snip20_reference_impl::contract::query, -// ); -// Box::new(contract) -// } - -// // fn sudo_proposal_contract() -> Box> { -// // let contract = ContractWrapper::new( -// // dao_proposal_sudo::contract::execute, -// // dao_proposal_sudo::contract::instantiate, -// // dao_proposal_sudo::contract::query, -// // ); -// // Box::new(contract) -// // } - -// // fn cw20_balances_voting() -> Box> { -// // let contract = ContractWrapper::new( -// // dao_voting_cw20_balance::contract::execute, -// // dao_voting_cw20_balance::contract::instantiate, -// // dao_voting_cw20_balance::contract::query, -// // ) -// // .with_reply(dao_voting_cw20_balance::contract::reply); -// // Box::new(contract) -// // } - -// fn cw_core_contract() -> Box> { -// let contract = ContractWrapper::new( -// crate::contract::execute, -// crate::contract::instantiate, -// crate::contract::query, -// ) -// .with_reply(crate::contract::reply) -// .with_migrate(crate::contract::migrate); -// Box::new(contract) -// } - -// fn v1_cw_core_contract() -> Box> { -// use cw_core_v1::contract; -// let contract = ContractWrapper::new( -// crate::contract::execute, -// crate::contract::instantiate, -// crate::contract::query, -// ) -// .with_reply(crate::contract::reply) -// .with_migrate(crate::contract::migrate); -// Box::new(contract) -// } - -// fn query_auth_contract() -> Box> { -// let contract = ContractWrapper::new( -// query_auth::contract::execute, -// query_auth::contract::instantiate, -// query_auth::contract::query, -// ); -// Box::new(contract) -// } - -// fn group_contract() -> Box> { -// let contract = ContractWrapper::new( -// cw4_group::contract::execute, -// cw4_group::contract::instantiate, -// cw4_group::contract::query, -// ); -// Box::new(contract) -// } - -// fn voting_cw4_contract() -> Box> { -// let contract = ContractWrapper::new( -// dao_voting_cw4::contract::execute, -// dao_voting_cw4::contract::instantiate, -// dao_voting_cw4::contract::query, -// ) -// .with_reply(dao_voting_cw4::contract::reply) -// .with_migrate(dao_voting_cw4::contract::migrate); -// Box::new(contract) -// } - -// fn instantiate_gov( -// app: &mut App, -// contract_instantiation_info: ContractInstantiationInfo, -// msg: InstantiateMsg, -// ) -> ContractInfo { -// app.instantiate_contract( -// contract_instantiation_info, -// Addr::unchecked(CREATOR_ADDR), -// &msg, -// &[], -// "cw-governance", -// None, -// ) -// .unwrap() -// } - -// fn instantiate_query_auth( -// app: &mut App, -// contract_instantiation_info: ContractInstantiationInfo, -// msg: InstantiateMsg, -// ) -> ContractInfo { -// app.instantiate_contract( -// contract_instantiation_info, -// Addr::unchecked(CREATOR_ADDR), -// &msg, -// &[], -// "cw-governance", -// None, -// ) -// .unwrap() -// } - -// fn test_instantiate_with_gov_modules() -> ContractInfo { -// let mut app = App::default(); -// let module_info = app.store_code(voting_cw4_contract()); -// let group_contract = app.store_code(group_contract()); -// let gov_info = app.store_code(cw_core_contract()); -// let query_auth_info = app.store_code(query_auth_contract()); -// let module_instantiate = dao_voting_cw4::msg::InstantiateMsg { -// group_contract: dao_voting_cw4::msg::GroupContract::New { -// cw4_group_code_id: group_contract.code_id, -// cw4_group_code_hash: group_contract.code_hash, -// initial_members: vec![Member { -// addr: CREATOR_ADDR.to_string(), -// weight: 1, -// }], -// query_auth: None, -// }, -// dao_code_hash: "dao_code_hash".to_string(), -// }; -// let instantiate = InstantiateMsg { -// dao_uri: None, -// admin: None, -// name: "DAO DAO".to_string(), -// description: "A DAO that builds DAOs.".to_string(), -// image_url: None, -// voting_module_instantiate_info: ModuleInstantiateInfo { -// code_id: module_info.clone().code_id, -// code_hash: module_info.clone().code_hash, -// msg: to_binary(&module_instantiate).unwrap(), -// admin: Some(Admin::CoreModule {}), -// funds: vec![], -// label: "voting module".to_string(), -// }, -// proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { -// code_id: module_info.clone().code_id, -// code_hash: module_info.clone().code_hash, -// msg: to_binary(&module_instantiate).unwrap(), -// admin: Some(Admin::CoreModule {}), -// funds: vec![], -// label: format!("governance module"), -// }], -// initial_items: None, -// query_auth_code_id: query_auth_info.code_id, -// query_auth_code_hash: query_auth_info.code_hash, -// prng_seed: "seed".to_string(), -// }; - -// let gov_contract_info = instantiate_gov(&mut app, gov_info, instantiate); -// // app.update_block(next_block); - - - -// // let state: DumpStateResponse = app -// // .wrap() -// // .query_wasm_smart( -// // gov_contract_info.code_hash.clone(), -// // gov_contract_info.address.clone().to_string(), -// // &QueryMsg::DumpState {}, -// // ) -// // .unwrap(); - -// // assert_eq!( -// // state.config, -// // Config { -// // dao_uri: None, -// // name: "DAO DAO".to_string(), -// // description: "A DAO that builds DAOs.".to_string(), -// // image_url: None, -// // } -// // ); - -// // assert_eq!(state.proposal_modules.len(), 1); - -// // assert_eq!(state.active_proposal_module_count, 1 as u32); - -// // assert_eq!(state.total_proposal_module_count, 1 as u32); - -// gov_contract_info -// } - - -// fn test_instantiate_with_0_gov_modules() { -// let mut app = App::default(); -// let module_info = app.store_code(voting_cw4_contract()); -// let group_contract = app.store_code(group_contract()); -// let gov_info = app.store_code(cw_core_contract()); -// let query_auth_info = app.store_code(query_auth_contract()); -// let module_instantiate = dao_voting_cw4::msg::InstantiateMsg { -// group_contract: dao_voting_cw4::msg::GroupContract::New { -// cw4_group_code_id: group_contract.code_id, -// cw4_group_code_hash: group_contract.code_hash, -// initial_members: vec![Member { -// addr: CREATOR_ADDR.to_string(), -// weight: 1, -// }], -// query_auth: None, -// }, -// dao_code_hash: "dao_code_hash".to_string(), -// }; -// let instantiate = InstantiateMsg { -// dao_uri: None, -// admin: None, -// name: "DAO DAO".to_string(), -// description: "A DAO that builds DAOs.".to_string(), -// image_url: None, -// voting_module_instantiate_info: ModuleInstantiateInfo { -// code_id: module_info.clone().code_id, -// code_hash: module_info.clone().code_hash, -// msg: to_binary(&module_instantiate).unwrap(), -// admin: Some(Admin::CoreModule {}), -// funds: vec![], -// label: "voting module".to_string(), -// }, -// proposal_modules_instantiate_info: vec![], -// initial_items: None, -// query_auth_code_id: query_auth_info.code_id, -// query_auth_code_hash: query_auth_info.code_hash, -// prng_seed: "seed".to_string(), -// }; -// let _ = instantiate_gov(&mut app, gov_info, instantiate); -// } - -// #[test] -// #[should_panic(expected = "Execution would result in no proposal modules being active.")] -// fn test_instantiate_with_zero_gov_modules() { -// test_instantiate_with_0_gov_modules() -// } - -// #[test] -// fn test_valid_instantiate() { -// test_instantiate_with_gov_modules(); -// } - -// #[test] -// fn test_update_config() { -// let mut app = App::default(); -// let module_info = app.store_code(voting_cw4_contract()); -// let group_contract = app.store_code(group_contract()); -// let gov_info = app.store_code(cw_core_contract()); -// let query_auth_info = app.store_code(query_auth_contract()); -// let module_instantiate = dao_voting_cw4::msg::InstantiateMsg { -// group_contract: dao_voting_cw4::msg::GroupContract::New { -// cw4_group_code_id: group_contract.code_id, -// cw4_group_code_hash: group_contract.code_hash, -// initial_members: vec![Member { -// addr: CREATOR_ADDR.to_string(), -// weight: 1, -// }], -// query_auth: None, -// }, -// dao_code_hash: "dao_code_hash".to_string(), -// }; -// let instantiate = InstantiateMsg { -// dao_uri: None, -// admin: None, -// name: "DAO DAO".to_string(), -// description: "A DAO that builds DAOs.".to_string(), -// image_url: None, -// voting_module_instantiate_info: ModuleInstantiateInfo { -// code_id: module_info.clone().code_id, -// code_hash: module_info.clone().code_hash, -// msg: to_binary(&module_instantiate).unwrap(), -// admin: Some(Admin::CoreModule {}), -// funds: vec![], -// label: "voting module".to_string(), -// }, -// proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { -// code_id: module_info.clone().code_id, -// code_hash: module_info.clone().code_hash, -// msg: to_binary(&module_instantiate).unwrap(), -// admin: Some(Admin::CoreModule {}), -// funds: vec![], -// label: format!("governance module"), -// }], -// initial_items: None, -// query_auth_code_id: query_auth_info.code_id, -// query_auth_code_hash: query_auth_info.code_hash, -// prng_seed: "seed".to_string(), -// }; - -// let gov_contract_info = instantiate_gov(&mut app, gov_info, instantiate); -// let modules: Vec = app -// .wrap() -// .query_wasm_smart( -// gov_contract_info.code_hash.clone(), -// gov_contract_info.address.clone(), -// &QueryMsg::ProposalModules { -// start_after: None, -// limit: None, -// }, -// ) -// .unwrap(); - -// assert_eq!(modules.len(), 1); - -// let expected_config = Config { -// dao_uri: None, -// name: "DAO DAO".to_string(), -// description: "A DAO that builds DAOs.".to_string(), -// image_url: None, -// }; - -// let config: Config = app -// .wrap() -// .query_wasm_smart( -// gov_contract_info.code_hash.clone(), -// gov_contract_info.address.clone(), -// &QueryMsg::Config {}, -// ) -// .unwrap(); - -// println!("here"); - -// assert_eq!(expected_config, config); - -// let dao_uri: DaoURIResponse = app -// .wrap() -// .query_wasm_smart( -// gov_contract_info.code_hash, -// gov_contract_info.address, -// &QueryMsg::DaoURI {}, -// ) -// .unwrap(); -// println!("here"); -// assert_eq!(dao_uri.dao_uri, expected_config.dao_uri); -// } - -// // fn test_swap_governance(swaps: Vec<(u32, u32)>) { -// // let mut app = App::default(); -// // let propmod_id = app.store_code(sudo_proposal_contract()); -// // let core_id = app.store_code(cw_core_contract()); - -// // let govmod_instantiate = dao_proposal_sudo::msg::InstantiateMsg { -// // root: CREATOR_ADDR.to_string(), -// // }; - -// // let gov_instantiate = InstantiateMsg { -// // dao_uri: None, -// // admin: None, -// // name: "DAO DAO".to_string(), -// // description: "A DAO that builds DAOs.".to_string(), -// // image_url: None, -// // automatically_add_cw20s: true, -// // automatically_add_cw721s: true, -// // voting_module_instantiate_info: ModuleInstantiateInfo { -// // code_id: propmod_id, -// // msg: to_binary(&govmod_instantiate).unwrap(), -// // admin: Some(Admin::CoreModule {}), -// // funds: vec![], -// // label: "voting module".to_string(), -// // }, -// // proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { -// // code_id: propmod_id, -// // msg: to_binary(&govmod_instantiate).unwrap(), -// // admin: Some(Admin::CoreModule {}), -// // funds: vec![], -// // label: "governance module".to_string(), -// // }], -// // initial_items: None, -// // }; - -// // let gov_addr = app -// // .instantiate_contract( -// // core_id, -// // Addr::unchecked(CREATOR_ADDR), -// // &gov_instantiate, -// // &[], -// // "cw-governance", -// // None, -// // ) -// // .unwrap(); - -// // let modules: Vec = app -// // .wrap() -// // .query_wasm_smart( -// // gov_addr.clone(), -// // &QueryMsg::ProposalModules { -// // start_after: None, -// // limit: None, -// // }, -// // ) -// // .unwrap(); - -// // assert_eq!(modules.len(), 1); - -// // let module_count = query_proposal_module_count(&app, &gov_addr); -// // assert_eq!( -// // module_count, -// // ProposalModuleCountResponse { -// // active_proposal_module_count: 1, -// // total_proposal_module_count: 1, -// // } -// // ); - -// // let (to_add, to_remove) = swaps -// // .iter() -// // .cloned() -// // .reduce(|(to_add, to_remove), (add, remove)| (to_add + add, to_remove + remove)) -// // .unwrap_or((0, 0)); - -// // for (add, remove) in swaps { -// // let start_modules: Vec = app -// // .wrap() -// // .query_wasm_smart( -// // gov_addr.clone(), -// // &QueryMsg::ProposalModules { -// // start_after: None, -// // limit: None, -// // }, -// // ) -// // .unwrap(); - -// // let start_modules_active: Vec = get_active_modules(&app, gov_addr.clone()); - -// // let to_add: Vec<_> = (0..add) -// // .map(|n| ModuleInstantiateInfo { -// // code_id: propmod_id, -// // msg: to_binary(&govmod_instantiate).unwrap(), -// // admin: Some(Admin::CoreModule {}), -// // funds: vec![], -// // label: format!("governance module {n}"), -// // }) -// // .collect(); - -// // let to_disable: Vec<_> = start_modules_active -// // .iter() -// // .rev() -// // .take(remove as usize) -// // .map(|a| a.address.to_string()) -// // .collect(); - -// // app.execute_contract( -// // Addr::unchecked(CREATOR_ADDR), -// // start_modules_active[0].address.clone(), -// // &dao_proposal_sudo::msg::ExecuteMsg::Execute { -// // msgs: vec![WasmMsg::Execute { -// // contract_addr: gov_addr.to_string(), -// // funds: vec![], -// // msg: to_binary(&ExecuteMsg::UpdateProposalModules { to_add, to_disable }) -// // .unwrap(), -// // } -// // .into()], -// // }, -// // &[], -// // ) -// // .unwrap(); - -// // let finish_modules_active = get_active_modules(&app, gov_addr.clone()); - -// // assert_eq!( -// // finish_modules_active.len() as u32, -// // start_modules_active.len() as u32 + add - remove -// // ); -// // for module in start_modules -// // .clone() -// // .into_iter() -// // .rev() -// // .take(remove as usize) -// // { -// // assert!(!finish_modules_active.contains(&module)) -// // } - -// // let state: DumpStateResponse = app -// // .wrap() -// // .query_wasm_smart(gov_addr.clone(), &QueryMsg::DumpState {}) -// // .unwrap(); - -// // assert_eq!( -// // state.active_proposal_module_count, -// // finish_modules_active.len() as u32 -// // ); - -// // assert_eq!( -// // state.total_proposal_module_count, -// // start_modules.len() as u32 + add -// // ) -// // } - -// // let module_count = query_proposal_module_count(&app, &gov_addr); -// // assert_eq!( -// // module_count, -// // ProposalModuleCountResponse { -// // active_proposal_module_count: 1 + to_add - to_remove, -// // total_proposal_module_count: 1 + to_add, -// // } -// // ); -// // } - -// // #[test] -// // fn test_update_governance() { -// // test_swap_governance(vec![(1, 1), (5, 0), (0, 5), (0, 0)]); -// // test_swap_governance(vec![(1, 1), (1, 1), (1, 1), (1, 1)]) -// // } - -// // #[test] -// // fn test_add_then_remove_governance() { -// // test_swap_governance(vec![(1, 0), (0, 1)]) -// // } - -// // #[test] -// // #[should_panic(expected = "Execution would result in no proposal modules being active.")] -// // fn test_swap_governance_bad() { -// // test_swap_governance(vec![(1, 1), (0, 1)]) -// // } - -// // #[test] -// // fn test_removed_modules_can_not_execute() { -// // let mut app = App::default(); -// // let govmod_id = app.store_code(sudo_proposal_contract()); -// // let gov_id = app.store_code(cw_core_contract()); - -// // let govmod_instantiate = dao_proposal_sudo::msg::InstantiateMsg { -// // root: CREATOR_ADDR.to_string(), -// // }; - -// // let gov_instantiate = InstantiateMsg { -// // dao_uri: None, -// // admin: None, -// // name: "DAO DAO".to_string(), -// // description: "A DAO that builds DAOs.".to_string(), -// // image_url: None, -// // automatically_add_cw20s: true, -// // automatically_add_cw721s: true, -// // voting_module_instantiate_info: ModuleInstantiateInfo { -// // code_id: govmod_id, -// // msg: to_binary(&govmod_instantiate).unwrap(), -// // admin: Some(Admin::CoreModule {}), -// // funds: vec![], -// // label: "voting module".to_string(), -// // }, -// // proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { -// // code_id: govmod_id, -// // msg: to_binary(&govmod_instantiate).unwrap(), -// // admin: Some(Admin::CoreModule {}), -// // funds: vec![], -// // label: "governance module".to_string(), -// // }], -// // initial_items: None, -// // }; - -// // let gov_addr = app -// // .instantiate_contract( -// // gov_id, -// // Addr::unchecked(CREATOR_ADDR), -// // &gov_instantiate, -// // &[], -// // "cw-governance", -// // None, -// // ) -// // .unwrap(); - -// // let modules: Vec = app -// // .wrap() -// // .query_wasm_smart( -// // gov_addr.clone(), -// // &QueryMsg::ProposalModules { -// // start_after: None, -// // limit: None, -// // }, -// // ) -// // .unwrap(); - -// // assert_eq!(modules.len(), 1); - -// // let start_module = modules.into_iter().next().unwrap(); - -// // let to_add = vec![ModuleInstantiateInfo { -// // code_id: govmod_id, -// // msg: to_binary(&govmod_instantiate).unwrap(), -// // admin: Some(Admin::CoreModule {}), -// // funds: vec![], -// // label: "new governance module".to_string(), -// // }]; - -// // let to_disable = vec![start_module.address.to_string()]; - -// // // Swap ourselves out. -// // app.execute_contract( -// // Addr::unchecked(CREATOR_ADDR), -// // start_module.address.clone(), -// // &dao_proposal_sudo::msg::ExecuteMsg::Execute { -// // msgs: vec![WasmMsg::Execute { -// // contract_addr: gov_addr.to_string(), -// // funds: vec![], -// // msg: to_binary(&ExecuteMsg::UpdateProposalModules { to_add, to_disable }) -// // .unwrap(), -// // } -// // .into()], -// // }, -// // &[], -// // ) -// // .unwrap(); - -// // let finish_modules_active: Vec = get_active_modules(&app, gov_addr.clone()); - -// // let new_proposal_module = finish_modules_active.into_iter().next().unwrap(); - -// // // Try to add a new module and remove the one we added -// // // earlier. This should fail as we have been removed. -// // let to_add = vec![ModuleInstantiateInfo { -// // code_id: govmod_id, -// // msg: to_binary(&govmod_instantiate).unwrap(), -// // admin: Some(Admin::CoreModule {}), -// // funds: vec![], -// // label: "new governance module".to_string(), -// // }]; -// // let to_disable = vec![new_proposal_module.address.to_string()]; - -// // let err: ContractError = app -// // .execute_contract( -// // Addr::unchecked(CREATOR_ADDR), -// // start_module.address, -// // &dao_proposal_sudo::msg::ExecuteMsg::Execute { -// // msgs: vec![WasmMsg::Execute { -// // contract_addr: gov_addr.to_string(), -// // funds: vec![], -// // msg: to_binary(&ExecuteMsg::UpdateProposalModules { -// // to_add: to_add.clone(), -// // to_disable: to_disable.clone(), -// // }) -// // .unwrap(), -// // } -// // .into()], -// // }, -// // &[], -// // ) -// // .unwrap_err() -// // .downcast() -// // .unwrap(); -// // assert!(matches!( -// // err, -// // ContractError::ModuleDisabledCannotExecute { -// // address: _gov_address -// // } -// // )); - -// // // Check that the enabled query works. -// // let enabled_modules: Vec = app -// // .wrap() -// // .query_wasm_smart( -// // &gov_addr, -// // &QueryMsg::ActiveProposalModules { -// // start_after: None, -// // limit: None, -// // }, -// // ) -// // .unwrap(); - -// // assert_eq!(enabled_modules, vec![new_proposal_module.clone()]); - -// // // The new proposal module should be able to perform actions. -// // app.execute_contract( -// // Addr::unchecked(CREATOR_ADDR), -// // new_proposal_module.address, -// // &dao_proposal_sudo::msg::ExecuteMsg::Execute { -// // msgs: vec![WasmMsg::Execute { -// // contract_addr: gov_addr.to_string(), -// // funds: vec![], -// // msg: to_binary(&ExecuteMsg::UpdateProposalModules { to_add, to_disable }) -// // .unwrap(), -// // } -// // .into()], -// // }, -// // &[], -// // ) -// // .unwrap(); -// // } - -// // #[test] -// // fn test_module_already_disabled() { -// // let mut app = App::default(); -// // let govmod_id = app.store_code(sudo_proposal_contract()); -// // let gov_id = app.store_code(cw_core_contract()); - -// // let govmod_instantiate = dao_proposal_sudo::msg::InstantiateMsg { -// // root: CREATOR_ADDR.to_string(), -// // }; - -// // let gov_instantiate = InstantiateMsg { -// // dao_uri: None, -// // admin: None, -// // name: "DAO DAO".to_string(), -// // description: "A DAO that builds DAOs.".to_string(), -// // image_url: None, -// // automatically_add_cw20s: true, -// // automatically_add_cw721s: true, -// // voting_module_instantiate_info: ModuleInstantiateInfo { -// // code_id: govmod_id, -// // msg: to_binary(&govmod_instantiate).unwrap(), -// // admin: Some(Admin::CoreModule {}), -// // funds: vec![], -// // label: "voting module".to_string(), -// // }, -// // proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { -// // code_id: govmod_id, -// // msg: to_binary(&govmod_instantiate).unwrap(), -// // admin: Some(Admin::CoreModule {}), -// // funds: vec![], -// // label: "governance module".to_string(), -// // }], -// // initial_items: None, -// // }; - -// // let gov_addr = app -// // .instantiate_contract( -// // gov_id, -// // Addr::unchecked(CREATOR_ADDR), -// // &gov_instantiate, -// // &[], -// // "cw-governance", -// // None, -// // ) -// // .unwrap(); - -// // let modules: Vec = app -// // .wrap() -// // .query_wasm_smart( -// // gov_addr.clone(), -// // &QueryMsg::ProposalModules { -// // start_after: None, -// // limit: None, -// // }, -// // ) -// // .unwrap(); - -// // assert_eq!(modules.len(), 1); - -// // let start_module = modules.into_iter().next().unwrap(); - -// // let to_disable = vec![ -// // start_module.address.to_string(), -// // start_module.address.to_string(), -// // ]; - -// // let err: ContractError = app -// // .execute_contract( -// // Addr::unchecked(CREATOR_ADDR), -// // start_module.address.clone(), -// // &dao_proposal_sudo::msg::ExecuteMsg::Execute { -// // msgs: vec![WasmMsg::Execute { -// // contract_addr: gov_addr.to_string(), -// // funds: vec![], -// // msg: to_binary(&ExecuteMsg::UpdateProposalModules { -// // to_add: vec![ModuleInstantiateInfo { -// // code_id: govmod_id, -// // msg: to_binary(&govmod_instantiate).unwrap(), -// // admin: Some(Admin::CoreModule {}), -// // funds: vec![], -// // label: "governance module".to_string(), -// // }], -// // to_disable, -// // }) -// // .unwrap(), -// // } -// // .into()], -// // }, -// // &[], -// // ) -// // .unwrap_err() -// // .downcast() -// // .unwrap(); - -// // assert_eq!( -// // err, -// // ContractError::ModuleAlreadyDisabled { -// // address: start_module.address -// // } -// // ) -// // } - -// // #[test] -// // fn test_swap_voting_module() { -// // let mut app = App::default(); -// // let govmod_id = app.store_code(sudo_proposal_contract()); -// // let gov_id = app.store_code(cw_core_contract()); - -// // let govmod_instantiate = dao_proposal_sudo::msg::InstantiateMsg { -// // root: CREATOR_ADDR.to_string(), -// // }; - -// // let gov_instantiate = InstantiateMsg { -// // dao_uri: None, -// // admin: None, -// // name: "DAO DAO".to_string(), -// // description: "A DAO that builds DAOs.".to_string(), -// // image_url: None, -// // automatically_add_cw20s: true, -// // automatically_add_cw721s: true, -// // voting_module_instantiate_info: ModuleInstantiateInfo { -// // code_id: govmod_id, -// // msg: to_binary(&govmod_instantiate).unwrap(), -// // admin: Some(Admin::CoreModule {}), -// // funds: vec![], -// // label: "voting module".to_string(), -// // }, -// // proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { -// // code_id: govmod_id, -// // msg: to_binary(&govmod_instantiate).unwrap(), -// // admin: Some(Admin::CoreModule {}), -// // funds: vec![], -// // label: "governance module".to_string(), -// // }], -// // initial_items: None, -// // }; - -// // let gov_addr = app -// // .instantiate_contract( -// // gov_id, -// // Addr::unchecked(CREATOR_ADDR), -// // &gov_instantiate, -// // &[], -// // "cw-governance", -// // None, -// // ) -// // .unwrap(); - -// // let voting_addr: Addr = app -// // .wrap() -// // .query_wasm_smart(gov_addr.clone(), &QueryMsg::VotingModule {}) -// // .unwrap(); - -// // let modules: Vec = app -// // .wrap() -// // .query_wasm_smart( -// // gov_addr.clone(), -// // &QueryMsg::ProposalModules { -// // start_after: None, -// // limit: None, -// // }, -// // ) -// // .unwrap(); - -// // assert_eq!(modules.len(), 1); - -// // app.execute_contract( -// // Addr::unchecked(CREATOR_ADDR), -// // modules[0].address.clone(), -// // &dao_proposal_sudo::msg::ExecuteMsg::Execute { -// // msgs: vec![WasmMsg::Execute { -// // contract_addr: gov_addr.to_string(), -// // funds: vec![], -// // msg: to_binary(&ExecuteMsg::UpdateVotingModule { -// // module: ModuleInstantiateInfo { -// // code_id: govmod_id, -// // msg: to_binary(&govmod_instantiate).unwrap(), -// // admin: Some(Admin::CoreModule {}), -// // funds: vec![], -// // label: "voting module".to_string(), -// // }, -// // }) -// // .unwrap(), -// // } -// // .into()], -// // }, -// // &[], -// // ) -// // .unwrap(); - -// // let new_voting_addr: Addr = app -// // .wrap() -// // .query_wasm_smart(gov_addr, &QueryMsg::VotingModule {}) -// // .unwrap(); - -// // assert_ne!(new_voting_addr, voting_addr); -// // } - -// // fn test_unauthorized(app: &mut App, gov_addr: Addr, msg: ExecuteMsg) { -// // let err: ContractError = app -// // .execute_contract(Addr::unchecked(CREATOR_ADDR), gov_addr, &msg, &[]) -// // .unwrap_err() -// // .downcast() -// // .unwrap(); - -// // assert_eq!(err, ContractError::Unauthorized {}); -// // } - -// // #[test] -// // fn test_permissions() { -// // let mut app = App::default(); -// // let govmod_id = app.store_code(sudo_proposal_contract()); -// // let gov_id = app.store_code(cw_core_contract()); - -// // let govmod_instantiate = dao_proposal_sudo::msg::InstantiateMsg { -// // root: CREATOR_ADDR.to_string(), -// // }; - -// // let gov_instantiate = InstantiateMsg { -// // dao_uri: None, -// // admin: None, -// // name: "DAO DAO".to_string(), -// // description: "A DAO that builds DAOs.".to_string(), -// // image_url: None, -// // voting_module_instantiate_info: ModuleInstantiateInfo { -// // code_id: govmod_id, -// // msg: to_binary(&govmod_instantiate).unwrap(), -// // admin: Some(Admin::CoreModule {}), -// // funds: vec![], -// // label: "voting module".to_string(), -// // }, -// // proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { -// // code_id: govmod_id, -// // msg: to_binary(&govmod_instantiate).unwrap(), -// // admin: Some(Admin::CoreModule {}), -// // funds: vec![], -// // label: "governance module".to_string(), -// // }], -// // initial_items: None, -// // automatically_add_cw20s: true, -// // automatically_add_cw721s: true, -// // }; - -// // let gov_addr = app -// // .instantiate_contract( -// // gov_id, -// // Addr::unchecked(CREATOR_ADDR), -// // &gov_instantiate, -// // &[], -// // "cw-governance", -// // None, -// // ) -// // .unwrap(); - -// // test_unauthorized( -// // &mut app, -// // gov_addr.clone(), -// // ExecuteMsg::UpdateVotingModule { -// // module: ModuleInstantiateInfo { -// // code_id: govmod_id, -// // msg: to_binary(&govmod_instantiate).unwrap(), -// // admin: Some(Admin::CoreModule {}), -// // funds: vec![], -// // label: "voting module".to_string(), -// // }, -// // }, -// // ); - -// // test_unauthorized( -// // &mut app, -// // gov_addr.clone(), -// // ExecuteMsg::UpdateProposalModules { -// // to_add: vec![], -// // to_disable: vec![], -// // }, -// // ); - -// // test_unauthorized( -// // &mut app, -// // gov_addr, -// // ExecuteMsg::UpdateConfig { -// // config: Config { -// // dao_uri: None, -// // name: "Evil config.".to_string(), -// // description: "👿".to_string(), -// // image_url: None, -// // automatically_add_cw20s: true, -// // automatically_add_cw721s: true, -// // }, -// // }, -// // ); -// // } - -// // fn do_standard_instantiate(auto_add: bool, admin: Option) -> (Addr, App) { -// // let mut app = App::default(); -// // let govmod_id = app.store_code(sudo_proposal_contract()); -// // let voting_id = app.store_code(cw20_balances_voting()); -// // let gov_id = app.store_code(cw_core_contract()); -// // let cw20_id = app.store_code(cw20_contract()); - -// // let govmod_instantiate = dao_proposal_sudo::msg::InstantiateMsg { -// // root: CREATOR_ADDR.to_string(), -// // }; -// // let voting_instantiate = dao_voting_cw20_balance::msg::InstantiateMsg { -// // token_info: dao_voting_cw20_balance::msg::TokenInfo::New { -// // code_id: cw20_id, -// // label: "DAO DAO voting".to_string(), -// // name: "DAO DAO".to_string(), -// // symbol: "DAO".to_string(), -// // decimals: 6, -// // initial_balances: vec![cw20::Cw20Coin { -// // address: CREATOR_ADDR.to_string(), -// // amount: Uint128::from(2u64), -// // }], -// // marketing: None, -// // }, -// // }; - -// // let gov_instantiate = InstantiateMsg { -// // dao_uri: None, -// // admin, -// // name: "DAO DAO".to_string(), -// // description: "A DAO that builds DAOs.".to_string(), -// // image_url: None, -// // automatically_add_cw20s: auto_add, -// // automatically_add_cw721s: auto_add, -// // voting_module_instantiate_info: ModuleInstantiateInfo { -// // code_id: voting_id, -// // msg: to_binary(&voting_instantiate).unwrap(), -// // admin: Some(Admin::CoreModule {}), -// // funds: vec![], -// // label: "voting module".to_string(), -// // }, -// // proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { -// // code_id: govmod_id, -// // msg: to_binary(&govmod_instantiate).unwrap(), -// // admin: Some(Admin::CoreModule {}), -// // funds: vec![], -// // label: "governance module".to_string(), -// // }], -// // initial_items: None, -// // }; - -// // let gov_addr = app -// // .instantiate_contract( -// // gov_id, -// // Addr::unchecked(CREATOR_ADDR), -// // &gov_instantiate, -// // &[], -// // "cw-governance", -// // None, -// // ) -// // .unwrap(); - -// // (gov_addr, app) -// // } - -// // #[test] -// // fn test_admin_permissions() { -// // let (core_addr, mut app) = do_standard_instantiate(true, None); - -// // let start_height = app.block_info().height; -// // let proposal_modules: Vec = app -// // .wrap() -// // .query_wasm_smart( -// // core_addr.clone(), -// // &QueryMsg::ProposalModules { -// // start_after: None, -// // limit: None, -// // }, -// // ) -// // .unwrap(); - -// // assert_eq!(proposal_modules.len(), 1); -// // let proposal_module = proposal_modules.into_iter().next().unwrap(); - -// // // Random address can't call ExecuteAdminMsgs -// // let res = app.execute_contract( -// // Addr::unchecked("random"), -// // core_addr.clone(), -// // &ExecuteMsg::ExecuteAdminMsgs { -// // msgs: vec![WasmMsg::Execute { -// // contract_addr: core_addr.to_string(), -// // msg: to_binary(&ExecuteMsg::Pause { -// // duration: Duration::Height(10), -// // }) -// // .unwrap(), -// // funds: vec![], -// // } -// // .into()], -// // }, -// // &[], -// // ); -// // res.unwrap_err(); - -// // // Proposal mdoule can't call ExecuteAdminMsgs -// // let res = app.execute_contract( -// // proposal_module.address.clone(), -// // core_addr.clone(), -// // &ExecuteMsg::ExecuteAdminMsgs { -// // msgs: vec![WasmMsg::Execute { -// // contract_addr: core_addr.to_string(), -// // msg: to_binary(&ExecuteMsg::Pause { -// // duration: Duration::Height(10), -// // }) -// // .unwrap(), -// // funds: vec![], -// // } -// // .into()], -// // }, -// // &[], -// // ); -// // res.unwrap_err(); - -// // // Update Admin can't be called by non-admins -// // let res = app.execute_contract( -// // Addr::unchecked("rando"), -// // core_addr.clone(), -// // &ExecuteMsg::NominateAdmin { -// // admin: Some("rando".to_string()), -// // }, -// // &[], -// // ); -// // res.unwrap_err(); - -// // // Nominate admin can be called by core contract as no admin was -// // // specified so the admin defaulted to the core contract. -// // let res = app.execute_contract( -// // proposal_module.address.clone(), -// // core_addr.clone(), -// // &ExecuteMsg::ExecuteProposalHook { -// // msgs: vec![WasmMsg::Execute { -// // contract_addr: core_addr.to_string(), -// // msg: to_binary(&ExecuteMsg::NominateAdmin { -// // admin: Some("meow".to_string()), -// // }) -// // .unwrap(), -// // funds: vec![], -// // } -// // .into()], -// // }, -// // &[], -// // ); -// // res.unwrap(); - -// // // Instantiate new DAO with an admin -// // let (core_with_admin_addr, mut app) = -// // do_standard_instantiate(true, Some(Addr::unchecked("admin").to_string())); - -// // // Non admins still can't call ExecuteAdminMsgs -// // let res = app.execute_contract( -// // proposal_module.address, -// // core_with_admin_addr.clone(), -// // &ExecuteMsg::ExecuteAdminMsgs { -// // msgs: vec![WasmMsg::Execute { -// // contract_addr: core_with_admin_addr.to_string(), -// // msg: to_binary(&ExecuteMsg::Pause { -// // duration: Duration::Height(10), -// // }) -// // .unwrap(), -// // funds: vec![], -// // } -// // .into()], -// // }, -// // &[], -// // ); -// // res.unwrap_err(); - -// // // Admin can call ExecuteAdminMsgs, here an admin pasues the DAO -// // let res = app.execute_contract( -// // Addr::unchecked("admin"), -// // core_with_admin_addr.clone(), -// // &ExecuteMsg::ExecuteAdminMsgs { -// // msgs: vec![WasmMsg::Execute { -// // contract_addr: core_with_admin_addr.to_string(), -// // msg: to_binary(&ExecuteMsg::Pause { -// // duration: Duration::Height(10), -// // }) -// // .unwrap(), -// // funds: vec![], -// // } -// // .into()], -// // }, -// // &[], -// // ); -// // res.unwrap(); - -// // let paused: PauseInfoResponse = app -// // .wrap() -// // .query_wasm_smart(core_with_admin_addr.clone(), &QueryMsg::PauseInfo {}) -// // .unwrap(); -// // assert_eq!( -// // paused, -// // PauseInfoResponse::Paused { -// // expiration: Expiration::AtHeight(start_height + 10) -// // } -// // ); - -// // // DAO unpauses after 10 blocks -// // app.update_block(|block| block.height += 11); - -// // // Admin can nominate a new admin. -// // let res = app.execute_contract( -// // Addr::unchecked("admin"), -// // core_with_admin_addr.clone(), -// // &ExecuteMsg::NominateAdmin { -// // admin: Some("meow".to_string()), -// // }, -// // &[], -// // ); -// // res.unwrap(); - -// // let nomination: AdminNominationResponse = app -// // .wrap() -// // .query_wasm_smart(core_with_admin_addr.clone(), &QueryMsg::AdminNomination {}) -// // .unwrap(); -// // assert_eq!( -// // nomination, -// // AdminNominationResponse { -// // nomination: Some(Addr::unchecked("meow")) -// // } -// // ); - -// // // Check that admin has not yet been updated -// // let res: Addr = app -// // .wrap() -// // .query_wasm_smart(core_with_admin_addr.clone(), &QueryMsg::Admin {}) -// // .unwrap(); -// // assert_eq!(res, Addr::unchecked("admin")); - -// // // Only the nominated address may accept the nomination. -// // let err: ContractError = app -// // .execute_contract( -// // Addr::unchecked("random"), -// // core_with_admin_addr.clone(), -// // &ExecuteMsg::AcceptAdminNomination {}, -// // &[], -// // ) -// // .unwrap_err() -// // .downcast() -// // .unwrap(); -// // assert_eq!(err, ContractError::Unauthorized {}); - -// // // Accept the nomination. -// // app.execute_contract( -// // Addr::unchecked("meow"), -// // core_with_admin_addr.clone(), -// // &ExecuteMsg::AcceptAdminNomination {}, -// // &[], -// // ) -// // .unwrap(); - -// // // Check that admin has been updated -// // let res: Addr = app -// // .wrap() -// // .query_wasm_smart(core_with_admin_addr.clone(), &QueryMsg::Admin {}) -// // .unwrap(); -// // assert_eq!(res, Addr::unchecked("meow")); - -// // // Check that the pending admin has been cleared. -// // let nomination: AdminNominationResponse = app -// // .wrap() -// // .query_wasm_smart(core_with_admin_addr, &QueryMsg::AdminNomination {}) -// // .unwrap(); -// // assert_eq!(nomination, AdminNominationResponse { nomination: None }); -// // } - -// // #[test] -// // fn test_admin_nomination() { -// // let (core_addr, mut app) = do_standard_instantiate(true, Some("admin".to_string())); - -// // // Check that there is no pending nominations. -// // let nomination: AdminNominationResponse = app -// // .wrap() -// // .query_wasm_smart(core_addr.clone(), &QueryMsg::AdminNomination {}) -// // .unwrap(); -// // assert_eq!(nomination, AdminNominationResponse { nomination: None }); - -// // // Nominate a new admin. -// // app.execute_contract( -// // Addr::unchecked("admin"), -// // core_addr.clone(), -// // &ExecuteMsg::NominateAdmin { -// // admin: Some("ekez".to_string()), -// // }, -// // &[], -// // ) -// // .unwrap(); - -// // // Check that the nomination is in place. -// // let nomination: AdminNominationResponse = app -// // .wrap() -// // .query_wasm_smart(core_addr.clone(), &QueryMsg::AdminNomination {}) -// // .unwrap(); -// // assert_eq!( -// // nomination, -// // AdminNominationResponse { -// // nomination: Some(Addr::unchecked("ekez")) -// // } -// // ); - -// // // Non-admin can not withdraw. -// // let err: ContractError = app -// // .execute_contract( -// // Addr::unchecked("ekez"), -// // core_addr.clone(), -// // &ExecuteMsg::WithdrawAdminNomination {}, -// // &[], -// // ) -// // .unwrap_err() -// // .downcast() -// // .unwrap(); -// // assert_eq!(err, ContractError::Unauthorized {}); - -// // // Admin can withdraw. -// // app.execute_contract( -// // Addr::unchecked("admin"), -// // core_addr.clone(), -// // &ExecuteMsg::WithdrawAdminNomination {}, -// // &[], -// // ) -// // .unwrap(); - -// // // Check that the nomination is withdrawn. -// // let nomination: AdminNominationResponse = app -// // .wrap() -// // .query_wasm_smart(core_addr.clone(), &QueryMsg::AdminNomination {}) -// // .unwrap(); -// // assert_eq!(nomination, AdminNominationResponse { nomination: None }); - -// // // Can not withdraw if no nomination is pending. -// // let err: ContractError = app -// // .execute_contract( -// // Addr::unchecked("admin"), -// // core_addr.clone(), -// // &ExecuteMsg::WithdrawAdminNomination {}, -// // &[], -// // ) -// // .unwrap_err() -// // .downcast() -// // .unwrap(); -// // assert_eq!(err, ContractError::NoAdminNomination {}); - -// // // Can not claim nomination b/c it has been withdrawn. -// // let err: ContractError = app -// // .execute_contract( -// // Addr::unchecked("ekez"), -// // core_addr.clone(), -// // &ExecuteMsg::AcceptAdminNomination {}, -// // &[], -// // ) -// // .unwrap_err() -// // .downcast() -// // .unwrap(); -// // assert_eq!(err, ContractError::NoAdminNomination {}); - -// // // Nominate a new admin. -// // app.execute_contract( -// // Addr::unchecked("admin"), -// // core_addr.clone(), -// // &ExecuteMsg::NominateAdmin { -// // admin: Some("meow".to_string()), -// // }, -// // &[], -// // ) -// // .unwrap(); - -// // // A new nomination can not be created if there is already a -// // // pending nomination. -// // let err: ContractError = app -// // .execute_contract( -// // Addr::unchecked("admin"), -// // core_addr.clone(), -// // &ExecuteMsg::NominateAdmin { -// // admin: Some("arthur".to_string()), -// // }, -// // &[], -// // ) -// // .unwrap_err() -// // .downcast() -// // .unwrap(); -// // assert_eq!(err, ContractError::PendingNomination {}); - -// // // Only nominated admin may accept. -// // let err: ContractError = app -// // .execute_contract( -// // Addr::unchecked("ekez"), -// // core_addr.clone(), -// // &ExecuteMsg::AcceptAdminNomination {}, -// // &[], -// // ) -// // .unwrap_err() -// // .downcast() -// // .unwrap(); -// // assert_eq!(err, ContractError::Unauthorized {}); - -// // app.execute_contract( -// // Addr::unchecked("meow"), -// // core_addr.clone(), -// // &ExecuteMsg::AcceptAdminNomination {}, -// // &[], -// // ) -// // .unwrap(); - -// // // Check that meow is the new admin. -// // let admin: Addr = app -// // .wrap() -// // .query_wasm_smart(core_addr.clone(), &QueryMsg::Admin {}) -// // .unwrap(); -// // assert_eq!(admin, Addr::unchecked("meow".to_string())); - -// // let start_height = app.block_info().height; -// // // Check that the new admin can do admin things and the old can not. -// // let err: ContractError = app -// // .execute_contract( -// // Addr::unchecked("admin"), -// // core_addr.clone(), -// // &ExecuteMsg::ExecuteAdminMsgs { -// // msgs: vec![WasmMsg::Execute { -// // contract_addr: core_addr.to_string(), -// // msg: to_binary(&ExecuteMsg::Pause { -// // duration: Duration::Height(10), -// // }) -// // .unwrap(), -// // funds: vec![], -// // } -// // .into()], -// // }, -// // &[], -// // ) -// // .unwrap_err() -// // .downcast() -// // .unwrap(); -// // assert_eq!(err, ContractError::Unauthorized {}); - -// // let res = app.execute_contract( -// // Addr::unchecked("meow"), -// // core_addr.clone(), -// // &ExecuteMsg::ExecuteAdminMsgs { -// // msgs: vec![WasmMsg::Execute { -// // contract_addr: core_addr.to_string(), -// // msg: to_binary(&ExecuteMsg::Pause { -// // duration: Duration::Height(10), -// // }) -// // .unwrap(), -// // funds: vec![], -// // } -// // .into()], -// // }, -// // &[], -// // ); -// // res.unwrap(); - -// // let paused: PauseInfoResponse = app -// // .wrap() -// // .query_wasm_smart(core_addr.clone(), &QueryMsg::PauseInfo {}) -// // .unwrap(); -// // assert_eq!( -// // paused, -// // PauseInfoResponse::Paused { -// // expiration: Expiration::AtHeight(start_height + 10) -// // } -// // ); - -// // // DAO unpauses after 10 blocks -// // app.update_block(|block| block.height += 11); - -// // // Remove the admin. -// // app.execute_contract( -// // Addr::unchecked("meow"), -// // core_addr.clone(), -// // &ExecuteMsg::NominateAdmin { admin: None }, -// // &[], -// // ) -// // .unwrap(); - -// // // Check that this has not caused an admin to be nominated. -// // let nomination: AdminNominationResponse = app -// // .wrap() -// // .query_wasm_smart(core_addr.clone(), &QueryMsg::AdminNomination {}) -// // .unwrap(); -// // assert_eq!(nomination, AdminNominationResponse { nomination: None }); - -// // // Check that admin has been updated. As there was no admin -// // // nominated the admin should revert back to the contract address. -// // let res: Addr = app -// // .wrap() -// // .query_wasm_smart(core_addr.clone(), &QueryMsg::Admin {}) -// // .unwrap(); -// // assert_eq!(res, core_addr); -// // } - -// // #[test] -// // fn test_passthrough_voting_queries() { -// // let (gov_addr, app) = do_standard_instantiate(true, None); - -// // let creator_voting_power: VotingPowerAtHeightResponse = app -// // .wrap() -// // .query_wasm_smart( -// // gov_addr, -// // &QueryMsg::VotingPowerAtHeight { -// // address: CREATOR_ADDR.to_string(), -// // height: None, -// // }, -// // ) -// // .unwrap(); - -// // assert_eq!( -// // creator_voting_power, -// // VotingPowerAtHeightResponse { -// // power: Uint128::from(2u64), -// // height: app.block_info().height, -// // } -// // ); -// // } - -// // fn set_item(app: &mut App, gov_addr: Addr, key: String, value: String) { -// // app.execute_contract( -// // gov_addr.clone(), -// // gov_addr, -// // &ExecuteMsg::SetItem { key, value }, -// // &[], -// // ) -// // .unwrap(); -// // } - -// // fn remove_item(app: &mut App, gov_addr: Addr, key: String) { -// // app.execute_contract( -// // gov_addr.clone(), -// // gov_addr, -// // &ExecuteMsg::RemoveItem { key }, -// // &[], -// // ) -// // .unwrap(); -// // } - -// // fn get_item(app: &mut App, gov_addr: Addr, key: String) -> GetItemResponse { -// // app.wrap() -// // .query_wasm_smart(gov_addr, &QueryMsg::GetItem { key }) -// // .unwrap() -// // } - -// // fn list_items( -// // app: &mut App, -// // gov_addr: Addr, -// // start_at: Option, -// // limit: Option, -// // ) -> Vec<(String, String)> { -// // app.wrap() -// // .query_wasm_smart( -// // gov_addr, -// // &QueryMsg::ListItems { -// // start_after: start_at, -// // limit, -// // }, -// // ) -// // .unwrap() -// // } - -// // #[test] -// // fn test_item_permissions() { -// // let (gov_addr, mut app) = do_standard_instantiate(true, None); - -// // let err: ContractError = app -// // .execute_contract( -// // Addr::unchecked("ekez"), -// // gov_addr.clone(), -// // &ExecuteMsg::SetItem { -// // key: "k".to_string(), -// // value: "v".to_string(), -// // }, -// // &[], -// // ) -// // .unwrap_err() -// // .downcast() -// // .unwrap(); -// // assert_eq!(err, ContractError::Unauthorized {}); - -// // let err: ContractError = app -// // .execute_contract( -// // Addr::unchecked("ekez"), -// // gov_addr, -// // &ExecuteMsg::RemoveItem { -// // key: "k".to_string(), -// // }, -// // &[], -// // ) -// // .unwrap_err() -// // .downcast() -// // .unwrap(); -// // assert_eq!(err, ContractError::Unauthorized {}); -// // } - -// // #[test] -// // fn test_add_remove_get() { -// // let (gov_addr, mut app) = do_standard_instantiate(true, None); - -// // let a = get_item(&mut app, gov_addr.clone(), "aaaaa".to_string()); -// // assert_eq!(a, GetItemResponse { item: None }); - -// // set_item( -// // &mut app, -// // gov_addr.clone(), -// // "aaaaakey".to_string(), -// // "aaaaaaddr".to_string(), -// // ); -// // let a = get_item(&mut app, gov_addr.clone(), "aaaaakey".to_string()); -// // assert_eq!( -// // a, -// // GetItemResponse { -// // item: Some("aaaaaaddr".to_string()) -// // } -// // ); - -// // remove_item(&mut app, gov_addr.clone(), "aaaaakey".to_string()); -// // let a = get_item(&mut app, gov_addr, "aaaaakey".to_string()); -// // assert_eq!(a, GetItemResponse { item: None }); -// // } - -// // #[test] -// // #[should_panic(expected = "Key is missing from storage")] -// // fn test_remove_missing_key() { -// // let (gov_addr, mut app) = do_standard_instantiate(true, None); -// // remove_item(&mut app, gov_addr, "b".to_string()) -// // } - -// // #[test] -// // fn test_list_items() { -// // let mut app = App::default(); -// // let govmod_id = app.store_code(sudo_proposal_contract()); -// // let voting_id = app.store_code(cw20_balances_voting()); -// // let gov_id = app.store_code(cw_core_contract()); -// // let cw20_id = app.store_code(cw20_contract()); - -// // let govmod_instantiate = dao_proposal_sudo::msg::InstantiateMsg { -// // root: CREATOR_ADDR.to_string(), -// // }; -// // let voting_instantiate = dao_voting_cw20_balance::msg::InstantiateMsg { -// // token_info: dao_voting_cw20_balance::msg::TokenInfo::New { -// // code_id: cw20_id, -// // label: "DAO DAO voting".to_string(), -// // name: "DAO DAO".to_string(), -// // symbol: "DAO".to_string(), -// // decimals: 6, -// // initial_balances: vec![cw20::Cw20Coin { -// // address: CREATOR_ADDR.to_string(), -// // amount: Uint128::from(2u64), -// // }], -// // marketing: None, -// // }, -// // }; - -// // let gov_instantiate = InstantiateMsg { -// // dao_uri: None, -// // admin: None, -// // name: "DAO DAO".to_string(), -// // description: "A DAO that builds DAOs.".to_string(), -// // image_url: None, -// // automatically_add_cw20s: true, -// // automatically_add_cw721s: true, -// // voting_module_instantiate_info: ModuleInstantiateInfo { -// // code_id: voting_id, -// // msg: to_binary(&voting_instantiate).unwrap(), -// // admin: Some(Admin::CoreModule {}), -// // funds: vec![], -// // label: "voting module".to_string(), -// // }, -// // proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { -// // code_id: govmod_id, -// // msg: to_binary(&govmod_instantiate).unwrap(), -// // admin: Some(Admin::CoreModule {}), -// // funds: vec![], -// // label: "governance module".to_string(), -// // }], -// // initial_items: None, -// // }; - -// // let gov_addr = app -// // .instantiate_contract( -// // gov_id, -// // Addr::unchecked(CREATOR_ADDR), -// // &gov_instantiate, -// // &[], -// // "cw-governance", -// // None, -// // ) -// // .unwrap(); - -// // set_item( -// // &mut app, -// // gov_addr.clone(), -// // "fookey".to_string(), -// // "fooaddr".to_string(), -// // ); -// // set_item( -// // &mut app, -// // gov_addr.clone(), -// // "barkey".to_string(), -// // "baraddr".to_string(), -// // ); -// // set_item( -// // &mut app, -// // gov_addr.clone(), -// // "loremkey".to_string(), -// // "loremaddr".to_string(), -// // ); -// // set_item( -// // &mut app, -// // gov_addr.clone(), -// // "ipsumkey".to_string(), -// // "ipsumaddr".to_string(), -// // ); - -// // // Foo returned as we are only getting one item and items are in -// // // decending order. -// // let first_item = list_items(&mut app, gov_addr.clone(), None, Some(1)); -// // assert_eq!(first_item.len(), 1); -// // assert_eq!( -// // first_item[0], -// // ("loremkey".to_string(), "loremaddr".to_string()) -// // ); - -// // let no_items = list_items(&mut app, gov_addr.clone(), None, Some(0)); -// // assert_eq!(no_items.len(), 0); - -// // // Items are retreived in decending order so asking for foo with -// // // no limit ought to give us the barkey k/v. this will be the last item -// // // note: the paginate map bound is exclusive, so fookey will be starting point -// // let last_item = list_items(&mut app, gov_addr.clone(), Some("foo".to_string()), None); -// // assert_eq!(last_item.len(), 1); -// // assert_eq!(last_item[0], ("barkey".to_string(), "baraddr".to_string())); - -// // // Items are retreived in decending order so asking for ipsum with -// // // 4 limit ought to give us the fookey and barkey k/vs. -// // let after_foo_list = list_items(&mut app, gov_addr, Some("ipsum".to_string()), Some(4)); -// // assert_eq!(after_foo_list.len(), 2); -// // assert_eq!( -// // after_foo_list, -// // vec![ -// // ("fookey".to_string(), "fooaddr".to_string()), -// // ("barkey".to_string(), "baraddr".to_string()) -// // ] -// // ); -// // } - -// // #[test] -// // fn test_instantiate_with_items() { -// // let mut app = App::default(); -// // let govmod_id = app.store_code(sudo_proposal_contract()); -// // let voting_id = app.store_code(cw20_balances_voting()); -// // let gov_id = app.store_code(cw_core_contract()); -// // let cw20_id = app.store_code(cw20_contract()); - -// // let govmod_instantiate = dao_proposal_sudo::msg::InstantiateMsg { -// // root: CREATOR_ADDR.to_string(), -// // }; -// // let voting_instantiate = dao_voting_cw20_balance::msg::InstantiateMsg { -// // token_info: dao_voting_cw20_balance::msg::TokenInfo::New { -// // code_id: cw20_id, -// // label: "DAO DAO voting".to_string(), -// // name: "DAO DAO".to_string(), -// // symbol: "DAO".to_string(), -// // decimals: 6, -// // initial_balances: vec![cw20::Cw20Coin { -// // address: CREATOR_ADDR.to_string(), -// // amount: Uint128::from(2u64), -// // }], -// // marketing: None, -// // }, -// // }; - -// // let mut initial_items = vec![ -// // InitialItem { -// // key: "item0".to_string(), -// // value: "item0_value".to_string(), -// // }, -// // InitialItem { -// // key: "item1".to_string(), -// // value: "item1_value".to_string(), -// // }, -// // InitialItem { -// // key: "item0".to_string(), -// // value: "item0_value_override".to_string(), -// // }, -// // ]; - -// // let mut gov_instantiate = InstantiateMsg { -// // dao_uri: None, -// // admin: None, -// // name: "DAO DAO".to_string(), -// // description: "A DAO that builds DAOs.".to_string(), -// // image_url: None, -// // automatically_add_cw20s: true, -// // automatically_add_cw721s: true, -// // voting_module_instantiate_info: ModuleInstantiateInfo { -// // code_id: voting_id, -// // msg: to_binary(&voting_instantiate).unwrap(), -// // admin: Some(Admin::CoreModule {}), -// // funds: vec![], -// // label: "voting module".to_string(), -// // }, -// // proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { -// // code_id: govmod_id, -// // msg: to_binary(&govmod_instantiate).unwrap(), -// // admin: Some(Admin::CoreModule {}), -// // funds: vec![], -// // label: "governance module".to_string(), -// // }], -// // initial_items: Some(initial_items.clone()), -// // }; - -// // // Ensure duplicates are dissallowed. -// // let err: ContractError = app -// // .instantiate_contract( -// // gov_id, -// // Addr::unchecked(CREATOR_ADDR), -// // &gov_instantiate, -// // &[], -// // "cw-governance", -// // None, -// // ) -// // .unwrap_err() -// // .downcast() -// // .unwrap(); -// // assert_eq!( -// // err, -// // ContractError::DuplicateInitialItem { -// // item: "item0".to_string() -// // } -// // ); - -// // initial_items.pop(); -// // gov_instantiate.initial_items = Some(initial_items); -// // let gov_addr = app -// // .instantiate_contract( -// // gov_id, -// // Addr::unchecked(CREATOR_ADDR), -// // &gov_instantiate, -// // &[], -// // "cw-governance", -// // None, -// // ) -// // .unwrap(); - -// // // Ensure initial items were added. -// // let items = list_items(&mut app, gov_addr.clone(), None, None); -// // assert_eq!(items.len(), 2); - -// // // Descending order, so item1 is first. -// // assert_eq!(items[1].0, "item0".to_string()); -// // let get_item0 = get_item(&mut app, gov_addr.clone(), "item0".to_string()); -// // assert_eq!( -// // get_item0, -// // GetItemResponse { -// // item: Some("item0_value".to_string()), -// // } -// // ); - -// // assert_eq!(items[0].0, "item1".to_string()); -// // let item1_value = get_item(&mut app, gov_addr, "item1".to_string()).item; -// // assert_eq!(item1_value, Some("item1_value".to_string())) -// // } - -// // #[test] -// // fn test_cw20_receive_auto_add() { -// // let (gov_addr, mut app) = do_standard_instantiate(true, None); - -// // let cw20_id = app.store_code(cw20_contract()); -// // let another_cw20 = app -// // .instantiate_contract( -// // cw20_id, -// // Addr::unchecked(CREATOR_ADDR), -// // &cw20_base::msg::InstantiateMsg { -// // name: "DAO".to_string(), -// // symbol: "DAO".to_string(), -// // decimals: 6, -// // initial_balances: vec![], -// // mint: None, -// // marketing: None, -// // }, -// // &[], -// // "another-token", -// // None, -// // ) -// // .unwrap(); - -// // let voting_module: Addr = app -// // .wrap() -// // .query_wasm_smart(gov_addr.clone(), &QueryMsg::VotingModule {}) -// // .unwrap(); -// // let gov_token: Addr = app -// // .wrap() -// // .query_wasm_smart( -// // voting_module, -// // &dao_interface::voting::Query::TokenContract {}, -// // ) -// // .unwrap(); - -// // // Check that the balances query works with no tokens. -// // let cw20_balances: Vec = app -// // .wrap() -// // .query_wasm_smart( -// // gov_addr.clone(), -// // &QueryMsg::Cw20Balances { -// // start_after: None, -// // limit: None, -// // }, -// // ) -// // .unwrap(); -// // assert_eq!(cw20_balances, vec![]); - -// // // Send a gov token to the governance contract. -// // app.execute_contract( -// // Addr::unchecked(CREATOR_ADDR), -// // gov_token.clone(), -// // &cw20::Cw20ExecuteMsg::Send { -// // contract: gov_addr.to_string(), -// // amount: Uint128::new(1), -// // msg: to_binary(&"").unwrap(), -// // }, -// // &[], -// // ) -// // .unwrap(); - -// // let cw20_list: Vec = app -// // .wrap() -// // .query_wasm_smart( -// // gov_addr.clone(), -// // &QueryMsg::Cw20TokenList { -// // start_after: None, -// // limit: None, -// // }, -// // ) -// // .unwrap(); -// // assert_eq!(cw20_list, vec![gov_token.clone()]); - -// // let cw20_balances: Vec = app -// // .wrap() -// // .query_wasm_smart( -// // gov_addr.clone(), -// // &QueryMsg::Cw20Balances { -// // start_after: None, -// // limit: None, -// // }, -// // ) -// // .unwrap(); -// // assert_eq!( -// // cw20_balances, -// // vec![Cw20BalanceResponse { -// // addr: gov_token.clone(), -// // balance: Uint128::new(1), -// // }] -// // ); - -// // // Test removing and adding some new ones. Invalid should fail. -// // let err: ContractError = app -// // .execute_contract( -// // Addr::unchecked(gov_addr.clone()), -// // gov_addr.clone(), -// // &ExecuteMsg::UpdateCw20List { -// // to_add: vec!["new".to_string()], -// // to_remove: vec![gov_token.to_string()], -// // }, -// // &[], -// // ) -// // .unwrap_err() -// // .downcast() -// // .unwrap(); -// // assert!(matches!(err, ContractError::Std(_))); - -// // // Test that non-DAO can not update the list. -// // let err: ContractError = app -// // .execute_contract( -// // Addr::unchecked("ekez"), -// // gov_addr.clone(), -// // &ExecuteMsg::UpdateCw20List { -// // to_add: vec![], -// // to_remove: vec![gov_token.to_string()], -// // }, -// // &[], -// // ) -// // .unwrap_err() -// // .downcast() -// // .unwrap(); -// // assert!(matches!(err, ContractError::Unauthorized {})); - -// // app.execute_contract( -// // Addr::unchecked(gov_addr.clone()), -// // gov_addr.clone(), -// // &ExecuteMsg::UpdateCw20List { -// // to_add: vec![another_cw20.to_string()], -// // to_remove: vec![gov_token.to_string()], -// // }, -// // &[], -// // ) -// // .unwrap(); - -// // let cw20_list: Vec = app -// // .wrap() -// // .query_wasm_smart( -// // gov_addr, -// // &QueryMsg::Cw20TokenList { -// // start_after: None, -// // limit: None, -// // }, -// // ) -// // .unwrap(); -// // assert_eq!(cw20_list, vec![another_cw20]); -// // } - -// // #[test] -// // fn test_cw20_receive_no_auto_add() { -// // let (gov_addr, mut app) = do_standard_instantiate(false, None); - -// // let cw20_id = app.store_code(cw20_contract()); -// // let another_cw20 = app -// // .instantiate_contract( -// // cw20_id, -// // Addr::unchecked(CREATOR_ADDR), -// // &cw20_base::msg::InstantiateMsg { -// // name: "DAO".to_string(), -// // symbol: "DAO".to_string(), -// // decimals: 6, -// // initial_balances: vec![], -// // mint: None, -// // marketing: None, -// // }, -// // &[], -// // "another-token", -// // None, -// // ) -// // .unwrap(); - -// // let voting_module: Addr = app -// // .wrap() -// // .query_wasm_smart(gov_addr.clone(), &QueryMsg::VotingModule {}) -// // .unwrap(); -// // let gov_token: Addr = app -// // .wrap() -// // .query_wasm_smart( -// // voting_module, -// // &dao_interface::voting::Query::TokenContract {}, -// // ) -// // .unwrap(); - -// // // Send a gov token to the governance contract. Should not be -// // // added becasue auto add is turned off. -// // app.execute_contract( -// // Addr::unchecked(CREATOR_ADDR), -// // gov_token.clone(), -// // &cw20::Cw20ExecuteMsg::Send { -// // contract: gov_addr.to_string(), -// // amount: Uint128::new(1), -// // msg: to_binary(&"").unwrap(), -// // }, -// // &[], -// // ) -// // .unwrap(); - -// // let cw20_list: Vec = app -// // .wrap() -// // .query_wasm_smart( -// // gov_addr.clone(), -// // &QueryMsg::Cw20TokenList { -// // start_after: None, -// // limit: None, -// // }, -// // ) -// // .unwrap(); -// // assert_eq!(cw20_list, Vec::::new()); - -// // app.execute_contract( -// // Addr::unchecked(gov_addr.clone()), -// // gov_addr.clone(), -// // &ExecuteMsg::UpdateCw20List { -// // to_add: vec![another_cw20.to_string(), gov_token.to_string()], -// // to_remove: vec!["ok to remove non existent".to_string()], -// // }, -// // &[], -// // ) -// // .unwrap(); - -// // let cw20_list: Vec = app -// // .wrap() -// // .query_wasm_smart( -// // gov_addr, -// // &QueryMsg::Cw20TokenList { -// // start_after: None, -// // limit: None, -// // }, -// // ) -// // .unwrap(); -// // assert_eq!(cw20_list, vec![another_cw20, gov_token]); -// // } - -// // #[test] -// // fn test_cw721_receive() { -// // let (gov_addr, mut app) = do_standard_instantiate(true, None); - -// // let cw721_id = app.store_code(cw721_contract()); - -// // let cw721_addr = app -// // .instantiate_contract( -// // cw721_id, -// // Addr::unchecked(CREATOR_ADDR), -// // &cw721_base::msg::InstantiateMsg { -// // name: "ekez".to_string(), -// // symbol: "ekez".to_string(), -// // minter: CREATOR_ADDR.to_string(), -// // }, -// // &[], -// // "cw721", -// // None, -// // ) -// // .unwrap(); - -// // let another_cw721 = app -// // .instantiate_contract( -// // cw721_id, -// // Addr::unchecked(CREATOR_ADDR), -// // &cw721_base::msg::InstantiateMsg { -// // name: "ekez".to_string(), -// // symbol: "ekez".to_string(), -// // minter: CREATOR_ADDR.to_string(), -// // }, -// // &[], -// // "cw721", -// // None, -// // ) -// // .unwrap(); - -// // app.execute_contract( -// // Addr::unchecked(CREATOR_ADDR), -// // cw721_addr.clone(), -// // &cw721_base::msg::ExecuteMsg::, Empty>::Mint { -// // token_id: "ekez".to_string(), -// // owner: CREATOR_ADDR.to_string(), -// // token_uri: None, -// // extension: None, -// // }, -// // &[], -// // ) -// // .unwrap(); - -// // app.execute_contract( -// // Addr::unchecked(CREATOR_ADDR), -// // cw721_addr.clone(), -// // &cw721_base::msg::ExecuteMsg::, Empty>::SendNft { -// // contract: gov_addr.to_string(), -// // token_id: "ekez".to_string(), -// // msg: to_binary("").unwrap(), -// // }, -// // &[], -// // ) -// // .unwrap(); - -// // let cw721_list: Vec = app -// // .wrap() -// // .query_wasm_smart( -// // gov_addr.clone(), -// // &QueryMsg::Cw721TokenList { -// // start_after: None, -// // limit: None, -// // }, -// // ) -// // .unwrap(); -// // assert_eq!(cw721_list, vec![cw721_addr.clone()]); - -// // // Try to add an invalid cw721. -// // let err: ContractError = app -// // .execute_contract( -// // Addr::unchecked(gov_addr.clone()), -// // gov_addr.clone(), -// // &ExecuteMsg::UpdateCw721List { -// // to_add: vec!["new".to_string(), cw721_addr.to_string()], -// // to_remove: vec![cw721_addr.to_string()], -// // }, -// // &[], -// // ) -// // .unwrap_err() -// // .downcast() -// // .unwrap(); -// // assert!(matches!(err, ContractError::Std(_))); - -// // // Test that non-DAO can not update the list. -// // let err: ContractError = app -// // .execute_contract( -// // Addr::unchecked("ekez"), -// // gov_addr.clone(), -// // &ExecuteMsg::UpdateCw721List { -// // to_add: vec![], -// // to_remove: vec![cw721_addr.to_string()], -// // }, -// // &[], -// // ) -// // .unwrap_err() -// // .downcast() -// // .unwrap(); -// // assert!(matches!(err, ContractError::Unauthorized {})); - -// // // Add a real cw721. -// // app.execute_contract( -// // Addr::unchecked(gov_addr.clone()), -// // gov_addr.clone(), -// // &ExecuteMsg::UpdateCw721List { -// // to_add: vec![another_cw721.to_string(), cw721_addr.to_string()], -// // to_remove: vec![cw721_addr.to_string()], -// // }, -// // &[], -// // ) -// // .unwrap(); - -// // let cw20_list: Vec = app -// // .wrap() -// // .query_wasm_smart( -// // gov_addr, -// // &QueryMsg::Cw721TokenList { -// // start_after: None, -// // limit: None, -// // }, -// // ) -// // .unwrap(); -// // assert_eq!(cw20_list, vec![another_cw721]); -// // } - -// // #[test] -// // fn test_cw721_receive_no_auto_add() { -// // let (gov_addr, mut app) = do_standard_instantiate(false, None); - -// // let cw721_id = app.store_code(cw721_contract()); - -// // let cw721_addr = app -// // .instantiate_contract( -// // cw721_id, -// // Addr::unchecked(CREATOR_ADDR), -// // &cw721_base::msg::InstantiateMsg { -// // name: "ekez".to_string(), -// // symbol: "ekez".to_string(), -// // minter: CREATOR_ADDR.to_string(), -// // }, -// // &[], -// // "cw721", -// // None, -// // ) -// // .unwrap(); - -// // let another_cw721 = app -// // .instantiate_contract( -// // cw721_id, -// // Addr::unchecked(CREATOR_ADDR), -// // &cw721_base::msg::InstantiateMsg { -// // name: "ekez".to_string(), -// // symbol: "ekez".to_string(), -// // minter: CREATOR_ADDR.to_string(), -// // }, -// // &[], -// // "cw721", -// // None, -// // ) -// // .unwrap(); - -// // app.execute_contract( -// // Addr::unchecked(CREATOR_ADDR), -// // cw721_addr.clone(), -// // &cw721_base::msg::ExecuteMsg::, Empty>::Mint { -// // token_id: "ekez".to_string(), -// // owner: CREATOR_ADDR.to_string(), -// // token_uri: None, -// // extension: None, -// // }, -// // &[], -// // ) -// // .unwrap(); - -// // app.execute_contract( -// // Addr::unchecked(CREATOR_ADDR), -// // cw721_addr.clone(), -// // &cw721_base::msg::ExecuteMsg::, Empty>::SendNft { -// // contract: gov_addr.to_string(), -// // token_id: "ekez".to_string(), -// // msg: to_binary("").unwrap(), -// // }, -// // &[], -// // ) -// // .unwrap(); - -// // let cw721_list: Vec = app -// // .wrap() -// // .query_wasm_smart( -// // gov_addr.clone(), -// // &QueryMsg::Cw721TokenList { -// // start_after: None, -// // limit: None, -// // }, -// // ) -// // .unwrap(); -// // assert_eq!(cw721_list, Vec::::new()); - -// // // Duplicates OK. Just adds one. -// // app.execute_contract( -// // Addr::unchecked(gov_addr.clone()), -// // gov_addr.clone(), -// // &ExecuteMsg::UpdateCw721List { -// // to_add: vec![ -// // another_cw721.to_string(), -// // cw721_addr.to_string(), -// // cw721_addr.to_string(), -// // ], -// // to_remove: vec![], -// // }, -// // &[], -// // ) -// // .unwrap(); - -// // let cw20_list: Vec = app -// // .wrap() -// // .query_wasm_smart( -// // gov_addr, -// // &QueryMsg::Cw721TokenList { -// // start_after: None, -// // limit: None, -// // }, -// // ) -// // .unwrap(); -// // assert_eq!(cw20_list, vec![another_cw721, cw721_addr]); -// // } - -// // #[test] -// // fn test_pause() { -// // let (core_addr, mut app) = do_standard_instantiate(false, None); - -// // let start_height = app.block_info().height; - -// // let proposal_modules: Vec = app -// // .wrap() -// // .query_wasm_smart( -// // core_addr.clone(), -// // &QueryMsg::ProposalModules { -// // start_after: None, -// // limit: None, -// // }, -// // ) -// // .unwrap(); - -// // assert_eq!(proposal_modules.len(), 1); -// // let proposal_module = proposal_modules.into_iter().next().unwrap(); - -// // let paused: PauseInfoResponse = app -// // .wrap() -// // .query_wasm_smart(core_addr.clone(), &QueryMsg::PauseInfo {}) -// // .unwrap(); -// // assert_eq!(paused, PauseInfoResponse::Unpaused {}); -// // let all_state: DumpStateResponse = app -// // .wrap() -// // .query_wasm_smart(core_addr.clone(), &QueryMsg::DumpState {}) -// // .unwrap(); -// // assert_eq!(all_state.pause_info, PauseInfoResponse::Unpaused {}); - -// // // DAO is not paused. Check that we can execute things. -// // // -// // // Tests intentionally use the core address to send these -// // // messsages to simulate a worst case scenerio where the core -// // // contract has a vulnerability. -// // app.execute_contract( -// // core_addr.clone(), -// // core_addr.clone(), -// // &ExecuteMsg::UpdateConfig { -// // config: Config { -// // dao_uri: None, -// // name: "The Empire Strikes Back".to_string(), -// // description: "haha lol we have pwned your DAO".to_string(), -// // image_url: None, -// // automatically_add_cw20s: true, -// // automatically_add_cw721s: true, -// // }, -// // }, -// // &[], -// // ) -// // .unwrap(); - -// // // Oh no the DAO is under attack! Quick! Pause the DAO while we -// // // figure out what to do! -// // let err: ContractError = app -// // .execute_contract( -// // proposal_module.address.clone(), -// // core_addr.clone(), -// // &ExecuteMsg::Pause { -// // duration: Duration::Height(10), -// // }, -// // &[], -// // ) -// // .unwrap_err() -// // .downcast() -// // .unwrap(); - -// // // Only the DAO may call this on itself. Proposal modules must use -// // // the execute hook. -// // assert_eq!(err, ContractError::Unauthorized {}); - -// // app.execute_contract( -// // proposal_module.address.clone(), -// // core_addr.clone(), -// // &ExecuteMsg::ExecuteProposalHook { -// // msgs: vec![WasmMsg::Execute { -// // contract_addr: core_addr.to_string(), -// // msg: to_binary(&ExecuteMsg::Pause { -// // duration: Duration::Height(10), -// // }) -// // .unwrap(), -// // funds: vec![], -// // } -// // .into()], -// // }, -// // &[], -// // ) -// // .unwrap(); - -// // let paused: PauseInfoResponse = app -// // .wrap() -// // .query_wasm_smart(core_addr.clone(), &QueryMsg::PauseInfo {}) -// // .unwrap(); -// // assert_eq!( -// // paused, -// // PauseInfoResponse::Paused { -// // expiration: Expiration::AtHeight(start_height + 10) -// // } -// // ); -// // let all_state: DumpStateResponse = app -// // .wrap() -// // .query_wasm_smart(core_addr.clone(), &QueryMsg::DumpState {}) -// // .unwrap(); -// // assert_eq!( -// // all_state.pause_info, -// // PauseInfoResponse::Paused { -// // expiration: Expiration::AtHeight(start_height + 10) -// // } -// // ); - -// // let err: ContractError = app -// // .execute_contract( -// // core_addr.clone(), -// // core_addr.clone(), -// // &ExecuteMsg::UpdateConfig { -// // config: Config { -// // dao_uri: None, -// // name: "The Empire Strikes Back Again".to_string(), -// // description: "haha lol we have pwned your DAO again".to_string(), -// // image_url: None, -// // automatically_add_cw20s: true, -// // automatically_add_cw721s: true, -// // }, -// // }, -// // &[], -// // ) -// // .unwrap_err() -// // .downcast() -// // .unwrap(); - -// // assert!(matches!(err, ContractError::Paused { .. })); - -// // let err: ContractError = app -// // .execute_contract( -// // proposal_module.address.clone(), -// // core_addr.clone(), -// // &ExecuteMsg::ExecuteProposalHook { -// // msgs: vec![WasmMsg::Execute { -// // contract_addr: core_addr.to_string(), -// // msg: to_binary(&ExecuteMsg::Pause { -// // duration: Duration::Height(10), -// // }) -// // .unwrap(), -// // funds: vec![], -// // } -// // .into()], -// // }, -// // &[], -// // ) -// // .unwrap_err() -// // .downcast() -// // .unwrap(); - -// // assert!(matches!(err, ContractError::Paused { .. })); - -// // app.update_block(|block| block.height += 9); - -// // // Still not unpaused. -// // let err: ContractError = app -// // .execute_contract( -// // proposal_module.address.clone(), -// // core_addr.clone(), -// // &ExecuteMsg::ExecuteProposalHook { -// // msgs: vec![WasmMsg::Execute { -// // contract_addr: core_addr.to_string(), -// // msg: to_binary(&ExecuteMsg::Pause { -// // duration: Duration::Height(10), -// // }) -// // .unwrap(), -// // funds: vec![], -// // } -// // .into()], -// // }, -// // &[], -// // ) -// // .unwrap_err() -// // .downcast() -// // .unwrap(); - -// // assert!(matches!(err, ContractError::Paused { .. })); - -// // app.update_block(|block| block.height += 1); - -// // let paused: PauseInfoResponse = app -// // .wrap() -// // .query_wasm_smart(core_addr.clone(), &QueryMsg::PauseInfo {}) -// // .unwrap(); -// // assert_eq!(paused, PauseInfoResponse::Unpaused {}); -// // let all_state: DumpStateResponse = app -// // .wrap() -// // .query_wasm_smart(core_addr.clone(), &QueryMsg::DumpState {}) -// // .unwrap(); -// // assert_eq!(all_state.pause_info, PauseInfoResponse::Unpaused {}); - -// // // Now its unpaused so we should be able to pause again. -// // app.execute_contract( -// // proposal_module.address, -// // core_addr.clone(), -// // &ExecuteMsg::ExecuteProposalHook { -// // msgs: vec![WasmMsg::Execute { -// // contract_addr: core_addr.to_string(), -// // msg: to_binary(&ExecuteMsg::Pause { -// // duration: Duration::Height(10), -// // }) -// // .unwrap(), -// // funds: vec![], -// // } -// // .into()], -// // }, -// // &[], -// // ) -// // .unwrap(); - -// // let paused: PauseInfoResponse = app -// // .wrap() -// // .query_wasm_smart(core_addr.clone(), &QueryMsg::PauseInfo {}) -// // .unwrap(); -// // assert_eq!( -// // paused, -// // PauseInfoResponse::Paused { -// // expiration: Expiration::AtHeight(start_height + 20) -// // } -// // ); -// // let all_state: DumpStateResponse = app -// // .wrap() -// // .query_wasm_smart(core_addr, &QueryMsg::DumpState {}) -// // .unwrap(); -// // assert_eq!( -// // all_state.pause_info, -// // PauseInfoResponse::Paused { -// // expiration: Expiration::AtHeight(start_height + 20) -// // } -// // ); -// // } - -// // #[test] -// // fn test_dump_state_proposal_modules() { -// // let (core_addr, app) = do_standard_instantiate(false, None); -// // let proposal_modules: Vec = app -// // .wrap() -// // .query_wasm_smart( -// // core_addr.clone(), -// // &QueryMsg::ProposalModules { -// // start_after: None, -// // limit: None, -// // }, -// // ) -// // .unwrap(); - -// // assert_eq!(proposal_modules.len(), 1); -// // let proposal_module = proposal_modules.into_iter().next().unwrap(); - -// // let all_state: DumpStateResponse = app -// // .wrap() -// // .query_wasm_smart(core_addr, &QueryMsg::DumpState {}) -// // .unwrap(); -// // assert_eq!(all_state.pause_info, PauseInfoResponse::Unpaused {}); -// // assert_eq!(all_state.proposal_modules.len(), 1); -// // assert_eq!(all_state.proposal_modules[0], proposal_module); -// // } - -// // // Note that this isn't actually testing that we are migrating from the previous version since -// // // with multitest contract instantiation we can't manipulate storage to the previous version of state before invoking migrate. So if anything, -// // // this just tests the idempotency of migrate. -// // #[test] -// // fn test_migrate_from_compatible() { -// // let mut app = App::default(); -// // let govmod_id = app.store_code(sudo_proposal_contract()); -// // let voting_id = app.store_code(cw20_balances_voting()); -// // let gov_id = app.store_code(cw_core_contract()); -// // let cw20_id = app.store_code(cw20_contract()); - -// // let govmod_instantiate = dao_proposal_sudo::msg::InstantiateMsg { -// // root: CREATOR_ADDR.to_string(), -// // }; -// // let voting_instantiate = dao_voting_cw20_balance::msg::InstantiateMsg { -// // token_info: dao_voting_cw20_balance::msg::TokenInfo::New { -// // code_id: cw20_id, -// // label: "DAO DAO voting".to_string(), -// // name: "DAO DAO".to_string(), -// // symbol: "DAO".to_string(), -// // decimals: 6, -// // initial_balances: vec![cw20::Cw20Coin { -// // address: CREATOR_ADDR.to_string(), -// // amount: Uint128::from(2u64), -// // }], -// // marketing: None, -// // }, -// // }; - -// // // Instantiate the core module with an admin to do migrations. -// // let gov_instantiate = InstantiateMsg { -// // dao_uri: None, -// // admin: None, -// // name: "DAO DAO".to_string(), -// // description: "A DAO that builds DAOs.".to_string(), -// // image_url: None, -// // automatically_add_cw20s: false, -// // automatically_add_cw721s: false, -// // voting_module_instantiate_info: ModuleInstantiateInfo { -// // code_id: voting_id, -// // msg: to_binary(&voting_instantiate).unwrap(), -// // admin: Some(Admin::CoreModule {}), -// // funds: vec![], -// // label: "voting module".to_string(), -// // }, -// // proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { -// // code_id: govmod_id, -// // msg: to_binary(&govmod_instantiate).unwrap(), -// // admin: Some(Admin::CoreModule {}), -// // funds: vec![], -// // label: "governance module".to_string(), -// // }], -// // initial_items: None, -// // }; - -// // let core_addr = app -// // .instantiate_contract( -// // gov_id, -// // Addr::unchecked(CREATOR_ADDR), -// // &gov_instantiate, -// // &[], -// // "cw-governance", -// // Some(CREATOR_ADDR.to_string()), -// // ) -// // .unwrap(); - -// // let state: DumpStateResponse = app -// // .wrap() -// // .query_wasm_smart(core_addr.clone(), &QueryMsg::DumpState {}) -// // .unwrap(); - -// // app.execute( -// // Addr::unchecked(CREATOR_ADDR), -// // CosmosMsg::Wasm(WasmMsg::Migrate { -// // contract_addr: core_addr.to_string(), -// // new_code_id: gov_id, -// // msg: to_binary(&MigrateMsg::FromCompatible {}).unwrap(), -// // }), -// // ) -// // .unwrap(); - -// // let new_state: DumpStateResponse = app -// // .wrap() -// // .query_wasm_smart(core_addr, &QueryMsg::DumpState {}) -// // .unwrap(); - -// // assert_eq!(new_state, state); -// // } - -// // #[test] -// // fn test_migrate_from_beta() { -// // use cw_core_v1 as v1; - -// // let mut app = App::default(); -// // let govmod_id = app.store_code(sudo_proposal_contract()); -// // let voting_id = app.store_code(cw20_balances_voting()); -// // let core_id = app.store_code(cw_core_contract()); -// // let v1_core_id = app.store_code(v1_cw_core_contract()); -// // let cw20_id = app.store_code(cw20_contract()); - -// // let proposal_instantiate = dao_proposal_sudo::msg::InstantiateMsg { -// // root: CREATOR_ADDR.to_string(), -// // }; -// // let voting_instantiate = dao_voting_cw20_balance::msg::InstantiateMsg { -// // token_info: dao_voting_cw20_balance::msg::TokenInfo::New { -// // code_id: cw20_id, -// // label: "DAO DAO voting".to_string(), -// // name: "DAO DAO".to_string(), -// // symbol: "DAO".to_string(), -// // decimals: 6, -// // initial_balances: vec![cw20::Cw20Coin { -// // address: CREATOR_ADDR.to_string(), -// // amount: Uint128::from(2u64), -// // }], -// // marketing: None, -// // }, -// // }; - -// // // Instantiate the core module with an admin to do migrations. -// // let v1_core_instantiate = v1::msg::InstantiateMsg { -// // admin: None, -// // name: "DAO DAO".to_string(), -// // description: "A DAO that builds DAOs.".to_string(), -// // image_url: None, -// // automatically_add_cw20s: false, -// // automatically_add_cw721s: false, -// // voting_module_instantiate_info: v1::msg::ModuleInstantiateInfo { -// // code_id: voting_id, -// // msg: to_binary(&voting_instantiate).unwrap(), -// // admin: v1::msg::Admin::CoreContract {}, -// // label: "voting module".to_string(), -// // }, -// // proposal_modules_instantiate_info: vec![ -// // v1::msg::ModuleInstantiateInfo { -// // code_id: govmod_id, -// // msg: to_binary(&proposal_instantiate).unwrap(), -// // admin: v1::msg::Admin::CoreContract {}, -// // label: "governance module 1".to_string(), -// // }, -// // v1::msg::ModuleInstantiateInfo { -// // code_id: govmod_id, -// // msg: to_binary(&proposal_instantiate).unwrap(), -// // admin: v1::msg::Admin::CoreContract {}, -// // label: "governance module 2".to_string(), -// // }, -// // ], -// // initial_items: None, -// // }; - -// // let core_addr = app -// // .instantiate_contract( -// // v1_core_id, -// // Addr::unchecked(CREATOR_ADDR), -// // &v1_core_instantiate, -// // &[], -// // "cw-governance", -// // Some(CREATOR_ADDR.to_string()), -// // ) -// // .unwrap(); - -// // app.execute( -// // Addr::unchecked(CREATOR_ADDR), -// // CosmosMsg::Wasm(WasmMsg::Migrate { -// // contract_addr: core_addr.to_string(), -// // new_code_id: core_id, -// // msg: to_binary(&MigrateMsg::FromV1 { -// // dao_uri: None, -// // params: None, -// // }) -// // .unwrap(), -// // }), -// // ) -// // .unwrap(); - -// // let new_state: DumpStateResponse = app -// // .wrap() -// // .query_wasm_smart(&core_addr, &QueryMsg::DumpState {}) -// // .unwrap(); - -// // let proposal_modules = new_state.proposal_modules; -// // assert_eq!(2, proposal_modules.len()); -// // for (idx, module) in proposal_modules.iter().enumerate() { -// // let prefix = derive_proposal_module_prefix(idx).unwrap(); -// // assert_eq!(prefix, module.prefix); -// // assert_eq!(ProposalModuleStatus::Enabled, module.status); -// // } - -// // // Check that we may not migrate more than once. -// // let err: ContractError = app -// // .execute( -// // Addr::unchecked(CREATOR_ADDR), -// // CosmosMsg::Wasm(WasmMsg::Migrate { -// // contract_addr: core_addr.to_string(), -// // new_code_id: core_id, -// // msg: to_binary(&MigrateMsg::FromV1 { -// // dao_uri: None, -// // params: None, -// // }) -// // .unwrap(), -// // }), -// // ) -// // .unwrap_err() -// // .downcast() -// // .unwrap(); -// // assert_eq!(err, ContractError::AlreadyMigrated {}) -// // } - -// // #[test] -// // fn test_migrate_mock() { -// // let mut deps = mock_dependencies(); -// // let dao_uri: String = "/dao/uri".to_string(); -// // let msg = MigrateMsg::FromV1 { -// // dao_uri: Some(dao_uri.clone()), -// // params: None, -// // }; -// // let env = mock_env(); - -// // // Set starting version to v1. -// // set_contract_version(&mut deps.storage, CONTRACT_NAME, "0.1.0").unwrap(); - -// // // Write to storage in old proposal module format -// // let proposal_modules_key = Addr::unchecked("addr"); -// // let old_map: Map = Map::new("proposal_modules"); -// // let path = old_map.key(proposal_modules_key.clone()); -// // deps.storage.set(&path, &to_binary(&Empty {}).unwrap()); - -// // // Write to storage in old config format -// // #[cw_serde] -// // struct V1Config { -// // pub name: String, -// // pub description: String, -// // pub image_url: Option, -// // pub automatically_add_cw20s: bool, -// // pub automatically_add_cw721s: bool, -// // } - -// // let v1_config = V1Config { -// // name: "core dao".to_string(), -// // description: "a dao".to_string(), -// // image_url: None, -// // automatically_add_cw20s: false, -// // automatically_add_cw721s: false, -// // }; - -// // let config_item: Item = Item::new("config"); -// // config_item.save(&mut deps.storage, &v1_config).unwrap(); - -// // // Migrate to v2 -// // migrate(deps.as_mut(), env, msg).unwrap(); - -// // let new_path = PROPOSAL_MODULES.key(proposal_modules_key); -// // let prop_module_bytes = deps.storage.get(&new_path).unwrap(); -// // let module: ProposalModule = from_binary(prop_module_bytes).unwrap(); -// // assert_eq!(module.address, Addr::unchecked("addr")); -// // assert_eq!(module.prefix, derive_proposal_module_prefix(0).unwrap()); -// // assert_eq!(module.status, ProposalModuleStatus::Enabled {}); - -// // let v2_config_item: Item = Item::new("config_v2"); -// // let v2_config = v2_config_item.load(&deps.storage).unwrap(); -// // assert_eq!(v2_config.dao_uri, Some(dao_uri)); -// // assert_eq!(v2_config.name, v1_config.name); -// // assert_eq!(v2_config.description, v1_config.description); -// // assert_eq!(v2_config.image_url, v1_config.image_url); -// // assert_eq!( -// // v2_config.automatically_add_cw20s, -// // v1_config.automatically_add_cw20s -// // ); -// // assert_eq!( -// // v2_config.automatically_add_cw721s, -// // v1_config.automatically_add_cw721s -// // ) -// // } - -// // #[test] -// // fn test_execute_stargate_msg() { -// // let (core_addr, mut app) = do_standard_instantiate(true, None); -// // let proposal_modules: Vec = app -// // .wrap() -// // .query_wasm_smart( -// // core_addr.clone(), -// // &QueryMsg::ProposalModules { -// // start_after: None, -// // limit: None, -// // }, -// // ) -// // .unwrap(); - -// // assert_eq!(proposal_modules.len(), 1); -// // let proposal_module = proposal_modules.into_iter().next().unwrap(); - -// // let res = app.execute_contract( -// // proposal_module.address, -// // core_addr, -// // &ExecuteMsg::ExecuteProposalHook { -// // msgs: vec![CosmosMsg::Stargate { -// // type_url: "foo_type".to_string(), -// // value: to_binary("foo_bin").unwrap(), -// // }], -// // }, -// // &[], -// // ); -// // // TODO: Once cw-multi-test supports executing stargate/ibc messages we can change this test assert -// // assert!(res.is_err()); -// // } - -// // #[test] -// // fn test_module_prefixes() { -// // let mut app = App::default(); -// // let govmod_id = app.store_code(sudo_proposal_contract()); -// // let gov_id = app.store_code(cw_core_contract()); - -// // let govmod_instantiate = dao_proposal_sudo::msg::InstantiateMsg { -// // root: CREATOR_ADDR.to_string(), -// // }; - -// // let gov_instantiate = InstantiateMsg { -// // dao_uri: None, -// // admin: None, -// // name: "DAO DAO".to_string(), -// // description: "A DAO that builds DAOs.".to_string(), -// // image_url: None, -// // automatically_add_cw20s: true, -// // automatically_add_cw721s: true, -// // voting_module_instantiate_info: ModuleInstantiateInfo { -// // code_id: govmod_id, -// // msg: to_binary(&govmod_instantiate).unwrap(), -// // admin: Some(Admin::CoreModule {}), -// // funds: vec![], -// // label: "voting module".to_string(), -// // }, -// // proposal_modules_instantiate_info: vec![ -// // ModuleInstantiateInfo { -// // code_id: govmod_id, -// // msg: to_binary(&govmod_instantiate).unwrap(), -// // admin: Some(Admin::CoreModule {}), -// // funds: vec![], -// // label: "proposal module 1".to_string(), -// // }, -// // ModuleInstantiateInfo { -// // code_id: govmod_id, -// // msg: to_binary(&govmod_instantiate).unwrap(), -// // admin: Some(Admin::CoreModule {}), -// // funds: vec![], -// // label: "proposal module 2".to_string(), -// // }, -// // ModuleInstantiateInfo { -// // code_id: govmod_id, -// // msg: to_binary(&govmod_instantiate).unwrap(), -// // admin: Some(Admin::CoreModule {}), -// // funds: vec![], -// // label: "proposal module 2".to_string(), -// // }, -// // ], -// // initial_items: None, -// // }; - -// // let gov_addr = app -// // .instantiate_contract( -// // gov_id, -// // Addr::unchecked(CREATOR_ADDR), -// // &gov_instantiate, -// // &[], -// // "cw-governance", -// // None, -// // ) -// // .unwrap(); - -// // let modules: Vec = app -// // .wrap() -// // .query_wasm_smart( -// // gov_addr, -// // &QueryMsg::ProposalModules { -// // start_after: None, -// // limit: None, -// // }, -// // ) -// // .unwrap(); - -// // assert_eq!(modules.len(), 3); - -// // let module_1 = &modules[0]; -// // assert_eq!(module_1.status, ProposalModuleStatus::Enabled {}); -// // assert_eq!(module_1.prefix, "A"); -// // assert_eq!(&module_1.address, &modules[0].address); - -// // let module_2 = &modules[1]; -// // assert_eq!(module_2.status, ProposalModuleStatus::Enabled {}); -// // assert_eq!(module_2.prefix, "B"); -// // assert_eq!(&module_2.address, &modules[1].address); - -// // let module_3 = &modules[2]; -// // assert_eq!(module_3.status, ProposalModuleStatus::Enabled {}); -// // assert_eq!(module_3.prefix, "C"); -// // assert_eq!(&module_3.address, &modules[2].address); -// // } - -// // fn get_active_modules(app: &App, gov_addr: Addr) -> Vec { -// // let modules: Vec = app -// // .wrap() -// // .query_wasm_smart( -// // gov_addr, -// // &QueryMsg::ProposalModules { -// // start_after: None, -// // limit: None, -// // }, -// // ) -// // .unwrap(); - -// // modules -// // .into_iter() -// // .filter(|module: &ProposalModule| module.status == ProposalModuleStatus::Enabled) -// // .collect() -// // } - -// // fn query_proposal_module_count(app: &App, core_addr: &Addr) -> ProposalModuleCountResponse { -// // app.wrap() -// // .query_wasm_smart(core_addr, &QueryMsg::ProposalModuleCount {}) -// // .unwrap() -// // } - -// // #[test] -// // fn test_add_remove_subdaos() { -// // let (core_addr, mut app) = do_standard_instantiate(false, None); - -// // test_unauthorized( -// // &mut app, -// // core_addr.clone(), -// // ExecuteMsg::UpdateSubDaos { -// // to_add: vec![], -// // to_remove: vec![], -// // }, -// // ); - -// // let to_add: Vec = vec![ -// // SubDao { -// // addr: "subdao001".to_string(), -// // charter: None, -// // }, -// // SubDao { -// // addr: "subdao002".to_string(), -// // charter: Some("cool charter bro".to_string()), -// // }, -// // SubDao { -// // addr: "subdao005".to_string(), -// // charter: None, -// // }, -// // SubDao { -// // addr: "subdao007".to_string(), -// // charter: None, -// // }, -// // ]; -// // let to_remove: Vec = vec![]; - -// // app.execute_contract( -// // Addr::unchecked(core_addr.clone()), -// // core_addr.clone(), -// // &ExecuteMsg::UpdateSubDaos { to_add, to_remove }, -// // &[], -// // ) -// // .unwrap(); - -// // let res: Vec = app -// // .wrap() -// // .query_wasm_smart( -// // core_addr.clone(), -// // &QueryMsg::ListSubDaos { -// // start_after: None, -// // limit: None, -// // }, -// // ) -// // .unwrap(); - -// // assert_eq!(res.len(), 4); - -// // let to_remove: Vec = vec!["subdao005".to_string()]; - -// // app.execute_contract( -// // Addr::unchecked(core_addr.clone()), -// // core_addr.clone(), -// // &ExecuteMsg::UpdateSubDaos { -// // to_add: vec![], -// // to_remove, -// // }, -// // &[], -// // ) -// // .unwrap(); - -// // let res: Vec = app -// // .wrap() -// // .query_wasm_smart( -// // core_addr, -// // &QueryMsg::ListSubDaos { -// // start_after: None, -// // limit: None, -// // }, -// // ) -// // .unwrap(); - -// // assert_eq!(res.len(), 3); - -// // let test_res: SubDao = SubDao { -// // addr: "subdao002".to_string(), -// // charter: Some("cool charter bro".to_string()), -// // }; - -// // assert_eq!(res[1], test_res); - -// // let full_result_set: Vec = vec![ -// // SubDao { -// // addr: "subdao001".to_string(), -// // charter: None, -// // }, -// // SubDao { -// // addr: "subdao002".to_string(), -// // charter: Some("cool charter bro".to_string()), -// // }, -// // SubDao { -// // addr: "subdao007".to_string(), -// // charter: None, -// // }, -// // ]; - -// // assert_eq!(res, full_result_set); -// // } - -// // #[test] -// // pub fn test_migrate_update_version() { -// // let mut deps = mock_dependencies(); -// // cw2::set_contract_version(&mut deps.storage, "my-contract", "old-version").unwrap(); -// // migrate(deps.as_mut(), mock_env(), MigrateMsg::FromCompatible {}).unwrap(); -// // let version = cw2::get_contract_version(&deps.storage).unwrap(); -// // assert_eq!(version.version, CONTRACT_VERSION); -// // assert_eq!(version.contract, CONTRACT_NAME); -// // } - -// // #[test] -// // fn test_query_info() { -// // let (core_addr, app) = do_standard_instantiate(true, None); -// // let res: InfoResponse = app -// // .wrap() -// // .query_wasm_smart(core_addr, &QueryMsg::Info {}) -// // .unwrap(); -// // assert_eq!( -// // res, -// // InfoResponse { -// // info: ContractVersion { -// // contract: CONTRACT_NAME.to_string(), -// // version: CONTRACT_VERSION.to_string() -// // } -// // } -// // ) -// // } +use cosmwasm_std::{ + from_binary, + testing::{mock_dependencies, mock_env, mock_info}, + to_binary, Addr, ContractInfo, CosmosMsg, Empty, MessageInfo, Uint128, WasmMsg, +}; +use cw4::Member; +use dao_interface::{ + msg::{ExecuteMsg, InitialItem, InstantiateMsg, MigrateMsg, QueryMsg}, + query::{ + AdminNominationResponse, DaoURIResponse, DumpStateResponse, GetItemResponse, + PauseInfoResponse, ProposalModuleCountResponse, Snip20BalanceResponse, SubDao, + }, + state::{ + Admin, AnyContractInfo, Config, ModuleInstantiateInfo, ProposalModule, + ProposalModuleStatus, VotingModuleInfo, + }, + voting::{InfoResponse, VotingPowerAtHeightResponse}, +}; +use secret_cw2::ContractVersion; +use secret_multi_test::{ + next_block, App, Contract, ContractInstantiationInfo, ContractWrapper, Executor, +}; +use secret_utils::{Duration, Expiration}; +use snip20_reference_impl::msg::InitialBalance; +use snip721_reference_impl::msg::ReceiverInfo; + +use crate::{ + contract::{migrate, CONTRACT_NAME, CONTRACT_VERSION}, + ContractError, +}; + +const CREATOR_ADDR: &str = "creator"; + +fn snip20_contract() -> Box> { + let contract = ContractWrapper::new( + snip20_reference_impl::contract::execute, + snip20_reference_impl::contract::instantiate, + snip20_reference_impl::contract::query, + ); + Box::new(contract) +} + +fn snip721_contract() -> Box> { + let contract = ContractWrapper::new( + snip721_reference_impl::contract::execute, + snip721_reference_impl::contract::instantiate, + snip721_reference_impl::contract::query, + ); + Box::new(contract) +} + +fn sudo_proposal_contract() -> Box> { + let contract = ContractWrapper::new( + dao_proposal_sudo::contract::execute, + dao_proposal_sudo::contract::instantiate, + dao_proposal_sudo::contract::query, + ); + Box::new(contract) +} + +fn snip20_balances_voting() -> Box> { + let contract = ContractWrapper::new( + dao_voting_snip20_balance::contract::execute, + dao_voting_snip20_balance::contract::instantiate, + dao_voting_snip20_balance::contract::query, + ) + .with_reply(dao_voting_snip20_balance::contract::reply); + Box::new(contract) +} + +fn cw_core_contract() -> Box> { + let contract = ContractWrapper::new( + crate::contract::execute, + crate::contract::instantiate, + crate::contract::query, + ) + .with_reply(crate::contract::reply) + .with_migrate(crate::contract::migrate); + Box::new(contract) +} + +fn query_auth_contract() -> Box> { + let contract = ContractWrapper::new( + query_auth::contract::execute, + query_auth::contract::instantiate, + query_auth::contract::query, + ); + Box::new(contract) +} + +fn group_contract() -> Box> { + let contract = ContractWrapper::new( + cw4_group::contract::execute, + cw4_group::contract::instantiate, + cw4_group::contract::query, + ); + Box::new(contract) +} + +fn voting_cw4_contract() -> Box> { + let contract = ContractWrapper::new( + dao_voting_cw4::contract::execute, + dao_voting_cw4::contract::instantiate, + dao_voting_cw4::contract::query, + ) + .with_reply(dao_voting_cw4::contract::reply) + .with_migrate(dao_voting_cw4::contract::migrate); + Box::new(contract) +} + +fn instantiate_gov( + app: &mut App, + contract_instantiation_info: ContractInstantiationInfo, + msg: InstantiateMsg, +) -> ContractInfo { + app.instantiate_contract( + contract_instantiation_info, + Addr::unchecked(CREATOR_ADDR), + &msg, + &[], + "cw-governance", + None, + ) + .unwrap() +} + +fn create_token_viewing_key( + app: &mut App, + contract_info: ContractInfo, + info: MessageInfo, +) -> String { + let msg = snip20_reference_impl::msg::ExecuteMsg::CreateViewingKey { + entropy: "entropy".to_string(), + padding: None, + }; + let res = app + .execute_contract(info.sender, &contract_info, &msg, &[]) + .unwrap(); + let mut viewing_key = String::new(); + let data: snip20_reference_impl::msg::ExecuteAnswer = from_binary(&res.data.unwrap()).unwrap(); + if let snip20_reference_impl::msg::ExecuteAnswer::CreateViewingKey { key } = data { + viewing_key = key; + }; + viewing_key +} + +fn test_instantiate_with_gov_modules() -> ContractInfo { + let mut app = App::default(); + let module_info = app.store_code(voting_cw4_contract()); + let group_contract = app.store_code(group_contract()); + let gov_info = app.store_code(cw_core_contract()); + let query_auth_info = app.store_code(query_auth_contract()); + let module_instantiate = dao_voting_cw4::msg::InstantiateMsg { + group_contract: dao_voting_cw4::msg::GroupContract::New { + cw4_group_code_id: group_contract.code_id, + cw4_group_code_hash: group_contract.code_hash, + initial_members: vec![Member { + addr: CREATOR_ADDR.to_string(), + weight: 1, + }], + query_auth: None, + }, + dao_code_hash: "dao_code_hash".to_string(), + }; + let instantiate = InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that builds DAOs.".to_string(), + image_url: None, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: module_info.clone().code_id, + code_hash: module_info.clone().code_hash, + msg: to_binary(&module_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "voting module".to_string(), + }, + proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { + code_id: module_info.clone().code_id, + code_hash: module_info.clone().code_hash, + msg: to_binary(&module_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: format!("governance module"), + }], + initial_items: None, + query_auth_code_id: query_auth_info.code_id, + query_auth_code_hash: query_auth_info.code_hash, + prng_seed: "seed".to_string(), + snip20_code_hash: "".to_string(), + snip721_code_hash: "".to_string(), + }; + + let gov_contract_info = instantiate_gov(&mut app, gov_info, instantiate); + app.update_block(next_block); + + let state: DumpStateResponse = app + .wrap() + .query_wasm_smart( + gov_contract_info.code_hash.clone(), + gov_contract_info.address.clone().to_string(), + &QueryMsg::DumpState {}, + ) + .unwrap(); + + assert_eq!( + state.config, + Config { + dao_uri: None, + name: "DAO DAO".to_string(), + description: "A DAO that builds DAOs.".to_string(), + image_url: None, + } + ); + + assert_eq!(state.proposal_modules.len(), 1); + + assert_eq!(state.active_proposal_module_count, 1 as u32); + + assert_eq!(state.total_proposal_module_count, 1 as u32); + + gov_contract_info +} + +fn test_instantiate_with_0_gov_modules() { + let mut app = App::default(); + let module_info = app.store_code(voting_cw4_contract()); + let group_contract = app.store_code(group_contract()); + let gov_info = app.store_code(cw_core_contract()); + let query_auth_info = app.store_code(query_auth_contract()); + let module_instantiate = dao_voting_cw4::msg::InstantiateMsg { + group_contract: dao_voting_cw4::msg::GroupContract::New { + cw4_group_code_id: group_contract.code_id, + cw4_group_code_hash: group_contract.code_hash, + initial_members: vec![Member { + addr: CREATOR_ADDR.to_string(), + weight: 1, + }], + query_auth: None, + }, + dao_code_hash: "dao_code_hash".to_string(), + }; + let instantiate = InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that builds DAOs.".to_string(), + image_url: None, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: module_info.clone().code_id, + code_hash: module_info.clone().code_hash, + msg: to_binary(&module_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "voting module".to_string(), + }, + proposal_modules_instantiate_info: vec![], + initial_items: None, + query_auth_code_id: query_auth_info.code_id, + query_auth_code_hash: query_auth_info.code_hash, + prng_seed: "seed".to_string(), + snip20_code_hash: "".to_string(), + snip721_code_hash: "".to_string(), + }; + let _ = instantiate_gov(&mut app, gov_info, instantiate); +} + +#[test] +#[should_panic(expected = "Execution would result in no proposal modules being active.")] +fn test_instantiate_with_zero_gov_modules() { + test_instantiate_with_0_gov_modules() +} + +#[test] +fn test_valid_instantiate() { + test_instantiate_with_gov_modules(); +} + +#[test] +fn test_update_config() { + let mut app = App::default(); + let module_info = app.store_code(voting_cw4_contract()); + let group_contract = app.store_code(group_contract()); + let gov_info = app.store_code(cw_core_contract()); + let query_auth_info = app.store_code(query_auth_contract()); + let module_instantiate = dao_voting_cw4::msg::InstantiateMsg { + group_contract: dao_voting_cw4::msg::GroupContract::New { + cw4_group_code_id: group_contract.code_id, + cw4_group_code_hash: group_contract.code_hash, + initial_members: vec![Member { + addr: CREATOR_ADDR.to_string(), + weight: 1, + }], + query_auth: None, + }, + dao_code_hash: "dao_code_hash".to_string(), + }; + let instantiate = InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that builds DAOs.".to_string(), + image_url: None, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: module_info.clone().code_id, + code_hash: module_info.clone().code_hash, + msg: to_binary(&module_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "voting module".to_string(), + }, + proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { + code_id: module_info.clone().code_id, + code_hash: module_info.clone().code_hash, + msg: to_binary(&module_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: format!("governance module"), + }], + initial_items: None, + query_auth_code_id: query_auth_info.code_id, + query_auth_code_hash: query_auth_info.code_hash, + prng_seed: "seed".to_string(), + snip20_code_hash: "".to_string(), + snip721_code_hash: "".to_string(), + }; + + let gov_contract_info = instantiate_gov(&mut app, gov_info, instantiate); + let modules: Vec = app + .wrap() + .query_wasm_smart( + gov_contract_info.code_hash.clone(), + gov_contract_info.address.clone(), + &QueryMsg::ProposalModules { + start_after: None, + limit: None, + }, + ) + .unwrap(); + + assert_eq!(modules.len(), 1); + + let expected_config = Config { + dao_uri: None, + name: "DAO DAO".to_string(), + description: "A DAO that builds DAOs.".to_string(), + image_url: None, + }; + + let config: Config = app + .wrap() + .query_wasm_smart( + gov_contract_info.code_hash.clone(), + gov_contract_info.address.clone(), + &QueryMsg::Config {}, + ) + .unwrap(); + + assert_eq!(expected_config, config); + + let dao_uri: DaoURIResponse = app + .wrap() + .query_wasm_smart( + gov_contract_info.code_hash, + gov_contract_info.address, + &QueryMsg::DaoURI {}, + ) + .unwrap(); + assert_eq!(dao_uri.dao_uri, expected_config.dao_uri); +} + +fn test_swap_governance(swaps: Vec<(u32, u32)>) { + let mut app = App::default(); + let module_info = app.store_code(voting_cw4_contract()); + let group_contract = app.store_code(group_contract()); + let gov_info = app.store_code(cw_core_contract()); + let query_auth_info = app.store_code(query_auth_contract()); + let module_instantiate = dao_voting_cw4::msg::InstantiateMsg { + group_contract: dao_voting_cw4::msg::GroupContract::New { + cw4_group_code_id: group_contract.code_id, + cw4_group_code_hash: group_contract.code_hash, + initial_members: vec![Member { + addr: CREATOR_ADDR.to_string(), + weight: 1, + }], + query_auth: None, + }, + dao_code_hash: "dao_code_hash".to_string(), + }; + let instantiate = InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that builds DAOs.".to_string(), + image_url: None, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: module_info.clone().code_id, + code_hash: module_info.clone().code_hash, + msg: to_binary(&module_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "voting module".to_string(), + }, + proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { + code_id: module_info.clone().code_id, + code_hash: module_info.clone().code_hash, + msg: to_binary(&module_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: format!("governance module"), + }], + initial_items: None, + query_auth_code_id: query_auth_info.code_id, + query_auth_code_hash: query_auth_info.code_hash, + prng_seed: "seed".to_string(), + snip20_code_hash: "".to_string(), + snip721_code_hash: "".to_string(), + }; + + let gov_contract_info = instantiate_gov(&mut app, gov_info, instantiate); + let modules: Vec = app + .wrap() + .query_wasm_smart( + gov_contract_info.code_hash.clone(), + gov_contract_info.address.clone(), + &QueryMsg::ProposalModules { + start_after: None, + limit: None, + }, + ) + .unwrap(); + + assert_eq!(modules.len(), 1); + + let module_count = query_proposal_module_count(&app, &gov_contract_info.clone()); + assert_eq!( + module_count, + ProposalModuleCountResponse { + active_proposal_module_count: 1, + total_proposal_module_count: 1, + } + ); + + let (to_add, to_remove) = swaps + .iter() + .cloned() + .reduce(|(to_add, to_remove), (add, remove)| (to_add + add, to_remove + remove)) + .unwrap_or((0, 0)); + + for (add, remove) in swaps { + let start_modules: Vec = app + .wrap() + .query_wasm_smart( + gov_contract_info.code_hash.clone(), + gov_contract_info.address.clone(), + &QueryMsg::ProposalModules { + start_after: None, + limit: None, + }, + ) + .unwrap(); + + let start_modules_active: Vec = + get_active_modules(&app, gov_contract_info.clone()); + + let to_add: Vec<_> = (0..add) + .map(|n| ModuleInstantiateInfo { + code_id: module_info.code_id, + code_hash: module_info.code_hash.clone(), + msg: to_binary(&module_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: format!("governance module {n}"), + }) + .collect(); + + let to_disable: Vec<_> = start_modules_active + .iter() + .rev() + .take(remove as usize) + .map(|a| a.address.to_string()) + .collect(); + println!("{:?}", to_disable); + + app.execute_contract( + Addr::unchecked(gov_contract_info.address.clone().into_string()), + &ContractInfo { + address: gov_contract_info.address.clone(), + code_hash: gov_contract_info.code_hash.clone(), + }, + &ExecuteMsg::UpdateProposalModules { to_add, to_disable }, + &[], + ) + .unwrap(); + app.update_block(next_block); + + let finish_modules_active = get_active_modules(&app, gov_contract_info.clone()); + + for module in start_modules + .clone() + .into_iter() + .rev() + .take(remove as usize) + { + assert!(!finish_modules_active.contains(&module)) + } + } + + let module_count = query_proposal_module_count(&app, &gov_contract_info); + println!("{:?}", module_count); + assert_eq!( + module_count, + ProposalModuleCountResponse { + active_proposal_module_count: 1 + to_add - to_remove, + total_proposal_module_count: 1 + to_add, + } + ); +} + +#[test] +fn test_update_governance() { + test_swap_governance(vec![(1, 1)]) +} + +#[test] +fn test_add_then_remove_governance() { + test_swap_governance(vec![(1, 0), (0, 1)]) +} + +#[test] +fn test_removed_modules_can_not_execute() { + let mut app = App::default(); + let govmod_info = app.store_code(sudo_proposal_contract()); + let gov_info = app.store_code(cw_core_contract()); + let query_auth_info = app.store_code(query_auth_contract()); + + let govmod_instantiate = dao_proposal_sudo::msg::InstantiateMsg { + root: CREATOR_ADDR.to_string(), + dao_code_hash: gov_info.code_hash.clone(), + }; + let gov_instantiate = InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that builds DAOs.".to_string(), + image_url: None, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: govmod_info.code_id.clone(), + code_hash: govmod_info.code_hash.clone(), + msg: to_binary(&govmod_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "voting module".to_string(), + }, + proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { + code_id: govmod_info.code_id.clone(), + code_hash: govmod_info.code_hash.clone(), + msg: to_binary(&govmod_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "governance module".to_string(), + }], + initial_items: None, + query_auth_code_id: query_auth_info.code_id, + query_auth_code_hash: query_auth_info.code_hash, + prng_seed: "seed".to_string(), + snip20_code_hash: "".to_string(), + snip721_code_hash: "".to_string(), + }; + + let gov_contract_info = app + .instantiate_contract( + gov_info, + Addr::unchecked(CREATOR_ADDR), + &gov_instantiate, + &[], + "cw-governance", + None, + ) + .unwrap(); + + let modules: Vec = app + .wrap() + .query_wasm_smart( + gov_contract_info.code_hash.clone(), + gov_contract_info.address.clone(), + &QueryMsg::ProposalModules { + start_after: None, + limit: None, + }, + ) + .unwrap(); + + assert_eq!(modules.len(), 1); + + let start_module = modules.into_iter().next().unwrap(); + + let to_add = vec![ModuleInstantiateInfo { + code_id: govmod_info.code_id.clone(), + code_hash: govmod_info.code_hash.clone(), + msg: to_binary(&govmod_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "new governance module".to_string(), + }]; + + let to_disable = vec![start_module.address.to_string()]; + + // Swap ourselves out. + app.execute_contract( + Addr::unchecked(CREATOR_ADDR), + &ContractInfo { + address: start_module.address.clone(), + code_hash: start_module.code_hash.clone(), + }, + &dao_proposal_sudo::msg::ExecuteMsg::Execute { + msgs: vec![WasmMsg::Execute { + contract_addr: gov_contract_info.address.clone().to_string(), + code_hash: gov_contract_info.code_hash.clone(), + funds: vec![], + msg: to_binary(&ExecuteMsg::UpdateProposalModules { to_add, to_disable }).unwrap(), + } + .into()], + }, + &[], + ) + .unwrap(); + + app.update_block(next_block); + + let finish_modules_active: Vec = + get_active_modules(&app, gov_contract_info.clone()); + println!("{:?}", finish_modules_active); + + let new_proposal_module = finish_modules_active.into_iter().next().unwrap(); + + // Try to add a new module and remove the one we added + // earlier. This should fail as we have been removed. + let to_add = vec![ModuleInstantiateInfo { + code_id: govmod_info.code_id.clone(), + code_hash: govmod_info.code_hash.clone(), + msg: to_binary(&govmod_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "new governance module".to_string(), + }]; + let to_disable = vec![new_proposal_module.address.to_string()]; + + let err: ContractError = app + .execute_contract( + Addr::unchecked(CREATOR_ADDR), + &ContractInfo { + address: start_module.address.clone(), + code_hash: start_module.code_hash.clone(), + }, + &dao_proposal_sudo::msg::ExecuteMsg::Execute { + msgs: vec![WasmMsg::Execute { + contract_addr: gov_contract_info.address.clone().to_string(), + code_hash: gov_contract_info.code_hash.clone(), + funds: vec![], + msg: to_binary(&ExecuteMsg::UpdateProposalModules { + to_add: to_add.clone(), + to_disable: to_disable.clone(), + }) + .unwrap(), + } + .into()], + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert!(matches!( + err, + ContractError::ModuleDisabledCannotExecute { + address: _gov_address + } + )); + + // Check that the enabled query works. + let enabled_modules: Vec = app + .wrap() + .query_wasm_smart( + &gov_contract_info.code_hash.clone(), + gov_contract_info.address.clone(), + &QueryMsg::ActiveProposalModules { + start_after: None, + limit: None, + }, + ) + .unwrap(); + + assert_eq!(enabled_modules, vec![new_proposal_module.clone()]); + + // The new proposal module should be able to perform actions. + app.execute_contract( + Addr::unchecked(CREATOR_ADDR), + &ContractInfo { + address: new_proposal_module.address.clone(), + code_hash: new_proposal_module.code_hash.clone(), + }, + &dao_proposal_sudo::msg::ExecuteMsg::Execute { + msgs: vec![WasmMsg::Execute { + contract_addr: gov_contract_info.address.to_string(), + code_hash: gov_contract_info.code_hash.to_string(), + funds: vec![], + msg: to_binary(&ExecuteMsg::UpdateProposalModules { to_add, to_disable }).unwrap(), + } + .into()], + }, + &[], + ) + .unwrap(); +} + +#[test] +fn test_module_already_disabled() { + let mut app = App::default(); + let govmod_info = app.store_code(sudo_proposal_contract()); + let gov_info = app.store_code(cw_core_contract()); + let query_auth_info = app.store_code(query_auth_contract()); + + let govmod_instantiate = dao_proposal_sudo::msg::InstantiateMsg { + root: CREATOR_ADDR.to_string(), + dao_code_hash: gov_info.code_hash.clone(), + }; + let gov_instantiate = InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that builds DAOs.".to_string(), + image_url: None, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: govmod_info.code_id.clone(), + code_hash: govmod_info.code_hash.clone(), + msg: to_binary(&govmod_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "voting module".to_string(), + }, + proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { + code_id: govmod_info.code_id.clone(), + code_hash: govmod_info.code_hash.clone(), + msg: to_binary(&govmod_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "governance module".to_string(), + }], + initial_items: None, + query_auth_code_id: query_auth_info.code_id, + query_auth_code_hash: query_auth_info.code_hash, + prng_seed: "seed".to_string(), + snip20_code_hash: "".to_string(), + snip721_code_hash: "".to_string(), + }; + + let gov_contract_info = app + .instantiate_contract( + gov_info, + Addr::unchecked(CREATOR_ADDR), + &gov_instantiate, + &[], + "cw-governance", + None, + ) + .unwrap(); + + let modules: Vec = app + .wrap() + .query_wasm_smart( + gov_contract_info.code_hash.clone(), + gov_contract_info.address.clone(), + &QueryMsg::ProposalModules { + start_after: None, + limit: None, + }, + ) + .unwrap(); + + assert_eq!(modules.len(), 1); + + let start_module = modules.into_iter().next().unwrap(); + + let to_disable = vec![ + start_module.address.to_string(), + start_module.address.to_string(), + ]; + + let err: ContractError = app + .execute_contract( + Addr::unchecked(CREATOR_ADDR), + &ContractInfo { + address: start_module.address.clone(), + code_hash: start_module.code_hash.clone(), + }, + &dao_proposal_sudo::msg::ExecuteMsg::Execute { + msgs: vec![WasmMsg::Execute { + contract_addr: gov_contract_info.address.clone().to_string(), + code_hash: gov_contract_info.code_hash.clone(), + funds: vec![], + msg: to_binary(&ExecuteMsg::UpdateProposalModules { + to_add: vec![ModuleInstantiateInfo { + code_id: govmod_info.code_id.clone(), + code_hash: govmod_info.code_hash.clone(), + msg: to_binary(&govmod_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "governance module".to_string(), + }], + to_disable, + }) + .unwrap(), + } + .into()], + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + + assert_eq!( + err, + ContractError::ModuleAlreadyDisabled { + address: start_module.address + } + ) +} + +#[test] +fn test_swap_voting_module() { + let mut app = App::default(); + let govmod_info = app.store_code(sudo_proposal_contract()); + let gov_info = app.store_code(cw_core_contract()); + let query_auth_info = app.store_code(query_auth_contract()); + + let govmod_instantiate = dao_proposal_sudo::msg::InstantiateMsg { + root: CREATOR_ADDR.to_string(), + dao_code_hash: gov_info.code_hash.clone(), + }; + let gov_instantiate = InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that builds DAOs.".to_string(), + image_url: None, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: govmod_info.code_id.clone(), + code_hash: govmod_info.code_hash.clone(), + msg: to_binary(&govmod_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "voting module".to_string(), + }, + proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { + code_id: govmod_info.code_id.clone(), + code_hash: govmod_info.code_hash.clone(), + msg: to_binary(&govmod_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "governance module".to_string(), + }], + initial_items: None, + query_auth_code_id: query_auth_info.code_id, + query_auth_code_hash: query_auth_info.code_hash, + prng_seed: "seed".to_string(), + snip20_code_hash: "".to_string(), + snip721_code_hash: "".to_string(), + }; + + let gov_contract_info = app + .instantiate_contract( + gov_info, + Addr::unchecked(CREATOR_ADDR), + &gov_instantiate, + &[], + "cw-governance", + None, + ) + .unwrap(); + + let voting_module: VotingModuleInfo = app + .wrap() + .query_wasm_smart( + gov_contract_info.code_hash.clone(), + gov_contract_info.address.clone(), + &QueryMsg::VotingModule {}, + ) + .unwrap(); + + let modules: Vec = app + .wrap() + .query_wasm_smart( + gov_contract_info.code_hash.clone(), + gov_contract_info.address.clone(), + &QueryMsg::ProposalModules { + start_after: None, + limit: None, + }, + ) + .unwrap(); + + assert_eq!(modules.len(), 1); + + app.execute_contract( + Addr::unchecked(CREATOR_ADDR), + &ContractInfo { + address: modules[0].address.clone(), + code_hash: modules[0].code_hash.clone(), + }, + &dao_proposal_sudo::msg::ExecuteMsg::Execute { + msgs: vec![WasmMsg::Execute { + contract_addr: gov_contract_info.address.clone().to_string(), + code_hash: gov_contract_info.code_hash.clone(), + funds: vec![], + msg: to_binary(&ExecuteMsg::UpdateVotingModule { + module: ModuleInstantiateInfo { + code_id: govmod_info.code_id.clone(), + code_hash: govmod_info.code_hash.clone(), + msg: to_binary(&govmod_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "voting module".to_string(), + }, + }) + .unwrap(), + } + .into()], + }, + &[], + ) + .unwrap(); + + let new_voting_module: VotingModuleInfo = app + .wrap() + .query_wasm_smart( + gov_contract_info.code_hash, + gov_contract_info.address, + &QueryMsg::VotingModule {}, + ) + .unwrap(); + + assert_ne!(new_voting_module, voting_module); +} + +fn test_unauthorized(app: &mut App, gov_contract_info: ContractInfo, msg: ExecuteMsg) { + let err: ContractError = app + .execute_contract(Addr::unchecked(CREATOR_ADDR), &gov_contract_info, &msg, &[]) + .unwrap_err() + .downcast() + .unwrap(); + + assert_eq!(err, ContractError::Unauthorized {}); +} + +#[test] +fn test_permissions() { + let mut app = App::default(); + let govmod_info = app.store_code(sudo_proposal_contract()); + let gov_info = app.store_code(cw_core_contract()); + let query_auth_info = app.store_code(query_auth_contract()); + + let govmod_instantiate = dao_proposal_sudo::msg::InstantiateMsg { + root: CREATOR_ADDR.to_string(), + dao_code_hash: gov_info.code_hash.clone(), + }; + let gov_instantiate = InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that builds DAOs.".to_string(), + image_url: None, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: govmod_info.code_id.clone(), + code_hash: govmod_info.code_hash.clone(), + msg: to_binary(&govmod_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "voting module".to_string(), + }, + proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { + code_id: govmod_info.code_id.clone(), + code_hash: govmod_info.code_hash.clone(), + msg: to_binary(&govmod_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "governance module".to_string(), + }], + initial_items: None, + query_auth_code_id: query_auth_info.code_id, + query_auth_code_hash: query_auth_info.code_hash, + prng_seed: "seed".to_string(), + snip20_code_hash: "".to_string(), + snip721_code_hash: "".to_string(), + }; + + let gov_contract_info = app + .instantiate_contract( + gov_info, + Addr::unchecked(CREATOR_ADDR), + &gov_instantiate, + &[], + "cw-governance", + None, + ) + .unwrap(); + + test_unauthorized( + &mut app, + gov_contract_info.clone(), + ExecuteMsg::UpdateVotingModule { + module: ModuleInstantiateInfo { + code_id: govmod_info.code_id.clone(), + code_hash: govmod_info.code_hash.clone(), + msg: to_binary(&govmod_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "voting module".to_string(), + }, + }, + ); + + test_unauthorized( + &mut app, + gov_contract_info.clone(), + ExecuteMsg::UpdateProposalModules { + to_add: vec![], + to_disable: vec![], + }, + ); + + test_unauthorized( + &mut app, + gov_contract_info, + ExecuteMsg::UpdateConfig { + config: Config { + dao_uri: None, + name: "Evil config.".to_string(), + description: "👿".to_string(), + image_url: None, + }, + }, + ); +} + +fn do_standard_instantiate(_auto_add: bool, admin: Option) -> (ContractInfo, App) { + let mut app = App::default(); + let govmod_info = app.store_code(sudo_proposal_contract()); + let voting_info = app.store_code(snip20_balances_voting()); + let gov_info = app.store_code(cw_core_contract()); + let snip20_info = app.store_code(snip20_contract()); + let snip721_info = app.store_code(snip721_contract()); + let query_auth_info = app.store_code(query_auth_contract()); + + let govmod_instantiate = dao_proposal_sudo::msg::InstantiateMsg { + root: CREATOR_ADDR.to_string(), + dao_code_hash: gov_info.code_hash.clone(), + }; + let voting_instantiate = dao_voting_snip20_balance::msg::InstantiateMsg { + token_info: dao_voting_snip20_balance::msg::TokenInfo::New { + code_id: snip20_info.code_id.clone(), + code_hash: snip20_info.code_hash.clone(), + label: "DAO DAO voting".to_string(), + name: "DAO DAO".to_string(), + symbol: "DAO".to_string(), + decimals: 6, + initial_balances: vec![InitialBalance { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(2), + }], + }, + dao_code_hash: gov_info.code_hash.clone(), + }; + + let gov_instantiate = InstantiateMsg { + dao_uri: None, + admin, + name: "DAO DAO".to_string(), + description: "A DAO that builds DAOs.".to_string(), + image_url: None, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: voting_info.code_id.clone(), + code_hash: voting_info.code_hash.clone(), + msg: to_binary(&voting_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "voting module".to_string(), + }, + proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { + code_id: govmod_info.code_id.clone(), + code_hash: govmod_info.code_hash.clone(), + msg: to_binary(&govmod_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "governance module".to_string(), + }], + initial_items: None, + query_auth_code_id: query_auth_info.code_id, + query_auth_code_hash: query_auth_info.code_hash, + prng_seed: "seed".to_string(), + snip20_code_hash: snip20_info.code_hash.clone(), + snip721_code_hash: snip721_info.code_hash.to_string(), + }; + + let gov_contract_info = app + .instantiate_contract( + gov_info, + Addr::unchecked(CREATOR_ADDR), + &gov_instantiate, + &[], + "cw-governance", + None, + ) + .unwrap(); + + (gov_contract_info, app) +} + +#[test] +fn test_admin_permissions() { + let (core_contract_info, mut app) = do_standard_instantiate(true, None); + + let start_height = app.block_info().height; + let proposal_modules: Vec = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &QueryMsg::ProposalModules { + start_after: None, + limit: None, + }, + ) + .unwrap(); + + assert_eq!(proposal_modules.len(), 1); + let proposal_module = proposal_modules.into_iter().next().unwrap(); + + // Random address can't call ExecuteAdminMsgs + let res = app.execute_contract( + Addr::unchecked("random"), + &core_contract_info.clone(), + &ExecuteMsg::ExecuteAdminMsgs { + msgs: vec![WasmMsg::Execute { + contract_addr: core_contract_info.address.clone().to_string(), + code_hash: core_contract_info.code_hash.clone(), + msg: to_binary(&ExecuteMsg::Pause { + duration: Duration::Height(10), + }) + .unwrap(), + funds: vec![], + } + .into()], + }, + &[], + ); + res.unwrap_err(); + + // Proposal mdoule can't call ExecuteAdminMsgs + let res = app.execute_contract( + proposal_module.address.clone(), + &core_contract_info.clone(), + &ExecuteMsg::ExecuteAdminMsgs { + msgs: vec![WasmMsg::Execute { + contract_addr: core_contract_info.address.clone().to_string(), + code_hash: core_contract_info.code_hash.clone(), + msg: to_binary(&ExecuteMsg::Pause { + duration: Duration::Height(10), + }) + .unwrap(), + funds: vec![], + } + .into()], + }, + &[], + ); + res.unwrap_err(); + + // Update Admin can't be called by non-admins + let res = app.execute_contract( + Addr::unchecked("rando"), + &core_contract_info.clone(), + &ExecuteMsg::NominateAdmin { + admin: Some("rando".to_string()), + }, + &[], + ); + res.unwrap_err(); + + // Nominate admin can be called by core contract as no admin was + // specified so the admin defaulted to the core contract. + let res = app.execute_contract( + proposal_module.address.clone(), + &core_contract_info.clone(), + &ExecuteMsg::ExecuteProposalHook { + msgs: vec![WasmMsg::Execute { + contract_addr: core_contract_info.address.clone().to_string(), + code_hash: core_contract_info.code_hash.clone(), + msg: to_binary(&ExecuteMsg::NominateAdmin { + admin: Some("meow".to_string()), + }) + .unwrap(), + funds: vec![], + } + .into()], + }, + &[], + ); + res.unwrap(); + + // Instantiate new DAO with an admin + let (core_with_admin_addr, mut app) = + do_standard_instantiate(true, Some(Addr::unchecked("admin").to_string())); + + // Non admins still can't call ExecuteAdminMsgs + let res = app.execute_contract( + proposal_module.address, + &core_with_admin_addr.clone(), + &ExecuteMsg::ExecuteAdminMsgs { + msgs: vec![WasmMsg::Execute { + contract_addr: core_with_admin_addr.address.clone().to_string(), + code_hash: core_with_admin_addr.code_hash.clone(), + msg: to_binary(&ExecuteMsg::Pause { + duration: Duration::Height(10), + }) + .unwrap(), + funds: vec![], + } + .into()], + }, + &[], + ); + res.unwrap_err(); + + // Admin can call ExecuteAdminMsgs, here an admin pasues the DAO + let res = app.execute_contract( + Addr::unchecked("admin"), + &core_with_admin_addr.clone(), + &ExecuteMsg::ExecuteAdminMsgs { + msgs: vec![WasmMsg::Execute { + contract_addr: core_with_admin_addr.address.clone().to_string(), + code_hash: core_with_admin_addr.code_hash.clone(), + msg: to_binary(&ExecuteMsg::Pause { + duration: Duration::Height(10), + }) + .unwrap(), + funds: vec![], + } + .into()], + }, + &[], + ); + res.unwrap(); + + let paused: PauseInfoResponse = app + .wrap() + .query_wasm_smart( + core_with_admin_addr.code_hash.clone(), + core_with_admin_addr.address.clone(), + &QueryMsg::PauseInfo {}, + ) + .unwrap(); + assert_eq!( + paused, + PauseInfoResponse::Paused { + expiration: Expiration::AtHeight(start_height + 10) + } + ); + + // DAO unpauses after 10 blocks + app.update_block(|block| block.height += 11); + + // Admin can nominate a new admin. + let res = app.execute_contract( + Addr::unchecked("admin"), + &core_with_admin_addr.clone(), + &ExecuteMsg::NominateAdmin { + admin: Some("meow".to_string()), + }, + &[], + ); + res.unwrap(); + + let nomination: AdminNominationResponse = app + .wrap() + .query_wasm_smart( + core_with_admin_addr.code_hash.clone(), + core_with_admin_addr.address.clone(), + &QueryMsg::AdminNomination {}, + ) + .unwrap(); + assert_eq!( + nomination, + AdminNominationResponse { + nomination: Some(Addr::unchecked("meow")) + } + ); + + // Check that admin has not yet been updated + let res: Addr = app + .wrap() + .query_wasm_smart( + core_with_admin_addr.code_hash.clone(), + core_with_admin_addr.address.clone(), + &QueryMsg::Admin {}, + ) + .unwrap(); + assert_eq!(res, Addr::unchecked("admin")); + + // Only the nominated address may accept the nomination. + let err: ContractError = app + .execute_contract( + Addr::unchecked("random"), + &core_with_admin_addr.clone(), + &ExecuteMsg::AcceptAdminNomination {}, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, ContractError::Unauthorized {}); + + // Accept the nomination. + app.execute_contract( + Addr::unchecked("meow"), + &core_with_admin_addr.clone(), + &ExecuteMsg::AcceptAdminNomination {}, + &[], + ) + .unwrap(); + + // Check that admin has been updated + let res: Addr = app + .wrap() + .query_wasm_smart( + core_with_admin_addr.code_hash.clone(), + core_with_admin_addr.address.clone(), + &QueryMsg::Admin {}, + ) + .unwrap(); + assert_eq!(res, Addr::unchecked("meow")); + + // Check that the pending admin has been cleared. + let nomination: AdminNominationResponse = app + .wrap() + .query_wasm_smart( + core_with_admin_addr.code_hash, + core_with_admin_addr.address, + &QueryMsg::AdminNomination {}, + ) + .unwrap(); + assert_eq!(nomination, AdminNominationResponse { nomination: None }); +} + +#[test] +fn test_admin_nomination() { + let (core_contract_info, mut app) = do_standard_instantiate(true, Some("admin".to_string())); + + // Check that there is no pending nominations. + let nomination: AdminNominationResponse = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &QueryMsg::AdminNomination {}, + ) + .unwrap(); + assert_eq!(nomination, AdminNominationResponse { nomination: None }); + + // Nominate a new admin. + app.execute_contract( + Addr::unchecked("admin"), + &core_contract_info.clone(), + &ExecuteMsg::NominateAdmin { + admin: Some("ekez".to_string()), + }, + &[], + ) + .unwrap(); + + // Check that the nomination is in place. + let nomination: AdminNominationResponse = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &QueryMsg::AdminNomination {}, + ) + .unwrap(); + assert_eq!( + nomination, + AdminNominationResponse { + nomination: Some(Addr::unchecked("ekez")) + } + ); + + // Non-admin can not withdraw. + let err: ContractError = app + .execute_contract( + Addr::unchecked("ekez"), + &core_contract_info.clone(), + &ExecuteMsg::WithdrawAdminNomination {}, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, ContractError::Unauthorized {}); + + // Admin can withdraw. + app.execute_contract( + Addr::unchecked("admin"), + &core_contract_info.clone(), + &ExecuteMsg::WithdrawAdminNomination {}, + &[], + ) + .unwrap(); + + // Check that the nomination is withdrawn. + let nomination: AdminNominationResponse = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &QueryMsg::AdminNomination {}, + ) + .unwrap(); + assert_eq!(nomination, AdminNominationResponse { nomination: None }); + + // Can not withdraw if no nomination is pending. + let err: ContractError = app + .execute_contract( + Addr::unchecked("admin"), + &core_contract_info.clone(), + &ExecuteMsg::WithdrawAdminNomination {}, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, ContractError::NoAdminNomination {}); + + // Can not claim nomination b/c it has been withdrawn. + let err: ContractError = app + .execute_contract( + Addr::unchecked("ekez"), + &core_contract_info.clone(), + &ExecuteMsg::AcceptAdminNomination {}, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, ContractError::NoAdminNomination {}); + + // Nominate a new admin. + app.execute_contract( + Addr::unchecked("admin"), + &core_contract_info.clone(), + &ExecuteMsg::NominateAdmin { + admin: Some("meow".to_string()), + }, + &[], + ) + .unwrap(); + + // A new nomination can not be created if there is already a + // pending nomination. + let err: ContractError = app + .execute_contract( + Addr::unchecked("admin"), + &core_contract_info.clone(), + &ExecuteMsg::NominateAdmin { + admin: Some("arthur".to_string()), + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, ContractError::PendingNomination {}); + + // Only nominated admin may accept. + let err: ContractError = app + .execute_contract( + Addr::unchecked("ekez"), + &core_contract_info.clone(), + &ExecuteMsg::AcceptAdminNomination {}, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, ContractError::Unauthorized {}); + + app.execute_contract( + Addr::unchecked("meow"), + &core_contract_info.clone(), + &ExecuteMsg::AcceptAdminNomination {}, + &[], + ) + .unwrap(); + + // Check that meow is the new admin. + let admin: Addr = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &QueryMsg::Admin {}, + ) + .unwrap(); + assert_eq!(admin, Addr::unchecked("meow".to_string())); + + let start_height = app.block_info().height; + // Check that the new admin can do admin things and the old can not. + let err: ContractError = app + .execute_contract( + Addr::unchecked("admin"), + &core_contract_info.clone(), + &ExecuteMsg::ExecuteAdminMsgs { + msgs: vec![WasmMsg::Execute { + contract_addr: core_contract_info.address.clone().to_string(), + code_hash: core_contract_info.code_hash.clone(), + msg: to_binary(&ExecuteMsg::Pause { + duration: Duration::Height(10), + }) + .unwrap(), + funds: vec![], + } + .into()], + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, ContractError::Unauthorized {}); + + let res = app.execute_contract( + Addr::unchecked("meow"), + &core_contract_info.clone(), + &ExecuteMsg::ExecuteAdminMsgs { + msgs: vec![WasmMsg::Execute { + contract_addr: core_contract_info.address.clone().to_string(), + code_hash: core_contract_info.code_hash.clone(), + msg: to_binary(&ExecuteMsg::Pause { + duration: Duration::Height(10), + }) + .unwrap(), + funds: vec![], + } + .into()], + }, + &[], + ); + res.unwrap(); + + let paused: PauseInfoResponse = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &QueryMsg::PauseInfo {}, + ) + .unwrap(); + assert_eq!( + paused, + PauseInfoResponse::Paused { + expiration: Expiration::AtHeight(start_height + 10) + } + ); + + // DAO unpauses after 10 blocks + app.update_block(|block| block.height += 11); + + // Remove the admin. + app.execute_contract( + Addr::unchecked("meow"), + &core_contract_info.clone(), + &ExecuteMsg::NominateAdmin { admin: None }, + &[], + ) + .unwrap(); + + // Check that this has not caused an admin to be nominated. + let nomination: AdminNominationResponse = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &QueryMsg::AdminNomination {}, + ) + .unwrap(); + assert_eq!(nomination, AdminNominationResponse { nomination: None }); + + // Check that admin has been updated. As there was no admin + // nominated the admin should revert back to the contract address. + let res: Addr = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &QueryMsg::Admin {}, + ) + .unwrap(); + assert_eq!(res, core_contract_info.address); +} + +#[test] +fn test_passthrough_voting_queries() { + let (gov_conract_info, mut app) = do_standard_instantiate(true, None); + + let voting_module: VotingModuleInfo = app + .wrap() + .query_wasm_smart( + gov_conract_info.code_hash.clone(), + gov_conract_info.address.clone(), + &QueryMsg::VotingModule {}, + ) + .unwrap(); + + let token_contract: AnyContractInfo = app + .wrap() + .query_wasm_smart( + voting_module.code_hash.clone(), + voting_module.addr.clone(), + &dao_voting_snip20_balance::msg::QueryMsg::TokenContract {}, + ) + .unwrap(); + + let viewing_key_token = create_token_viewing_key( + &mut app, + ContractInfo { + address: token_contract.addr, + code_hash: token_contract.code_hash, + }, + mock_info(CREATOR_ADDR, &[]), + ); + + let creator_voting_power: VotingPowerAtHeightResponse = app + .wrap() + .query_wasm_smart( + gov_conract_info.code_hash.clone(), + gov_conract_info.address.clone(), + &QueryMsg::VotingPowerAtHeight { + auth: shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key_token.clone(), + address: CREATOR_ADDR.to_string(), + }, + height: None, + }, + ) + .unwrap(); + + assert_eq!( + creator_voting_power, + VotingPowerAtHeightResponse { + power: Uint128::from(2u64), + height: app.block_info().height, + } + ); +} + +fn set_item(app: &mut App, gov_contract_info: ContractInfo, key: String, value: String) { + app.execute_contract( + gov_contract_info.address.clone(), + &gov_contract_info, + &ExecuteMsg::SetItem { key, value }, + &[], + ) + .unwrap(); +} + +fn remove_item(app: &mut App, gov_contract_info: ContractInfo, key: String) { + app.execute_contract( + gov_contract_info.address.clone(), + &gov_contract_info, + &ExecuteMsg::RemoveItem { key }, + &[], + ) + .unwrap(); +} + +fn get_item(app: &mut App, gov_contract_info: ContractInfo, key: String) -> GetItemResponse { + app.wrap() + .query_wasm_smart( + gov_contract_info.code_hash, + gov_contract_info.address, + &QueryMsg::GetItem { key }, + ) + .unwrap() +} + +fn list_items( + app: &mut App, + gov_contract_info: ContractInfo, + start_at: Option, + limit: Option, +) -> Vec<(String, String)> { + app.wrap() + .query_wasm_smart( + gov_contract_info.code_hash, + gov_contract_info.address, + &QueryMsg::ListItems { + start_after: start_at, + limit, + }, + ) + .unwrap() +} + +#[test] +fn test_item_permissions() { + let (gov_contract_info, mut app) = do_standard_instantiate(true, None); + + let err: ContractError = app + .execute_contract( + Addr::unchecked("ekez"), + &gov_contract_info.clone(), + &ExecuteMsg::SetItem { + key: "k".to_string(), + value: "v".to_string(), + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, ContractError::Unauthorized {}); + + let err: ContractError = app + .execute_contract( + Addr::unchecked("ekez"), + &gov_contract_info, + &ExecuteMsg::RemoveItem { + key: "k".to_string(), + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, ContractError::Unauthorized {}); +} + +#[test] +fn test_add_remove_get() { + let (gov_contract_info, mut app) = do_standard_instantiate(true, None); + + let a = get_item(&mut app, gov_contract_info.clone(), "aaaaa".to_string()); + assert_eq!(a, GetItemResponse { item: None }); + + set_item( + &mut app, + gov_contract_info.clone(), + "aaaaakey".to_string(), + "aaaaaaddr".to_string(), + ); + let a = get_item(&mut app, gov_contract_info.clone(), "aaaaakey".to_string()); + assert_eq!( + a, + GetItemResponse { + item: Some("aaaaaaddr".to_string()) + } + ); + + remove_item(&mut app, gov_contract_info.clone(), "aaaaakey".to_string()); + let a = get_item(&mut app, gov_contract_info, "aaaaakey".to_string()); + assert_eq!(a, GetItemResponse { item: None }); +} + +#[test] +#[should_panic(expected = "Key is missing from storage")] +fn test_remove_missing_key() { + let (gov_contract_info, mut app) = do_standard_instantiate(true, None); + remove_item(&mut app, gov_contract_info, "b".to_string()) +} + +#[test] +fn test_list_items() { + let mut app = App::default(); + let govmod_info = app.store_code(sudo_proposal_contract()); + let voting_info = app.store_code(snip20_balances_voting()); + let gov_info = app.store_code(cw_core_contract()); + let snip20_info = app.store_code(snip20_contract()); + let query_auth_info = app.store_code(query_auth_contract()); + + let govmod_instantiate = dao_proposal_sudo::msg::InstantiateMsg { + root: CREATOR_ADDR.to_string(), + dao_code_hash: gov_info.code_hash.clone(), + }; + let voting_instantiate = dao_voting_snip20_balance::msg::InstantiateMsg { + token_info: dao_voting_snip20_balance::msg::TokenInfo::New { + code_id: snip20_info.code_id.clone(), + code_hash: snip20_info.code_hash.clone(), + label: "DAO DAO voting".to_string(), + name: "DAO DAO".to_string(), + symbol: "DAO".to_string(), + decimals: 6, + initial_balances: vec![InitialBalance { + address: CREATOR_ADDR.to_string(), + amount: Uint128::from(2u64), + }], + }, + dao_code_hash: gov_info.code_hash.clone(), + }; + + let gov_instantiate = InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that builds DAOs.".to_string(), + image_url: None, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: voting_info.code_id, + code_hash: voting_info.code_hash.clone(), + msg: to_binary(&voting_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "voting module".to_string(), + }, + proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { + code_id: govmod_info.code_id, + code_hash: govmod_info.code_hash.clone(), + msg: to_binary(&govmod_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "governance module".to_string(), + }], + initial_items: None, + query_auth_code_id: query_auth_info.code_id, + query_auth_code_hash: query_auth_info.code_hash, + prng_seed: "seed".to_string(), + snip20_code_hash: "".to_string(), + snip721_code_hash: "".to_string(), + }; + + let gov_contract_info = app + .instantiate_contract( + gov_info.clone(), + Addr::unchecked(CREATOR_ADDR), + &gov_instantiate, + &[], + "cw-governance", + None, + ) + .unwrap(); + + set_item( + &mut app, + gov_contract_info.clone(), + "fookey".to_string(), + "fooaddr".to_string(), + ); + set_item( + &mut app, + gov_contract_info.clone(), + "barkey".to_string(), + "baraddr".to_string(), + ); + set_item( + &mut app, + gov_contract_info.clone(), + "loremkey".to_string(), + "loremaddr".to_string(), + ); + set_item( + &mut app, + gov_contract_info.clone(), + "ipsumkey".to_string(), + "ipsumaddr".to_string(), + ); + + // Foo returned as we are only getting one item and items are in + // decending order. + let first_item = list_items(&mut app, gov_contract_info.clone(), None, Some(1)); + assert_eq!(first_item.len(), 1); + assert_eq!( + first_item[0], + ("loremkey".to_string(), "loremaddr".to_string()) + ); + + let no_items = list_items(&mut app, gov_contract_info.clone(), None, Some(0)); + assert_eq!(no_items.len(), 0); + + // Items are retreived in decending order so asking for foo with + // no limit ought to give us the barkey k/v. this will be the last item + // note: the paginate map bound is exclusive, so fookey will be starting point + let last_item = list_items( + &mut app, + gov_contract_info.clone(), + Some("fookey".to_string()), + None, + ); + assert_eq!(last_item.len(), 1); + assert_eq!(last_item[0], ("barkey".to_string(), "baraddr".to_string())); + + // Items are retreived in decending order so asking for ipsum with + // 4 limit ought to give us the fookey and barkey k/vs. + let after_foo_list = list_items( + &mut app, + gov_contract_info, + Some("ipsumkey".to_string()), + Some(4), + ); + assert_eq!(after_foo_list.len(), 2); + assert_eq!( + after_foo_list, + vec![ + ("fookey".to_string(), "fooaddr".to_string()), + ("barkey".to_string(), "baraddr".to_string()) + ] + ); +} + +#[test] +fn test_instantiate_with_items() { + let mut app = App::default(); + let govmod_info = app.store_code(sudo_proposal_contract()); + let voting_info = app.store_code(snip20_balances_voting()); + let gov_info = app.store_code(cw_core_contract()); + let snip20_info = app.store_code(snip20_contract()); + let query_auth_info = app.store_code(query_auth_contract()); + + let govmod_instantiate = dao_proposal_sudo::msg::InstantiateMsg { + root: CREATOR_ADDR.to_string(), + dao_code_hash: gov_info.code_hash.clone(), + }; + let voting_instantiate = dao_voting_snip20_balance::msg::InstantiateMsg { + token_info: dao_voting_snip20_balance::msg::TokenInfo::New { + code_id: snip20_info.code_id.clone(), + code_hash: snip20_info.code_hash.clone(), + label: "DAO DAO voting".to_string(), + name: "DAO DAO".to_string(), + symbol: "DAO".to_string(), + decimals: 6, + initial_balances: vec![InitialBalance { + address: CREATOR_ADDR.to_string(), + amount: Uint128::from(2u64), + }], + }, + dao_code_hash: gov_info.code_hash.clone(), + }; + + let mut initial_items = vec![ + InitialItem { + key: "item0".to_string(), + value: "item0_value".to_string(), + }, + InitialItem { + key: "item1".to_string(), + value: "item1_value".to_string(), + }, + InitialItem { + key: "item0".to_string(), + value: "item0_value_override".to_string(), + }, + ]; + + let mut gov_instantiate = InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that builds DAOs.".to_string(), + image_url: None, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: voting_info.code_id, + code_hash: voting_info.code_hash.clone(), + msg: to_binary(&voting_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "voting module".to_string(), + }, + proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { + code_id: govmod_info.code_id, + code_hash: govmod_info.code_hash.clone(), + msg: to_binary(&govmod_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "governance module".to_string(), + }], + initial_items: Some(initial_items.clone()), + query_auth_code_id: query_auth_info.code_id, + query_auth_code_hash: query_auth_info.code_hash, + prng_seed: "seed".to_string(), + snip20_code_hash: "".to_string(), + snip721_code_hash: "".to_string(), + }; + + // Ensure duplicates are dissallowed. + let err: ContractError = app + .instantiate_contract( + gov_info.clone(), + Addr::unchecked(CREATOR_ADDR), + &gov_instantiate, + &[], + "cw-governance", + None, + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + ContractError::DuplicateInitialItem { + item: "item0".to_string() + } + ); + + initial_items.pop(); + gov_instantiate.initial_items = Some(initial_items); + let gov_contract_info = app + .instantiate_contract( + gov_info.clone(), + Addr::unchecked(CREATOR_ADDR), + &gov_instantiate, + &[], + "cw-governance", + None, + ) + .unwrap(); + + // Ensure initial items were added. + let items = list_items(&mut app, gov_contract_info.clone(), None, None); + assert_eq!(items.len(), 2); + + // Descending order, so item1 is first. + assert_eq!(items[1].0, "item0".to_string()); + let get_item0 = get_item(&mut app, gov_contract_info.clone(), "item0".to_string()); + assert_eq!( + get_item0, + GetItemResponse { + item: Some("item0_value".to_string()), + } + ); + + assert_eq!(items[0].0, "item1".to_string()); + let item1_value = get_item(&mut app, gov_contract_info, "item1".to_string()).item; + assert_eq!(item1_value, Some("item1_value".to_string())) +} + +#[test] +fn test_snip20_receive_auto_add() { + let (gov_contract_info, mut app) = do_standard_instantiate(true, None); + + let snip20_info = app.store_code(snip20_contract()); + let another_snip20_contract = app + .instantiate_contract( + snip20_info, + Addr::unchecked(CREATOR_ADDR), + &snip20_reference_impl::msg::InstantiateMsg { + name: "DAO".to_string(), + symbol: "DAO".to_string(), + decimals: 6, + initial_balances: vec![].into(), + admin: None, + prng_seed: to_binary(&"seeed").unwrap(), + config: None, + supported_denoms: None, + }, + &[], + "another-token", + None, + ) + .unwrap(); + + let voting_module: AnyContractInfo = app + .wrap() + .query_wasm_smart( + gov_contract_info.code_hash.clone(), + gov_contract_info.address.clone(), + &QueryMsg::VotingModule {}, + ) + .unwrap(); + let gov_token_info: AnyContractInfo = app + .wrap() + .query_wasm_smart( + voting_module.code_hash.clone(), + voting_module.addr.clone(), + &dao_interface::voting::Query::TokenContract {}, + ) + .unwrap(); + + // Check that the balances query works with no tokens. + let snip20_balances: Vec = app + .wrap() + .query_wasm_smart( + gov_contract_info.code_hash.clone(), + gov_contract_info.address.clone(), + &QueryMsg::Snip20Balances { + start_after: None, + limit: None, + }, + ) + .unwrap(); + assert_eq!(snip20_balances, vec![]); + + // Send a gov token to the governance contract. + app.execute_contract( + Addr::unchecked(CREATOR_ADDR), + &ContractInfo { + address: gov_token_info.addr.clone(), + code_hash: gov_token_info.code_hash.clone(), + }, + &snip20_reference_impl::msg::ExecuteMsg::Send { + recipient: gov_contract_info.address.clone().to_string(), + recipient_code_hash: Some(gov_contract_info.code_hash.clone()), + amount: Uint128::new(1), + msg: Some(to_binary(&"").unwrap()), + memo: None, + decoys: None, + entropy: None, + padding: None, + }, + &[], + ) + .unwrap(); + + let snip20_list: Vec = app + .wrap() + .query_wasm_smart( + gov_contract_info.code_hash.clone(), + gov_contract_info.address.clone(), + &QueryMsg::Snip20TokenList { + start_after: None, + limit: None, + }, + ) + .unwrap(); + assert_eq!(snip20_list, vec![gov_token_info.addr.clone()]); + + let snip20_balances: Vec = app + .wrap() + .query_wasm_smart( + gov_contract_info.code_hash.clone(), + gov_contract_info.address.clone(), + &QueryMsg::Snip20Balances { + start_after: None, + limit: None, + }, + ) + .unwrap(); + assert_eq!( + snip20_balances, + vec![Snip20BalanceResponse { + addr: gov_token_info.addr.clone().to_string(), + balance: Uint128::new(1), + }] + ); + + // Test removing and adding some new ones. Invalid should fail. + let err: ContractError = app + .execute_contract( + Addr::unchecked(gov_contract_info.address.clone()), + &gov_contract_info.clone(), + &ExecuteMsg::UpdateSnip20List { + to_add: vec!["new".to_string()], + to_remove: vec![gov_token_info.addr.clone().to_string()], + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert!(matches!(err, ContractError::Std(_))); + + // Test that non-DAO can not update the list. + let err: ContractError = app + .execute_contract( + Addr::unchecked("ekez"), + &gov_contract_info.clone(), + &ExecuteMsg::UpdateSnip20List { + to_add: vec![], + to_remove: vec![gov_token_info.addr.clone().to_string()], + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert!(matches!(err, ContractError::Unauthorized {})); + + app.execute_contract( + Addr::unchecked(gov_contract_info.address.clone()), + &gov_contract_info.clone(), + &ExecuteMsg::UpdateSnip20List { + to_add: vec![another_snip20_contract.address.to_string()], + to_remove: vec![gov_token_info.addr.clone().to_string()], + }, + &[], + ) + .unwrap(); + + let snip20_list: Vec = app + .wrap() + .query_wasm_smart( + gov_contract_info.code_hash, + gov_contract_info.address, + &QueryMsg::Snip20TokenList { + start_after: None, + limit: None, + }, + ) + .unwrap(); + assert_eq!(snip20_list, vec![another_snip20_contract.address]); +} + +#[test] +fn test_snip721_receive() { + let (gov_contract_info, mut app) = do_standard_instantiate(true, None); + + let snip721_info = app.store_code(snip721_contract()); + + let snip721_contract_info = app + .instantiate_contract( + snip721_info.clone(), + Addr::unchecked(CREATOR_ADDR), + &snip721_reference_impl::msg::InstantiateMsg { + name: "ekez".to_string(), + symbol: "ekez".to_string(), + admin: None, + entropy: "entropy".to_string(), + royalty_info: None, + config: None, + post_init_callback: None, + }, + &[], + "snip721", + None, + ) + .unwrap(); + + let another_snip721 = app + .instantiate_contract( + snip721_info.clone(), + Addr::unchecked(CREATOR_ADDR), + &snip721_reference_impl::msg::InstantiateMsg { + name: "ekez".to_string(), + symbol: "ekez".to_string(), + admin: None, + entropy: "entropy".to_string(), + royalty_info: None, + config: None, + post_init_callback: None, + }, + &[], + "snip721", + None, + ) + .unwrap(); + + app.execute_contract( + Addr::unchecked(CREATOR_ADDR), + &snip721_contract_info.clone(), + &snip721_reference_impl::msg::ExecuteMsg::MintNft { + token_id: Some("ekez".to_string()), + owner: Some(CREATOR_ADDR.to_string()), + public_metadata: None, + private_metadata: None, + serial_number: None, + royalty_info: None, + transferable: Some(true), + memo: None, + padding: None, + }, + &[], + ) + .unwrap(); + + app.execute_contract( + Addr::unchecked(CREATOR_ADDR), + &snip721_contract_info.clone(), + &snip721_reference_impl::msg::ExecuteMsg::SendNft { + contract: gov_contract_info.address.to_string(), + token_id: "ekez".to_string(), + msg: Some(to_binary("").unwrap()), + receiver_info: Some(ReceiverInfo { + recipient_code_hash: gov_contract_info.code_hash.clone(), + also_implements_batch_receive_nft: Some(true), + }), + memo: None, + padding: None, + }, + &[], + ) + .unwrap(); + + let snip721_list: Vec = app + .wrap() + .query_wasm_smart( + gov_contract_info.code_hash.clone(), + gov_contract_info.address.clone(), + &QueryMsg::Snip721TokenList { + start_after: None, + limit: None, + }, + ) + .unwrap(); + assert_eq!(snip721_list, vec![CREATOR_ADDR.to_string()]); + + // Try to add an invalid snip721. + let err: ContractError = app + .execute_contract( + Addr::unchecked(gov_contract_info.address.clone()), + &gov_contract_info.clone(), + &ExecuteMsg::UpdateSnip721List { + to_add: vec!["new".to_string(), snip721_contract_info.address.to_string()], + to_remove: vec![snip721_contract_info.address.to_string()], + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert!(matches!(err, ContractError::Std(_))); + + // Test that non-DAO can not update the list. + let err: ContractError = app + .execute_contract( + Addr::unchecked("ekez"), + &gov_contract_info.clone(), + &ExecuteMsg::UpdateSnip721List { + to_add: vec![], + to_remove: vec![snip721_contract_info.address.clone().to_string()], + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert!(matches!(err, ContractError::Unauthorized {})); + + // Add a real snip721. + app.execute_contract( + Addr::unchecked(gov_contract_info.address.clone()), + &gov_contract_info.clone(), + &ExecuteMsg::UpdateSnip721List { + to_add: vec![another_snip721.address.to_string()], + to_remove: vec![CREATOR_ADDR.to_string()], + }, + &[], + ) + .unwrap(); + + let snip20_list: Vec = app + .wrap() + .query_wasm_smart( + gov_contract_info.code_hash, + gov_contract_info.address, + &QueryMsg::Snip721TokenList { + start_after: None, + limit: None, + }, + ) + .unwrap(); + assert_eq!(snip20_list, vec![another_snip721.address]); +} + +#[test] +fn test_pause() { + let (core_contract_info, mut app) = do_standard_instantiate(false, None); + + let start_height = app.block_info().height; + + let proposal_modules: Vec = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &QueryMsg::ProposalModules { + start_after: None, + limit: None, + }, + ) + .unwrap(); + + assert_eq!(proposal_modules.len(), 1); + let proposal_module = proposal_modules.into_iter().next().unwrap(); + + let paused: PauseInfoResponse = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &QueryMsg::PauseInfo {}, + ) + .unwrap(); + assert_eq!(paused, PauseInfoResponse::Unpaused {}); + let all_state: DumpStateResponse = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &QueryMsg::DumpState {}, + ) + .unwrap(); + assert_eq!(all_state.pause_info, PauseInfoResponse::Unpaused {}); + + // DAO is not paused. Check that we can execute things. + // + // Tests intentionally use the core address to send these + // messsages to simulate a worst case scenerio where the core + // contract has a vulnerability. + app.execute_contract( + core_contract_info.address.clone(), + &core_contract_info.clone(), + &ExecuteMsg::UpdateConfig { + config: Config { + dao_uri: None, + name: "The Empire Strikes Back".to_string(), + description: "haha lol we have pwned your DAO".to_string(), + image_url: None, + }, + }, + &[], + ) + .unwrap(); + + // Oh no the DAO is under attack! Quick! Pause the DAO while we + // figure out what to do! + let err: ContractError = app + .execute_contract( + proposal_module.address.clone(), + &core_contract_info.clone(), + &ExecuteMsg::Pause { + duration: Duration::Height(10), + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + + // Only the DAO may call this on itself. Proposal modules must use + // the execute hook. + assert_eq!(err, ContractError::Unauthorized {}); + + app.execute_contract( + proposal_module.address.clone(), + &core_contract_info.clone(), + &ExecuteMsg::ExecuteProposalHook { + msgs: vec![WasmMsg::Execute { + contract_addr: core_contract_info.address.clone().to_string(), + code_hash: core_contract_info.code_hash.clone(), + msg: to_binary(&ExecuteMsg::Pause { + duration: Duration::Height(10), + }) + .unwrap(), + funds: vec![], + } + .into()], + }, + &[], + ) + .unwrap(); + + let paused: PauseInfoResponse = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &QueryMsg::PauseInfo {}, + ) + .unwrap(); + assert_eq!( + paused, + PauseInfoResponse::Paused { + expiration: Expiration::AtHeight(start_height + 10) + } + ); + let all_state: DumpStateResponse = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &QueryMsg::DumpState {}, + ) + .unwrap(); + assert_eq!( + all_state.pause_info, + PauseInfoResponse::Paused { + expiration: Expiration::AtHeight(start_height + 10) + } + ); + + let err: ContractError = app + .execute_contract( + core_contract_info.address.clone(), + &core_contract_info.clone(), + &ExecuteMsg::UpdateConfig { + config: Config { + dao_uri: None, + name: "The Empire Strikes Back Again".to_string(), + description: "haha lol we have pwned your DAO again".to_string(), + image_url: None, + }, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + + assert!(matches!(err, ContractError::Paused { .. })); + + let err: ContractError = app + .execute_contract( + proposal_module.address.clone(), + &core_contract_info.clone(), + &ExecuteMsg::ExecuteProposalHook { + msgs: vec![WasmMsg::Execute { + contract_addr: core_contract_info.address.clone().to_string(), + code_hash: core_contract_info.code_hash.clone(), + msg: to_binary(&ExecuteMsg::Pause { + duration: Duration::Height(10), + }) + .unwrap(), + funds: vec![], + } + .into()], + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + + assert!(matches!(err, ContractError::Paused { .. })); + + app.update_block(|block| block.height += 9); + + // Still not unpaused. + let err: ContractError = app + .execute_contract( + proposal_module.address.clone(), + &core_contract_info.clone(), + &ExecuteMsg::ExecuteProposalHook { + msgs: vec![WasmMsg::Execute { + contract_addr: core_contract_info.address.clone().to_string(), + code_hash: core_contract_info.code_hash.clone(), + msg: to_binary(&ExecuteMsg::Pause { + duration: Duration::Height(10), + }) + .unwrap(), + funds: vec![], + } + .into()], + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + + assert!(matches!(err, ContractError::Paused { .. })); + + app.update_block(|block| block.height += 1); + + let paused: PauseInfoResponse = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &QueryMsg::PauseInfo {}, + ) + .unwrap(); + assert_eq!(paused, PauseInfoResponse::Unpaused {}); + let all_state: DumpStateResponse = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &QueryMsg::DumpState {}, + ) + .unwrap(); + assert_eq!(all_state.pause_info, PauseInfoResponse::Unpaused {}); + + // Now its unpaused so we should be able to pause again. + app.execute_contract( + proposal_module.address, + &core_contract_info.clone(), + &ExecuteMsg::ExecuteProposalHook { + msgs: vec![WasmMsg::Execute { + contract_addr: core_contract_info.address.clone().to_string(), + code_hash: core_contract_info.code_hash.clone(), + msg: to_binary(&ExecuteMsg::Pause { + duration: Duration::Height(10), + }) + .unwrap(), + funds: vec![], + } + .into()], + }, + &[], + ) + .unwrap(); + + let paused: PauseInfoResponse = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &QueryMsg::PauseInfo {}, + ) + .unwrap(); + assert_eq!( + paused, + PauseInfoResponse::Paused { + expiration: Expiration::AtHeight(start_height + 20) + } + ); + let all_state: DumpStateResponse = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash, + core_contract_info.address, + &QueryMsg::DumpState {}, + ) + .unwrap(); + assert_eq!( + all_state.pause_info, + PauseInfoResponse::Paused { + expiration: Expiration::AtHeight(start_height + 20) + } + ); +} + +#[test] +fn test_dump_state_proposal_modules() { + let (core_contract_info, app) = do_standard_instantiate(false, None); + let proposal_modules: Vec = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &QueryMsg::ProposalModules { + start_after: None, + limit: None, + }, + ) + .unwrap(); + + assert_eq!(proposal_modules.len(), 1); + let proposal_module = proposal_modules.into_iter().next().unwrap(); + + let all_state: DumpStateResponse = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash, + core_contract_info.address, + &QueryMsg::DumpState {}, + ) + .unwrap(); + assert_eq!(all_state.pause_info, PauseInfoResponse::Unpaused {}); + assert_eq!(all_state.proposal_modules.len(), 1); + assert_eq!(all_state.proposal_modules[0], proposal_module); +} + +#[test] +fn test_execute_stargate_msg() { + let (core_contract_info, mut app) = do_standard_instantiate(true, None); + let proposal_modules: Vec = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &QueryMsg::ProposalModules { + start_after: None, + limit: None, + }, + ) + .unwrap(); + + assert_eq!(proposal_modules.len(), 1); + let proposal_module = proposal_modules.into_iter().next().unwrap(); + + let res = app.execute_contract( + proposal_module.address, + &core_contract_info, + &ExecuteMsg::ExecuteProposalHook { + msgs: vec![CosmosMsg::Stargate { + type_url: "foo_type".to_string(), + value: to_binary("foo_bin").unwrap(), + }], + }, + &[], + ); + // TODO: Once cw-multi-test supports executing stargate/ibc messages we can change this test assert + assert!(res.is_err()); +} + +#[test] +fn test_module_prefixes() { + let mut app = App::default(); + let govmod_info = app.store_code(sudo_proposal_contract()); + let gov_info = app.store_code(cw_core_contract()); + let snip20_info = app.store_code(snip20_contract()); + let snip721_info = app.store_code(snip721_contract()); + let query_auth_info = app.store_code(query_auth_contract()); + + let govmod_instantiate = dao_proposal_sudo::msg::InstantiateMsg { + root: CREATOR_ADDR.to_string(), + dao_code_hash: gov_info.code_hash.clone(), + }; + + let gov_instantiate = InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that builds DAOs.".to_string(), + image_url: None, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: govmod_info.code_id, + code_hash: govmod_info.code_hash.clone(), + msg: to_binary(&govmod_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "voting module".to_string(), + }, + proposal_modules_instantiate_info: vec![ + ModuleInstantiateInfo { + code_id: govmod_info.code_id, + code_hash: govmod_info.code_hash.clone(), + msg: to_binary(&govmod_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "proposal module 1".to_string(), + }, + ModuleInstantiateInfo { + code_id: govmod_info.code_id, + code_hash: govmod_info.code_hash.clone(), + msg: to_binary(&govmod_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "proposal module 2".to_string(), + }, + ModuleInstantiateInfo { + code_id: govmod_info.code_id, + code_hash: govmod_info.code_hash.clone(), + msg: to_binary(&govmod_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "proposal module 2".to_string(), + }, + ], + initial_items: None, + query_auth_code_id: query_auth_info.code_id, + query_auth_code_hash: query_auth_info.code_hash, + prng_seed: "Seeed".to_string(), + snip20_code_hash: snip20_info.code_hash, + snip721_code_hash: snip721_info.code_hash, + }; + + let gov_contract_info = app + .instantiate_contract( + gov_info, + Addr::unchecked(CREATOR_ADDR), + &gov_instantiate, + &[], + "cw-governance", + None, + ) + .unwrap(); + + let modules: Vec = app + .wrap() + .query_wasm_smart( + gov_contract_info.code_hash.clone(), + gov_contract_info.address.clone(), + &QueryMsg::ProposalModules { + start_after: None, + limit: None, + }, + ) + .unwrap(); + + assert_eq!(modules.len(), 3); + + let module_1 = &modules[0]; + assert_eq!(module_1.status, ProposalModuleStatus::Enabled {}); + assert_eq!(module_1.prefix, "A"); + assert_eq!(&module_1.address, &modules[0].address); + + let module_2 = &modules[1]; + assert_eq!(module_2.status, ProposalModuleStatus::Enabled {}); + assert_eq!(module_2.prefix, "B"); + assert_eq!(&module_2.address, &modules[1].address); + + let module_3 = &modules[2]; + assert_eq!(module_3.status, ProposalModuleStatus::Enabled {}); + assert_eq!(module_3.prefix, "C"); + assert_eq!(&module_3.address, &modules[2].address); +} + +fn get_active_modules(app: &App, gov_info: ContractInfo) -> Vec { + let modules: Vec = app + .wrap() + .query_wasm_smart( + gov_info.code_hash, + gov_info.address, + &QueryMsg::ProposalModules { + start_after: None, + limit: None, + }, + ) + .unwrap(); + + modules + .into_iter() + .filter(|module: &ProposalModule| module.status == ProposalModuleStatus::Enabled) + .collect() +} + +fn query_proposal_module_count(app: &App, core_info: &ContractInfo) -> ProposalModuleCountResponse { + app.wrap() + .query_wasm_smart( + core_info.code_hash.clone(), + core_info.address.clone(), + &QueryMsg::ProposalModuleCount {}, + ) + .unwrap() +} + +#[test] +fn test_add_remove_subdaos() { + let (core_contract_info, mut app) = do_standard_instantiate(false, None); + + test_unauthorized( + &mut app, + core_contract_info.clone(), + ExecuteMsg::UpdateSubDaos { + to_add: vec![], + to_remove: vec![], + }, + ); + + let to_add: Vec = vec![ + SubDao { + addr: "subdao001".to_string(), + code_hash: "subdao001_code_hash".to_string(), + charter: None, + }, + SubDao { + addr: "subdao002".to_string(), + code_hash: "subdao002_code_hash".to_string(), + charter: Some("cool charter bro".to_string()), + }, + SubDao { + addr: "subdao005".to_string(), + code_hash: "subdao005_code_hash".to_string(), + charter: None, + }, + SubDao { + addr: "subdao007".to_string(), + code_hash: "subdao007_code_hash".to_string(), + charter: None, + }, + ]; + let to_remove: Vec = vec![]; + + app.execute_contract( + Addr::unchecked(core_contract_info.address.clone()), + &core_contract_info.clone(), + &ExecuteMsg::UpdateSubDaos { to_add, to_remove }, + &[], + ) + .unwrap(); + + let res: Vec = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &QueryMsg::ListSubDaos { + start_after: None, + limit: None, + }, + ) + .unwrap(); + + assert_eq!(res.len(), 4); + + let to_remove: Vec = vec!["subdao005".to_string()]; + + app.execute_contract( + Addr::unchecked(core_contract_info.address.clone()), + &core_contract_info.clone(), + &ExecuteMsg::UpdateSubDaos { + to_add: vec![], + to_remove, + }, + &[], + ) + .unwrap(); + + let res: Vec = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &QueryMsg::ListSubDaos { + start_after: None, + limit: None, + }, + ) + .unwrap(); + + assert_eq!(res.len(), 3); + + let test_res: SubDao = SubDao { + addr: "subdao002".to_string(), + code_hash: "subdao002_code_hash".to_string(), + charter: Some("cool charter bro".to_string()), + }; + + assert_eq!(res[1], test_res); + + let full_result_set: Vec = vec![ + SubDao { + addr: "subdao001".to_string(), + code_hash: "subdao001_code_hash".to_string(), + charter: None, + }, + SubDao { + addr: "subdao002".to_string(), + code_hash: "subdao002_code_hash".to_string(), + charter: Some("cool charter bro".to_string()), + }, + SubDao { + addr: "subdao007".to_string(), + code_hash: "subdao007_code_hash".to_string(), + charter: None, + }, + ]; + + assert_eq!(res, full_result_set); +} + +#[test] +pub fn test_migrate_update_version() { + let mut deps = mock_dependencies(); + secret_cw2::set_contract_version(&mut deps.storage, "my-contract", "1.0.0").unwrap(); + migrate(deps.as_mut(), mock_env(), MigrateMsg {}).unwrap(); + let version = secret_cw2::get_contract_version(&deps.storage).unwrap(); + assert_eq!(version.version, CONTRACT_VERSION); + assert_eq!(version.contract, CONTRACT_NAME); +} + +#[test] +fn test_query_info() { + let (core_contract_info, app) = do_standard_instantiate(true, None); + let res: InfoResponse = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash, + core_contract_info.address, + &QueryMsg::Info {}, + ) + .unwrap(); + assert_eq!( + res, + InfoResponse { + info: ContractVersion { + contract: CONTRACT_NAME.to_string(), + version: CONTRACT_VERSION.to_string() + } + } + ) +} diff --git a/contracts/external/dao-migrator/src/contract.rs b/contracts/external/dao-migrator/src/contract.rs index f82c784..8cf3021 100644 --- a/contracts/external/dao-migrator/src/contract.rs +++ b/contracts/external/dao-migrator/src/contract.rs @@ -113,20 +113,13 @@ fn execute_migration_v1_v2( .proposal_params .clone() .into_iter() - .map(|(addr, proposal_params)| { + .map(|(addr, _proposal_params)| { ( addr, CodeIdPair::new( v1_code_ids_and_hashes.proposal_single, v2_code_ids_and_hashes.proposal_single, - MigrationMsgs::DaoProposalSingle( - dao_proposal_single::msg::MigrateMsg::FromV1 { - close_proposal_on_execution_failure: proposal_params - .close_proposal_on_execution_failure, - pre_propose_info: proposal_params.pre_propose_info, - veto: proposal_params.veto, - }, - ), + MigrationMsgs::DaoProposalSingle(dao_proposal_single::msg::MigrateMsg {}), ), ) }) diff --git a/contracts/external/snip20-reference-impl/src/msg.rs b/contracts/external/snip20-reference-impl/src/msg.rs index 6c16e88..072b50d 100644 --- a/contracts/external/snip20-reference-impl/src/msg.rs +++ b/contracts/external/snip20-reference-impl/src/msg.rs @@ -1,6 +1,8 @@ #![allow(clippy::field_reassign_with_default)] // This is triggered in `#[derive(JsonSchema)]` +use cosmwasm_schema::cw_serde; use schemars::JsonSchema; +use secret_toolkit::utils::InitCallback; use serde::{Deserialize, Serialize}; use crate::batch; @@ -9,8 +11,7 @@ use crate::transaction_history::{ExtendedTx, Tx}; use cosmwasm_std::{Addr, Api, Binary, StdError, StdResult, Uint128}; use secret_toolkit::permit::Permit; -#[cfg_attr(test, derive(Eq, PartialEq))] -#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cw_serde] pub struct InitialBalance { pub address: String, pub amount: Uint128, @@ -34,6 +35,10 @@ impl InstantiateMsg { } } +impl InitCallback for InstantiateMsg { + const BLOCK_SIZE: usize = 256; +} + /// This type represents optional configuration values which can be overridden. /// All values are optional and have defaults which are more private by default, /// but can be overridden if necessary diff --git a/contracts/proposal/dao-proposal-condorcet/src/contract.rs b/contracts/proposal/dao-proposal-condorcet/src/contract.rs index 705d3c5..eb799ef 100644 --- a/contracts/proposal/dao-proposal-condorcet/src/contract.rs +++ b/contracts/proposal/dao-proposal-condorcet/src/contract.rs @@ -6,7 +6,7 @@ use cosmwasm_std::{ }; use dao_interface::state::AnyContractInfo; -use dao_interface::ReplyEvent; +use dao_voting::reply::TaggedReplyId; use dao_voting::voting::{get_total_power, get_voting_power}; use secret_cw2::set_contract_version; use shade_protocol::basic_staking::Auth; @@ -15,7 +15,7 @@ use crate::config::UncheckedConfig; use crate::error::ContractError; use crate::msg::{Choice, ExecuteMsg, InstantiateMsg, QueryMsg}; use crate::proposal::{Proposal, ProposalResponse, Status}; -use crate::state::{next_proposal_id, CONFIG, DAO, PROPOSAL, REPLY_IDS, TALLY, VOTE}; +use crate::state::{next_proposal_id, CONFIG, DAO, PROPOSAL, TALLY, VOTE}; use crate::tally::Tally; use crate::vote::Vote; @@ -223,12 +223,11 @@ fn execute_execute( .unwrap() .update_status(&env.block, &tally.clone().unwrap()) { - let msgs = proposal.clone().unwrap().set_executed( - deps.storage, - dao.addr, - dao.code_hash.clone(), - winner, - )?; + let msgs = + proposal + .clone() + .unwrap() + .set_executed(dao.addr, dao.code_hash.clone(), winner)?; PROPOSAL.insert(deps.storage, &proposal_id, &proposal.clone().unwrap())?; Ok(Response::default() @@ -307,9 +306,9 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { #[cfg_attr(not(feature = "library"), entry_point)] pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result { - let repl = REPLY_IDS.get_event(deps.storage, msg.id)?; + let repl = TaggedReplyId::new(msg.id)?; match repl { - ReplyEvent::FailedProposalExecution { proposal_id } => match msg.clone().result { + TaggedReplyId::FailedProposalExecution(proposal_id) => match msg.clone().result { SubMsgResult::Err(err) => Err(ContractError::Std(StdError::GenericErr { msg: err })), SubMsgResult::Ok(_) => { let proposal = PROPOSAL.get(deps.storage, &(proposal_id as u32)); diff --git a/contracts/proposal/dao-proposal-condorcet/src/proposal.rs b/contracts/proposal/dao-proposal-condorcet/src/proposal.rs index 187bd1a..64b628e 100644 --- a/contracts/proposal/dao-proposal-condorcet/src/proposal.rs +++ b/contracts/proposal/dao-proposal-condorcet/src/proposal.rs @@ -1,14 +1,15 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, BlockInfo, StdResult, Storage, SubMsg, Uint128}; -use dao_interface::ReplyEvent; -use dao_voting::{threshold::PercentageThreshold, voting::does_vote_count_pass}; +use cosmwasm_std::{Addr, BlockInfo, StdResult, SubMsg, Uint128}; +use dao_voting::{ + reply::mask_proposal_execution_proposal_id, threshold::PercentageThreshold, + voting::does_vote_count_pass, +}; use secret_toolkit::utils::HandleCallback; use secret_utils::Expiration; use crate::{ config::Config, msg::Choice, - state::REPLY_IDS, tally::{Tally, Winner}, }; @@ -155,7 +156,6 @@ impl Proposal { /// submessage to be executed. pub(crate) fn set_executed( &mut self, - store: &mut dyn Storage, dao: Addr, dao_code_hash: String, winner: u32, @@ -168,15 +168,11 @@ impl Proposal { let core_exec = dao_interface::msg::ExecuteMsg::ExecuteProposalHook { msgs }; Ok(if self.close_on_execution_failure { - let reply_id = REPLY_IDS.add_event( - store, - ReplyEvent::FailedProposalExecution { - proposal_id: self.id as u64, - }, - ); + let masked_id = mask_proposal_execution_proposal_id(self.id as u64); + SubMsg::reply_on_error( core_exec.to_cosmos_msg(dao_code_hash.clone(), dao.clone().to_string(), None)?, - reply_id.unwrap(), + masked_id, ) } else { SubMsg::new(core_exec.to_cosmos_msg(dao_code_hash, dao.to_string(), None)?) diff --git a/contracts/proposal/dao-proposal-condorcet/src/state.rs b/contracts/proposal/dao-proposal-condorcet/src/state.rs index 93b2a31..2a17eda 100644 --- a/contracts/proposal/dao-proposal-condorcet/src/state.rs +++ b/contracts/proposal/dao-proposal-condorcet/src/state.rs @@ -1,6 +1,5 @@ use cosmwasm_std::{Addr, StdResult, Storage}; use dao_interface::state::AnyContractInfo; -use dao_interface::ReplyIds; use secret_storage_plus::Item; use secret_toolkit::{serialization::Json, storage::Keymap}; @@ -9,10 +8,9 @@ use crate::{config::Config, proposal::Proposal, tally::Tally, vote::Vote}; pub(crate) const DAO: Item = Item::new("dao"); pub(crate) const CONFIG: Item = Item::new("config"); -pub(crate) static TALLY: Keymap = Keymap::new(b"tallys"); -pub(crate) static PROPOSAL: Keymap = Keymap::new(b"proposals"); -pub(crate) static VOTE: Keymap<(u32, Addr), Vote, Json> = Keymap::new(b"votes"); -pub(crate) static REPLY_IDS: ReplyIds = ReplyIds::new(b"reply_ids", b"reply_ids_count"); +pub(crate) const TALLY: Keymap = Keymap::new(b"tallys"); +pub(crate) const PROPOSAL: Keymap = Keymap::new(b"proposals"); +pub(crate) const VOTE: Keymap<(u32, Addr), Vote, Json> = Keymap::new(b"votes"); pub(crate) fn next_proposal_id(storage: &dyn Storage) -> StdResult { PROPOSAL diff --git a/contracts/proposal/dao-proposal-multiple/src/contract.rs b/contracts/proposal/dao-proposal-multiple/src/contract.rs index 00f45a5..c4e2e7e 100644 --- a/contracts/proposal/dao-proposal-multiple/src/contract.rs +++ b/contracts/proposal/dao-proposal-multiple/src/contract.rs @@ -1,5 +1,3 @@ -use std::borrow::Borrow; - #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ @@ -15,7 +13,9 @@ use dao_hooks::vote::new_vote_hooks; use dao_interface::replies::parse_reply_address_from_event; use dao_interface::state::{AnyContractInfo, VotingModuleInfo}; use dao_interface::voting::IsActiveResponse; -use dao_interface::ReplyEvent; +use dao_voting::reply::{ + failed_pre_propose_module_hook_id, mask_proposal_execution_proposal_id, TaggedReplyId, +}; use dao_voting::veto::{VetoConfig, VetoError}; use dao_voting::{ multiple_choice::{ @@ -35,7 +35,7 @@ use shade_protocol::query_auth::helpers::{ }; use shade_protocol::Contract; -use crate::state::{Ballot, DAO, REPLY_IDS}; +use crate::state::{Ballot, DAO, PRE_PROPOSE_CODE_HASH}; use crate::{msg::MigrateMsg, state::CREATION_POLICY}; use crate::{ msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, @@ -72,9 +72,9 @@ pub fn instantiate( let (min_voting_period, max_voting_period) = validate_voting_period(msg.min_voting_period, msg.max_voting_period)?; - let (initial_policy, pre_propose_messages) = msg + let (initial_policy, pre_propose_messages, code_hash) = msg .pre_propose_info - .into_initial_policy_and_messages(deps.storage, info.sender.clone(), REPLY_IDS.borrow())?; + .into_initial_policy_and_messages(info.sender.clone())?; // if veto is configured, validate its fields if let Some(veto_config) = &msg.veto { @@ -101,6 +101,7 @@ pub fn instantiate( PROPOSAL_COUNT.save(deps.storage, &0)?; CONFIG.save(deps.storage, &config)?; CREATION_POLICY.save(deps.storage, &initial_policy)?; + PRE_PROPOSE_CODE_HASH.save(deps.storage, &code_hash)?; Ok(Response::default() .add_submessages(pre_propose_messages) @@ -567,17 +568,15 @@ pub fn execute_execute( }; match config.close_proposal_on_execution_failure { true => { - let reply_id = REPLY_IDS.add_event( - deps.storage, - ReplyEvent::FailedProposalExecution { proposal_id }, - )?; + let masked_proposal_id = mask_proposal_execution_proposal_id(proposal_id); + Response::default().add_submessage(SubMsg::reply_on_error( execute_message.to_cosmos_msg( dao_info.code_hash.clone(), dao_info.addr.clone().to_string(), None, )?, - reply_id, + masked_proposal_id, )) } false => Response::default().add_message(execute_message.to_cosmos_msg( @@ -710,11 +709,7 @@ pub fn execute_update_proposal_creation_policy( return Err(ContractError::Unauthorized {}); } - let (initial_policy, messages) = new_info.into_initial_policy_and_messages( - deps.storage, - dao_info.addr, - REPLY_IDS.borrow(), - )?; + let (initial_policy, messages, _) = new_info.into_initial_policy_and_messages(dao_info.addr)?; CREATION_POLICY.save(deps.storage, &initial_policy)?; Ok(Response::default() @@ -985,31 +980,45 @@ pub fn query_list_proposals( start_after: Option, limit: Option, ) -> StdResult { - let limit = limit.unwrap_or(DEFAULT_LIMIT); - // let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + // Use the default limit if none is provided, and convert safely to usize + let limit: usize = limit + .unwrap_or(DEFAULT_LIMIT) + .try_into() + .map_err(|_| StdError::generic_err("Limit too large"))?; + + // Early return if the limit is 0 + if limit == 0 { + return to_binary(&ProposalListResponse { proposals: vec![] }); + } let mut proposals_res: Vec = Vec::new(); + let start_after_id = start_after; // Keep track of the start_after value - let mut start = start_after; // Clone start_after to mutate it if necessary + let binding = PROPOSALS; - let binding = &PROPOSALS; + // Get an iterator over the proposals stored let iter = binding.iter(deps.storage)?; + for item in iter { let (id, proposal) = item?; - if let Some(start_after) = &start { - if &id == start_after { - // If we found the start point, reset it to start iterating - start = None; + + // If `start_after` is specified, skip proposals until ID is greater than `start_after` + if let Some(start_after_value) = start_after_id { + if id <= start_after_value { + continue; // Skip this proposal if its ID is less than or equal to start_after } } - if start.is_none() { - proposals_res.push(ProposalResponse { id, proposal }); - if proposals_res.len() >= limit.try_into().unwrap() { - break; // Break out of loop if limit reached - } + + // Now that we're beyond the start_after point, collect the proposals + proposals_res.push(ProposalResponse { id, proposal }); + + // Stop if we reach the requested limit + if proposals_res.len() >= limit { + break; } } + // Return the list of proposals to_binary(&ProposalListResponse { proposals: proposals_res, }) @@ -1021,42 +1030,42 @@ pub fn query_reverse_proposals( start_before: Option, limit: Option, ) -> StdResult { - // let limit = limit.unwrap_or(DEFAULT_LIMIT); - // let max = start_before.map(Bound::exclusive); - // let props: Vec = PROPOSALS - // .range(deps.storage, None, max, cosmwasm_std::Order::Descending) - // .take(limit as usize) - // .collect::, _>>()? - // .into_iter() - // .map(|(id, proposal)| proposal.into_response(&env.block, id)) - // .collect::>>()?; - - // to_binary(&ProposalListResponse { proposals: props }) - - let limit = limit.unwrap_or(DEFAULT_LIMIT); - // let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + // Use the provided limit or fall back to DEFAULT_LIMIT + let limit: usize = limit + .unwrap_or(DEFAULT_LIMIT) + .try_into() + .map_err(|_| StdError::generic_err("Limit too large"))?; + + // Early return if limit is 0 + if limit == 0 { + return to_binary(&ProposalListResponse { proposals: vec![] }); + } let mut proposals_res: Vec = Vec::new(); - let binding = &PROPOSALS; let iter = binding.iter(deps.storage)?; + + // Iterate in reverse over proposals for item in iter.rev() { let (id, proposal) = item?; - if let Some(start_before) = start_before { - if id < start_before { - proposals_res.push(ProposalResponse { id, proposal }); - if proposals_res.len() >= limit as usize { - break; // Break out of loop if limit reached - } - } - } else { - proposals_res.push(ProposalResponse { id, proposal }); - if proposals_res.len() >= limit as usize { - break; // Break out of loop if limit reached + + // Skip proposals greater than or equal to start_before + if let Some(start_before_value) = start_before { + if id >= start_before_value { + continue; // Skip this proposal } } + + // Collect the proposal into the result set + proposals_res.push(ProposalResponse { id, proposal }); + + // Stop collecting proposals when the limit is reached + if proposals_res.len() >= limit { + break; + } } + // Return the list of proposals as a binary response to_binary(&ProposalListResponse { proposals: proposals_res, }) @@ -1091,10 +1100,94 @@ pub fn query_info(deps: Deps) -> StdResult { #[cfg_attr(not(feature = "library"), entry_point)] pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result { - let repl = REPLY_IDS.get_event(deps.storage, msg.id)?; + // let repl = REPLY_IDS.get_event(deps.storage, msg.id)?; + // match repl { + // ReplyEvent::FailedProposalExecution { proposal_id } => match msg.clone().result { + // SubMsgResult::Err(err) => Err(ContractError::Std(StdError::GenericErr { msg: err })), + // SubMsgResult::Ok(_) => { + // let proposals = PROPOSALS.get(deps.storage, &proposal_id); + // if proposals.clone().is_some() { + // proposals.clone().unwrap().status = Status::ExecutionFailed; + // } else { + // return Err(ContractError::NoSuchProposal { id: proposal_id }); + // } + // PROPOSALS.insert(deps.storage, &proposal_id, &proposals.unwrap())?; + + // Ok(Response::new() + // .add_attribute("proposal_execution_failed", proposal_id.to_string()) + // .add_attribute("error", msg.result.into_result().err().unwrap_or_default())) + // } + // }, + // ReplyEvent::FailedProposalHook { idx } => match msg.result { + // SubMsgResult::Err(err) => Err(ContractError::Std(StdError::GenericErr { msg: err })), + // SubMsgResult::Ok(_) => { + // let hook_item = PROPOSAL_HOOKS.remove_hook_by_index(deps.storage, idx)?; + // Ok(Response::new().add_attribute( + // "removed_proposal_hook", + // format!("{0}:{idx}", hook_item.addr), + // )) + // } + // }, + // ReplyEvent::FailedVoteHook { idx } => match msg.result { + // SubMsgResult::Err(err) => Err(ContractError::Std(StdError::GenericErr { msg: err })), + // SubMsgResult::Ok(_) => { + // let hook_item = VOTE_HOOKS.remove_hook_by_index(deps.storage, idx)?; + // Ok(Response::new() + // .add_attribute("removed_vote_hook", format!("{0}:{idx}", hook_item.addr))) + // } + // }, + // ReplyEvent::PreProposalModuleInstantiate { code_hash } => match msg.result { + // SubMsgResult::Err(err) => Err(ContractError::Std(StdError::GenericErr { msg: err })), + // SubMsgResult::Ok(res) => { + // let address = parse_reply_address_from_event(res.clone()); + + // CREATION_POLICY.save( + // deps.storage, + // &ProposalCreationPolicy::Module { + // addr: deps.api.addr_validate(&address.clone())?, + // code_hash, + // }, + // )?; + + // // per the cosmwasm docs, we shouldn't have to forward + // // data like this, yet here we are and it does not work if + // // we do not. + // // + // // + // match res.data { + // Some(data) => Ok(Response::new() + // .add_attribute("update_pre_propose_module", address.clone().to_string()) + // .set_data(data)), + // None => Ok(Response::new() + // .add_attribute("update_pre_propose_module", address.to_string())), + // } + // } + // }, + // ReplyEvent::FailedPreProposeModuleHook {} => { + // let addr = match CREATION_POLICY.load(deps.storage)? { + // ProposalCreationPolicy::Anyone {} => { + // // Something is off if we're getting this + // // reply and we don't have a pre-propose + // // module installed. This should be + // // unreachable. + // return Err(ContractError::InvalidReplyID { id: msg.id }); + // } + // ProposalCreationPolicy::Module { addr, code_hash: _ } => { + // // If we are here, our pre-propose module has + // // errored while receiving a proposal + // // hook. Rest in peace pre-propose module. + // CREATION_POLICY.save(deps.storage, &ProposalCreationPolicy::Anyone {})?; + // addr + // } + // }; + // Ok(Response::new().add_attribute("failed_prepropose_hook", format!("{addr}"))) + // } + // _ => Err(ContractError::UnknownReplyID {}), + // } + + let repl = TaggedReplyId::new(msg.id)?; match repl { - ReplyEvent::FailedProposalExecution { proposal_id } => match msg.clone().result { - SubMsgResult::Err(err) => Err(ContractError::Std(StdError::GenericErr { msg: err })), + TaggedReplyId::FailedProposalExecution(proposal_id) => match msg.result { SubMsgResult::Ok(_) => { let proposals = PROPOSALS.get(deps.storage, &proposal_id); if proposals.clone().is_some() { @@ -1108,9 +1201,10 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result match msg.result { SubMsgResult::Err(err) => Err(ContractError::Std(StdError::GenericErr { msg: err })), + }, + + TaggedReplyId::FailedProposalHook(idx) => match msg.result { SubMsgResult::Ok(_) => { let hook_item = PROPOSAL_HOOKS.remove_hook_by_index(deps.storage, idx)?; Ok(Response::new().add_attribute( @@ -1118,20 +1212,22 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result match msg.result { SubMsgResult::Err(err) => Err(ContractError::Std(StdError::GenericErr { msg: err })), + }, + + TaggedReplyId::FailedVoteHook(idx) => match msg.result { SubMsgResult::Ok(_) => { let hook_item = VOTE_HOOKS.remove_hook_by_index(deps.storage, idx)?; Ok(Response::new() .add_attribute("removed_vote_hook", format!("{0}:{idx}", hook_item.addr))) } - }, - ReplyEvent::PreProposalModuleInstantiate { code_hash } => match msg.result { SubMsgResult::Err(err) => Err(ContractError::Std(StdError::GenericErr { msg: err })), - SubMsgResult::Ok(res) => { - let address = parse_reply_address_from_event(res.clone()); + }, + TaggedReplyId::PreProposeModuleInstantiation => match msg.result { + SubMsgResult::Ok(sub_msg_response) => { + let address = parse_reply_address_from_event(sub_msg_response.clone()); + let code_hash = PRE_PROPOSE_CODE_HASH.load(deps.storage)?; CREATION_POLICY.save( deps.storage, &ProposalCreationPolicy::Module { @@ -1145,35 +1241,42 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result - match res.data { - Some(data) => Ok(Response::new() - .add_attribute("update_pre_propose_module", address.clone().to_string()) - .set_data(data)), - None => Ok(Response::new() - .add_attribute("update_pre_propose_module", address.to_string())), - } + // match sub_msg_response.data { + // Some(data) => Ok(Response::new() + // .add_attribute("update_pre_propose_module", address.clone().to_string()) + // .set_data(data)), + // None => Ok(Response::new() + // .add_attribute("update_pre_propose_module", address.to_string())), + // } + Ok(Response::new().add_attribute("update_pre_propose_module", address.to_string())) } + SubMsgResult::Err(_) => todo!(), + }, + + TaggedReplyId::FailedPreProposeModuleHook => match msg.result { + SubMsgResult::Ok(_) => { + let addr = match CREATION_POLICY.load(deps.storage)? { + ProposalCreationPolicy::Anyone {} => { + // Something is off if we're getting this + // reply and we don't have a pre-propose + // module installed. This should be + // unreachable. + return Err(ContractError::InvalidReplyID { + id: failed_pre_propose_module_hook_id(), + }); + } + ProposalCreationPolicy::Module { addr, .. } => { + // If we are here, our pre-propose module has + // errored while receiving a proposal + // hook. Rest in peace pre-propose module. + CREATION_POLICY.save(deps.storage, &ProposalCreationPolicy::Anyone {})?; + addr + } + }; + Ok(Response::new().add_attribute("failed_prepropose_hook", format!("{addr}"))) + } + SubMsgResult::Err(err) => Err(ContractError::Std(StdError::GenericErr { msg: err })), }, - ReplyEvent::FailedPreProposeModuleHook {} => { - let addr = match CREATION_POLICY.load(deps.storage)? { - ProposalCreationPolicy::Anyone {} => { - // Something is off if we're getting this - // reply and we don't have a pre-propose - // module installed. This should be - // unreachable. - return Err(ContractError::InvalidReplyID { id: msg.id }); - } - ProposalCreationPolicy::Module { addr, code_hash: _ } => { - // If we are here, our pre-propose module has - // errored while receiving a proposal - // hook. Rest in peace pre-propose module. - CREATION_POLICY.save(deps.storage, &ProposalCreationPolicy::Anyone {})?; - addr - } - }; - Ok(Response::new().add_attribute("failed_prepropose_hook", format!("{addr}"))) - } - _ => Err(ContractError::UnknownReplyID {}), } } diff --git a/contracts/proposal/dao-proposal-multiple/src/state.rs b/contracts/proposal/dao-proposal-multiple/src/state.rs index 6426771..3bbf7f1 100644 --- a/contracts/proposal/dao-proposal-multiple/src/state.rs +++ b/contracts/proposal/dao-proposal-multiple/src/state.rs @@ -2,7 +2,6 @@ use crate::proposal::MultipleChoiceProposal; use cosmwasm_std::{Addr, Uint128}; use cw_hooks::Hooks; use dao_interface::state::AnyContractInfo; -use dao_interface::ReplyIds; use dao_voting::{ multiple_choice::{MultipleChoiceVote, VotingStrategy}, pre_propose::ProposalCreationPolicy, @@ -68,8 +67,8 @@ pub struct Ballot { /// The current top level config for the module. pub const CONFIG: Item = Item::new("config"); pub const PROPOSAL_COUNT: Item = Item::new("proposal_count"); -pub static PROPOSALS: Keymap = Keymap::new(b"proposals"); -pub static BALLOTS: Keymap<(u64, Addr), Ballot, Json> = Keymap::new(b"ballots"); +pub const PROPOSALS: Keymap = Keymap::new(b"proposals"); +pub const BALLOTS: Keymap<(u64, Addr), Ballot, Json> = Keymap::new(b"ballots"); /// Consumers of proposal state change hooks. pub const PROPOSAL_HOOKS: Hooks = Hooks::new("proposal_hooks"); /// Consumers of vote hooks. @@ -78,4 +77,4 @@ pub const VOTE_HOOKS: Hooks = Hooks::new("vote_hooks"); /// proposal module (if any). pub const CREATION_POLICY: Item = Item::new("creation_policy"); pub const DAO: Item = Item::new("dao"); -pub static REPLY_IDS: ReplyIds = ReplyIds::new(b"reply_ids", b"reply_ids_count"); +pub const PRE_PROPOSE_CODE_HASH: Item = Item::new("ppch"); diff --git a/contracts/proposal/dao-proposal-single/src/contract.rs b/contracts/proposal/dao-proposal-single/src/contract.rs index 948b29a..f3afbc5 100644 --- a/contracts/proposal/dao-proposal-single/src/contract.rs +++ b/contracts/proposal/dao-proposal-single/src/contract.rs @@ -1,5 +1,3 @@ -use std::borrow::Borrow; - use crate::msg::MigrateMsg; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; @@ -15,11 +13,13 @@ use dao_hooks::vote::new_vote_hooks; use dao_interface::replies::parse_reply_address_from_event; use dao_interface::state::{AnyContractInfo, VotingModuleInfo}; use dao_interface::voting::IsActiveResponse; -use dao_interface::ReplyEvent; use dao_voting::pre_propose::{PreProposeInfo, ProposalCreationPolicy}; use dao_voting::proposal::{ SingleChoiceProposeMsg as ProposeMsg, DEFAULT_LIMIT, MAX_PROPOSAL_SIZE, }; +use dao_voting::reply::{ + failed_pre_propose_module_hook_id, mask_proposal_execution_proposal_id, TaggedReplyId, +}; use dao_voting::status::Status; use dao_voting::threshold::Threshold; use dao_voting::veto::{VetoConfig, VetoError}; @@ -34,10 +34,7 @@ use shade_protocol::query_auth::helpers::{ use shade_protocol::Contract; // use crate::msg::MigrateMsg; use crate::proposal::{next_proposal_id, SingleChoiceProposal}; -use crate::state::{Ballot, Config, CREATION_POLICY, DAO, REPLY_IDS}; -use crate::v1_state::{ - v1_duration_to_v2, v1_expiration_to_v2, v1_status_to_v2, v1_threshold_to_v2, v1_votes_to_v2, -}; +use crate::state::{Ballot, Config, CREATION_POLICY, DAO, PRE_PROPOSE_CODE_HASH}; use crate::{ error::ContractError, msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, @@ -74,9 +71,9 @@ pub fn instantiate( let (min_voting_period, max_voting_period) = validate_voting_period(msg.min_voting_period, msg.max_voting_period)?; - let (initial_policy, pre_propose_messages) = msg + let (initial_policy, pre_propose_messages, code_hash) = msg .pre_propose_info - .into_initial_policy_and_messages(deps.storage, info.sender.clone(), REPLY_IDS.borrow())?; + .into_initial_policy_and_messages(info.sender.clone())?; // if veto is configured, validate its fields if let Some(veto_config) = &msg.veto { @@ -103,6 +100,7 @@ pub fn instantiate( PROPOSAL_COUNT.save(deps.storage, &0)?; CONFIG.save(deps.storage, &config)?; CREATION_POLICY.save(deps.storage, &initial_policy)?; + PRE_PROPOSE_CODE_HASH.save(deps.storage, &code_hash)?; Ok(Response::default() .add_submessages(pre_propose_messages) @@ -449,17 +447,15 @@ pub fn execute_execute( dao_interface::msg::ExecuteMsg::ExecuteProposalHook { msgs: prop.msgs }; match config.close_proposal_on_execution_failure { true => { - let reply_id = REPLY_IDS.add_event( - deps.storage, - ReplyEvent::FailedProposalExecution { proposal_id }, - )?; + let masked_proposal_id = mask_proposal_execution_proposal_id(proposal_id); + Response::default().add_submessage(SubMsg::reply_on_error( execute_message.to_cosmos_msg( dao_info.code_hash.clone(), dao_info.addr.clone().into_string(), None, )?, - reply_id, + masked_proposal_id, )) } false => Response::default().add_message(execute_message.to_cosmos_msg( @@ -734,11 +730,7 @@ pub fn execute_update_proposal_creation_policy( return Err(ContractError::Unauthorized {}); } - let (initial_policy, messages) = new_info.into_initial_policy_and_messages( - deps.storage, - dao_info.addr, - REPLY_IDS.borrow(), - )?; + let (initial_policy, messages, _) = new_info.into_initial_policy_and_messages(dao_info.addr)?; CREATION_POLICY.save(deps.storage, &initial_policy)?; Ok(Response::default() @@ -972,31 +964,45 @@ pub fn query_list_proposals( start_after: Option, limit: Option, ) -> StdResult { - let limit = limit.unwrap_or(DEFAULT_LIMIT); - // let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + // Use the default limit if none is provided, and convert safely to usize + let limit: usize = limit + .unwrap_or(DEFAULT_LIMIT) + .try_into() + .map_err(|_| StdError::generic_err("Limit too large"))?; + + // Early return if the limit is 0 + if limit == 0 { + return to_binary(&ProposalListResponse { proposals: vec![] }); + } let mut proposals_res: Vec = Vec::new(); + let start_after_id = start_after; // Keep track of the start_after value - let mut start = start_after; // Clone start_after to mutate it if necessary + let binding = PROPOSALS; - let binding = &PROPOSALS; + // Get an iterator over the proposals stored let iter = binding.iter(deps.storage)?; + for item in iter { let (id, proposal) = item?; - if let Some(start_after) = &start { - if &id == start_after { - // If we found the start point, reset it to start iterating - start = None; + + // If `start_after` is specified, skip proposals until ID is greater than `start_after` + if let Some(start_after_value) = start_after_id { + if id <= start_after_value { + continue; // Skip this proposal if its ID is less than or equal to start_after } } - if start.is_none() { - proposals_res.push(ProposalResponse { id, proposal }); - if proposals_res.len() >= limit.try_into().unwrap() { - break; // Break out of loop if limit reached - } + + // Now that we're beyond the start_after point, collect the proposals + proposals_res.push(ProposalResponse { id, proposal }); + + // Stop if we reach the requested limit + if proposals_res.len() >= limit { + break; } } + // Return the list of proposals to_binary(&ProposalListResponse { proposals: proposals_res, }) @@ -1008,43 +1014,42 @@ pub fn query_reverse_proposals( start_before: Option, limit: Option, ) -> StdResult { - // let limit = limit.unwrap_or(DEFAULT_LIMIT); - // let max = start_before.map(Bound::exclusive); - // let props: Vec = PROPOSALS - // .range(deps.storage, None, max, cosmwasm_std::Order::Descending) - // .take(limit as usize) - // .collect::, _>>()? - // .into_iter() - // .map(|(id, proposal)| proposal.into_response(&env.block, id)) - // .collect::>>()?; - - // let max = start_before.map(Bound::exclusive); - // to_binary(&ProposalListResponse { proposals: props }) - - let limit = limit.unwrap_or(DEFAULT_LIMIT); - // let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + // Use the provided limit or fall back to DEFAULT_LIMIT + let limit: usize = limit + .unwrap_or(DEFAULT_LIMIT) + .try_into() + .map_err(|_| StdError::generic_err("Limit too large"))?; + + // Early return if limit is 0 + if limit == 0 { + return to_binary(&ProposalListResponse { proposals: vec![] }); + } let mut proposals_res: Vec = Vec::new(); - let binding = &PROPOSALS; let iter = binding.iter(deps.storage)?; + + // Iterate in reverse over proposals for item in iter.rev() { let (id, proposal) = item?; - if let Some(start_before) = start_before { - if id < start_before { - proposals_res.push(ProposalResponse { id, proposal }); - if proposals_res.len() >= limit as usize { - break; // Break out of loop if limit reached - } - } - } else { - proposals_res.push(ProposalResponse { id, proposal }); - if proposals_res.len() >= limit as usize { - break; // Break out of loop if limit reached + + // Skip proposals greater than or equal to start_before + if let Some(start_before_value) = start_before { + if id >= start_before_value { + continue; // Skip this proposal } } + + // Collect the proposal into the result set + proposals_res.push(ProposalResponse { id, proposal }); + + // Stop collecting proposals when the limit is reached + if proposals_res.len() >= limit { + break; + } } + // Return the list of proposals as a binary response to_binary(&ProposalListResponse { proposals: proposals_res, }) @@ -1078,111 +1083,23 @@ pub fn query_info(deps: Deps) -> StdResult { } #[cfg_attr(not(feature = "library"), entry_point)] -pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result { - let ContractVersion { version, .. } = get_contract_version(deps.storage)?; - set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - - match msg { - MigrateMsg::FromV1 { - close_proposal_on_execution_failure, - pre_propose_info, - veto, - } => { - // `CONTRACT_VERSION` here is from the data section of the - // blob we are migrating to. `version` is from storage. If - // the version in storage matches the version in the blob - // we are not upgrading. - if version == CONTRACT_VERSION { - return Err(ContractError::AlreadyMigrated {}); - } - - let current_config = crate::state::CONFIG.load(deps.storage)?; - let max_voting_period = v1_duration_to_v2(current_config.max_voting_period); - let dao = DAO.load(deps.storage)?.addr; +pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + let storage_version: ContractVersion = get_contract_version(deps.storage)?; - // if veto is configured, validate its fields - if let Some(veto_config) = &veto { - veto_config.validate(&deps.as_ref(), &max_voting_period)?; - }; - - // Update the stored config to have the new - // `close_proposal_on_execution_failure` field. - CONFIG.save( - deps.storage, - &Config { - threshold: v1_threshold_to_v2(current_config.threshold), - max_voting_period, - min_voting_period: current_config.min_voting_period.map(v1_duration_to_v2), - only_members_execute: current_config.only_members_execute, - allow_revoting: current_config.allow_revoting, - close_proposal_on_execution_failure, - veto, - query_auth: current_config.query_auth, - }, - )?; - - let (initial_policy, pre_propose_messages) = - pre_propose_info.into_initial_policy_and_messages(deps.storage, dao, &REPLY_IDS)?; - CREATION_POLICY.save(deps.storage, &initial_policy)?; - - // Update the module's proposals to v2. - - let current_proposals = crate::state::PROPOSALS - .iter(deps.storage)? - .collect::>>()?; - - // Based on gas usage testing, we estimate that we will be - // able to migrate ~4200 proposals at a time before - // reaching the block max_gas limit. - current_proposals - .into_iter() - .try_for_each::<_, Result<_, ContractError>>(|(id, prop)| { - if prop.status != dao_voting::status::Status::Closed - && prop.status != dao_voting::status::Status::Executed - { - // No migration path for outstanding - // deposits. - return Err(ContractError::PendingProposals {}); - } - - let migrated_proposal = SingleChoiceProposal { - title: prop.title, - description: prop.description, - proposer: prop.proposer, - start_height: prop.start_height, - min_voting_period: prop.min_voting_period.map(v1_expiration_to_v2), - expiration: v1_expiration_to_v2(prop.expiration), - threshold: v1_threshold_to_v2(prop.threshold), - total_power: prop.total_power, - msgs: prop.msgs, - status: v1_status_to_v2(prop.status), - votes: v1_votes_to_v2(prop.votes), - allow_revoting: prop.allow_revoting, - veto: None, - }; - - PROPOSALS - .insert(deps.storage, &id, &migrated_proposal) - .map_err(|e| e.into()) - })?; - - Ok(Response::default() - .add_attribute("action", "migrate") - .add_attribute("from", "v1") - .add_submessages(pre_propose_messages)) - } - MigrateMsg::FromCompatible {} => Ok(Response::default() - .add_attribute("action", "migrate") - .add_attribute("from", "compatible")), + // Only migrate if newer + if storage_version.version.as_str() < CONTRACT_VERSION { + // Set contract to version to latest + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; } + + Ok(Response::new().add_attribute("action", "migrate")) } #[cfg_attr(not(feature = "library"), entry_point)] pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result { - let repl = REPLY_IDS.get_event(deps.storage, msg.id)?; + let repl = TaggedReplyId::new(msg.id)?; match repl { - ReplyEvent::FailedProposalExecution { proposal_id } => match msg.clone().result { - SubMsgResult::Err(err) => Err(ContractError::Std(StdError::GenericErr { msg: err })), + TaggedReplyId::FailedProposalExecution(proposal_id) => match msg.result { SubMsgResult::Ok(_) => { let proposals = PROPOSALS.get(deps.storage, &proposal_id); if proposals.clone().is_some() { @@ -1196,9 +1113,10 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result match msg.result { SubMsgResult::Err(err) => Err(ContractError::Std(StdError::GenericErr { msg: err })), + }, + + TaggedReplyId::FailedProposalHook(idx) => match msg.result { SubMsgResult::Ok(_) => { let hook_item = PROPOSAL_HOOKS.remove_hook_by_index(deps.storage, idx)?; Ok(Response::new().add_attribute( @@ -1206,20 +1124,22 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result match msg.result { SubMsgResult::Err(err) => Err(ContractError::Std(StdError::GenericErr { msg: err })), + }, + + TaggedReplyId::FailedVoteHook(idx) => match msg.result { SubMsgResult::Ok(_) => { let hook_item = VOTE_HOOKS.remove_hook_by_index(deps.storage, idx)?; Ok(Response::new() .add_attribute("removed_vote_hook", format!("{0}:{idx}", hook_item.addr))) } - }, - ReplyEvent::PreProposalModuleInstantiate { code_hash } => match msg.result { SubMsgResult::Err(err) => Err(ContractError::Std(StdError::GenericErr { msg: err })), - SubMsgResult::Ok(res) => { - let address = parse_reply_address_from_event(res.clone()); + }, + TaggedReplyId::PreProposeModuleInstantiation => match msg.result { + SubMsgResult::Ok(sub_msg_response) => { + let address = parse_reply_address_from_event(sub_msg_response.clone()); + let code_hash = PRE_PROPOSE_CODE_HASH.load(deps.storage)?; CREATION_POLICY.save( deps.storage, &ProposalCreationPolicy::Module { @@ -1233,34 +1153,41 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result - match res.data { - Some(data) => Ok(Response::new() - .add_attribute("update_pre_propose_module", address.clone().to_string()) - .set_data(data)), - None => Ok(Response::new() - .add_attribute("update_pre_propose_module", address.to_string())), - } + // match sub_msg_response.data { + // Some(data) => Ok(Response::new() + // .add_attribute("update_pre_propose_module", address.clone().to_string()) + // .set_data(data)), + // None => Ok(Response::new() + // .add_attribute("update_pre_propose_module", address.to_string())), + // } + Ok(Response::new().add_attribute("update_pre_propose_module", address.to_string())) } + SubMsgResult::Err(_) => todo!(), + }, + + TaggedReplyId::FailedPreProposeModuleHook => match msg.result { + SubMsgResult::Ok(_) => { + let addr = match CREATION_POLICY.load(deps.storage)? { + ProposalCreationPolicy::Anyone {} => { + // Something is off if we're getting this + // reply and we don't have a pre-propose + // module installed. This should be + // unreachable. + return Err(ContractError::InvalidReplyID { + id: failed_pre_propose_module_hook_id(), + }); + } + ProposalCreationPolicy::Module { addr, .. } => { + // If we are here, our pre-propose module has + // errored while receiving a proposal + // hook. Rest in peace pre-propose module. + CREATION_POLICY.save(deps.storage, &ProposalCreationPolicy::Anyone {})?; + addr + } + }; + Ok(Response::new().add_attribute("failed_prepropose_hook", format!("{addr}"))) + } + SubMsgResult::Err(err) => Err(ContractError::Std(StdError::GenericErr { msg: err })), }, - ReplyEvent::FailedPreProposeModuleHook {} => { - let addr = match CREATION_POLICY.load(deps.storage)? { - ProposalCreationPolicy::Anyone {} => { - // Something is off if we're getting this - // reply and we don't have a pre-propose - // module installed. This should be - // unreachable. - return Err(ContractError::InvalidReplyID { id: msg.id }); - } - ProposalCreationPolicy::Module { addr, code_hash: _ } => { - // If we are here, our pre-propose module has - // errored while receiving a proposal - // hook. Rest in peace pre-propose module. - CREATION_POLICY.save(deps.storage, &ProposalCreationPolicy::Anyone {})?; - addr - } - }; - Ok(Response::new().add_attribute("failed_prepropose_hook", format!("{addr}"))) - } - _ => Err(ContractError::UnknownReplyID {}), } } diff --git a/contracts/proposal/dao-proposal-single/src/msg.rs b/contracts/proposal/dao-proposal-single/src/msg.rs index 770d989..9c6db11 100644 --- a/contracts/proposal/dao-proposal-single/src/msg.rs +++ b/contracts/proposal/dao-proposal-single/src/msg.rs @@ -213,32 +213,4 @@ pub enum QueryMsg { #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] #[serde(rename_all = "snake_case")] -pub enum MigrateMsg { - FromV1 { - /// This field was not present in DAO DAO v1. To migrate, a - /// value must be specified. - /// - /// If set to true proposals will be closed if their execution - /// fails. Otherwise, proposals will remain open after execution - /// failure. For example, with this enabled a proposal to send 5 - /// tokens out of a DAO's treasury with 4 tokens would be closed when - /// it is executed. With this disabled, that same proposal would - /// remain open until the DAO's treasury was large enough for it to be - /// executed. - close_proposal_on_execution_failure: bool, - /// This field was not present in DAO DAO v1. To migrate, a - /// value must be specified. - /// - /// This contains information about how a pre-propose module may be configured. - /// If set to "AnyoneMayPropose", there will be no pre-propose module and consequently, - /// no deposit or membership checks when submitting a proposal. The "ModuleMayPropose" - /// option allows for instantiating a prepropose module which will handle deposit verification and return logic. - pre_propose_info: PreProposeInfo, - /// This field was not present in DAO DAO v1. To migrate, a - /// value must be specified. - /// - /// optional configuration for veto feature - veto: Option, - }, - FromCompatible {}, -} +pub struct MigrateMsg {} diff --git a/contracts/proposal/dao-proposal-single/src/state.rs b/contracts/proposal/dao-proposal-single/src/state.rs index 3e41623..c1fa406 100644 --- a/contracts/proposal/dao-proposal-single/src/state.rs +++ b/contracts/proposal/dao-proposal-single/src/state.rs @@ -1,7 +1,6 @@ use cosmwasm_std::{Addr, Uint128}; use cw_hooks::Hooks; use dao_interface::state::AnyContractInfo; -use dao_interface::ReplyIds; use dao_voting::{ pre_propose::ProposalCreationPolicy, threshold::Threshold, veto::VetoConfig, voting::Vote, }; @@ -74,8 +73,8 @@ pub struct Config { pub const CONFIG: Item = Item::new("config_v2"); /// The number of proposals that have been created. pub const PROPOSAL_COUNT: Item = Item::new("proposal_count"); -pub static PROPOSALS: Keymap = Keymap::new(b"proposals_v2"); -pub static BALLOTS: Keymap<(u64, Addr), Ballot, Json> = Keymap::new(b"ballots"); +pub const PROPOSALS: Keymap = Keymap::new(b"proposals_v2"); +pub const BALLOTS: Keymap<(u64, Addr), Ballot, Json> = Keymap::new(b"ballots"); /// Consumers of proposal state change hooks. pub const PROPOSAL_HOOKS: Hooks = Hooks::new("proposal_hooks"); /// Consumers of vote hooks. @@ -84,4 +83,4 @@ pub const VOTE_HOOKS: Hooks = Hooks::new("vote_hooks"); /// proposal module (if any). pub const CREATION_POLICY: Item = Item::new("creation_policy"); pub const DAO: Item = Item::new("dao"); -pub static REPLY_IDS: ReplyIds = ReplyIds::new(b"reply_ids", b"reply_ids_count"); +pub const PRE_PROPOSE_CODE_HASH: Item = Item::new("ppch"); diff --git a/contracts/proposal/dao-proposal-single/src/testing/adversarial_tests.rs b/contracts/proposal/dao-proposal-single/src/testing/adversarial_tests.rs new file mode 100644 index 0000000..cc083ca --- /dev/null +++ b/contracts/proposal/dao-proposal-single/src/testing/adversarial_tests.rs @@ -0,0 +1,785 @@ +use crate::msg::InstantiateMsg; +use crate::testing::execute::create_snip20_viewing_key; +use crate::testing::instantiate::get_pre_propose_info; +use crate::testing::{ + execute::{ + close_proposal, execute_proposal, execute_proposal_should_fail, make_proposal, + mint_snip20s, vote_on_proposal, + }, + instantiate::{ + get_default_token_dao_proposal_module_instantiate, + instantiate_with_staked_balances_governance, + }, + queries::{query_balance_cw20, query_dao_token, query_proposal, query_single_proposal_module}, +}; +use cosmwasm_std::testing::mock_info; +use cosmwasm_std::{to_binary, ContractInfo, CosmosMsg, Decimal, Uint128, WasmMsg}; +use dao_interface::msg::InitialBalance; +use dao_interface::state::AnyContractInfo; +use dao_voting::{ + deposit::{DepositRefundPolicy, UncheckedDepositInfo, VotingModuleTokenType}, + status::Status, + threshold::{PercentageThreshold, Threshold::AbsolutePercentage}, + voting::Vote, +}; +use secret_multi_test::{next_block, App}; +use secret_utils::Duration; + +use super::execute::create_viewing_key; +use super::CREATOR_ADDR; +use crate::{query::ProposalResponse, ContractError}; + +struct CommonTest { + app: App, + proposal_module: AnyContractInfo, + proposal_id: u64, + query_auth_info: AnyContractInfo, +} +fn setup_test(messages: Vec) -> CommonTest { + let mut app = App::default(); + let instantiate = get_default_token_dao_proposal_module_instantiate(&mut app); + let core_contract_info = + instantiate_with_staked_balances_governance(&mut app, instantiate, None); + let proposal_module = query_single_proposal_module( + &app, + &core_contract_info.address.clone(), + core_contract_info.code_hash.clone(), + ); + let gov_token_info = query_dao_token( + &app, + &core_contract_info.address.clone(), + core_contract_info.code_hash.clone(), + ); + + let query_auth_info: AnyContractInfo = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::QueryAuthInfo {}, + ) + .unwrap(); + + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + // Mint some tokens to pay the proposal deposit. + mint_snip20s( + &mut app, + &gov_token_info.addr.clone(), + gov_token_info.code_hash.clone(), + &core_contract_info.address.clone(), + CREATOR_ADDR, + 10_000_000, + ); + let proposal_id = make_proposal( + &mut app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.to_owned(), + }, + messages, + ); + + CommonTest { + app, + proposal_module, + proposal_id, + query_auth_info, + } +} + +// A proposal that is still accepting votes (is open) cannot +// be executed. Any attempts to do so should fail and return +// an error. +#[test] +fn test_execute_proposal_open() { + let CommonTest { + mut app, + proposal_module, + proposal_id, + query_auth_info, + } = setup_test(vec![]); + + app.update_block(next_block); + + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr, + code_hash: query_auth_info.code_hash, + }, + mock_info(CREATOR_ADDR, &[]), + ); + + // assert proposal is open + let proposal = query_proposal( + &app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + proposal_id, + ); + assert_eq!(proposal.proposal.status, Status::Open); + + // attempt to execute and assert that it fails + let err = execute_proposal_should_fail( + &mut app, + &proposal_module.addr, + proposal_module.code_hash, + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.to_owned(), + }, + proposal_id, + ); + assert!(matches!(err, ContractError::NotPassed {})) +} + +// A proposal can be executed if and only if it passed. +// Any attempts to execute a proposal that has been rejected +// or closed (after rejection) should fail and return an error. +#[test] +fn test_execute_proposal_rejected_closed() { + let CommonTest { + mut app, + proposal_module, + proposal_id, + query_auth_info, + } = setup_test(vec![]); + + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr, + code_hash: query_auth_info.code_hash, + }, + mock_info(CREATOR_ADDR, &[]), + ); + + // Assert proposal is open and vote enough to reject it + let proposal: ProposalResponse = query_proposal( + &app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + 1, + ); + assert_eq!(proposal.proposal.status, Status::Open); + vote_on_proposal( + &mut app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + CREATOR_ADDR, + proposal_id, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.to_owned(), + }, + Vote::No, + ); + + app.update_block(next_block); + + // Assert proposal is rejected + let proposal: ProposalResponse = query_proposal( + &app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + proposal_id, + ); + assert_eq!(proposal.proposal.status, Status::Rejected); + + // Attempt to execute + let err = execute_proposal_should_fail( + &mut app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.to_owned(), + }, + proposal_id, + ); + assert!(matches!(err, ContractError::NotPassed {})); + + app.update_block(next_block); + + // close the proposal + close_proposal( + &mut app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + CREATOR_ADDR, + proposal_id, + ); + let proposal = query_proposal( + &app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + proposal_id, + ); + assert_eq!(proposal.proposal.status, Status::Closed); + + // Attempt to execute + let err = execute_proposal_should_fail( + &mut app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.to_owned(), + }, + proposal_id, + ); + assert!(matches!(err, ContractError::NotPassed {})) +} + +// A proposal can only be executed once. Any subsequent +// attempts to execute it should fail and return an error. +#[test] +fn test_execute_proposal_more_than_once() { + let CommonTest { + mut app, + proposal_module, + proposal_id, + query_auth_info, + } = setup_test(vec![]); + + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr, + code_hash: query_auth_info.code_hash, + }, + mock_info(CREATOR_ADDR, &[]), + ); + + // Assert proposal is open and vote enough to reject it + let proposal: ProposalResponse = query_proposal( + &app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + proposal_id, + ); + assert_eq!(proposal.proposal.status, Status::Open); + vote_on_proposal( + &mut app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + CREATOR_ADDR, + proposal_id, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.to_owned(), + }, + Vote::Yes, + ); + + app.update_block(next_block); + + // assert proposal is passed, execute it + let proposal = query_proposal( + &app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + proposal_id, + ); + assert_eq!(proposal.proposal.status, Status::Passed); + execute_proposal( + &mut app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.to_owned(), + }, + proposal_id, + ); + + app.update_block(next_block); + + // assert proposal executed and attempt to execute it again + let proposal = query_proposal( + &app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + proposal_id, + ); + assert_eq!(proposal.proposal.status, Status::Executed); + let err: ContractError = execute_proposal_should_fail( + &mut app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.to_owned(), + }, + proposal_id, + ); + assert!(matches!(err, ContractError::NotPassed {})); +} + +// After proposal is executed, no subsequent votes +// should change the status of the proposal, even if +// the votes should shift to the opposing direction. +#[test] +pub fn test_executed_prop_state_remains_after_vote_swing() { + let mut app = App::default(); + + let instantiate = InstantiateMsg { + veto: None, + threshold: AbsolutePercentage { + percentage: PercentageThreshold::Percent(Decimal::percent(15)), + }, + max_voting_period: Duration::Time(604800), // One week. + min_voting_period: None, + only_members_execute: true, + allow_revoting: false, + pre_propose_info: get_pre_propose_info( + &mut app, + Some(UncheckedDepositInfo { + denom: dao_voting::deposit::DepositToken::VotingModuleToken { + token_type: VotingModuleTokenType::Cw20, + }, + amount: Uint128::new(10_000_000), + refund_policy: DepositRefundPolicy::OnlyPassed, + }), + false, + ), + close_proposal_on_execution_failure: true, + dao_code_hash: "".to_string(), + query_auth: None, + }; + + let core_contract_info = instantiate_with_staked_balances_governance( + &mut app, + instantiate, + Some(vec![ + InitialBalance { + address: "threshold".to_string(), + amount: Uint128::new(20), + }, + InitialBalance { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(50), + }, + InitialBalance { + address: "overslept_vote".to_string(), + amount: Uint128::new(30), + }, + ]), + ); + let proposal_module = query_single_proposal_module( + &app, + &core_contract_info.address.clone(), + core_contract_info.code_hash.clone(), + ); + let gov_token_info = query_dao_token( + &app, + &core_contract_info.address.clone(), + core_contract_info.code_hash.clone(), + ); + + let query_auth_info: AnyContractInfo = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::QueryAuthInfo {}, + ) + .unwrap(); + + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + + let viewing_key_threshold = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info("threshold", &[]), + ); + + let viewing_key_overslept_vote = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info("overslept_vote", &[]), + ); + + mint_snip20s( + &mut app, + &gov_token_info.addr.clone(), + gov_token_info.code_hash.clone(), + &core_contract_info.address.clone(), + CREATOR_ADDR, + 10_000_000, + ); + let proposal_id = make_proposal( + &mut app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + vec![], + ); + + // someone quickly votes, proposal gets executed + vote_on_proposal( + &mut app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + "threshold", + proposal_id, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key_threshold.clone(), + address: "threshold".into(), + }, + Vote::Yes, + ); + execute_proposal( + &mut app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + proposal_id, + ); + + app.update_block(next_block); + + // assert prop is executed prior to its expiry + let proposal = query_proposal( + &app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + proposal_id, + ); + assert_eq!(proposal.proposal.status, Status::Executed); + assert_eq!(proposal.proposal.votes.yes, Uint128::new(20)); + assert!(!proposal.proposal.expiration.is_expired(&app.block_info())); + + // someone wakes up and casts their vote to express their + // opinion (not affecting the result of proposal) + vote_on_proposal( + &mut app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + CREATOR_ADDR, + proposal_id, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + Vote::No, + ); + vote_on_proposal( + &mut app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + "overslept_vote", + proposal_id, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key_overslept_vote.clone(), + address: "overslept_vote".into(), + }, + Vote::No, + ); + + app.update_block(next_block); + + // assert that everyone's votes are reflected in the proposal + // and proposal remains in executed state + let proposal = query_proposal( + &app, + &proposal_module.addr, + proposal_module.code_hash, + proposal_id, + ); + assert_eq!(proposal.proposal.status, Status::Executed); + assert_eq!(proposal.proposal.votes.yes, Uint128::new(20)); + assert_eq!(proposal.proposal.votes.no, Uint128::new(80)); +} + +// After reaching a passing state, no subsequent votes +// should change the status of the proposal, even if +// the votes should shift to the opposing direction. +#[test] +pub fn test_passed_prop_state_remains_after_vote_swing() { + let mut app = App::default(); + + let instantiate = InstantiateMsg { + veto: None, + threshold: AbsolutePercentage { + percentage: PercentageThreshold::Percent(Decimal::percent(15)), + }, + max_voting_period: Duration::Time(604800), // One week. + min_voting_period: None, + only_members_execute: true, + allow_revoting: false, + pre_propose_info: get_pre_propose_info( + &mut app, + Some(UncheckedDepositInfo { + denom: dao_voting::deposit::DepositToken::VotingModuleToken { + token_type: VotingModuleTokenType::Cw20, + }, + amount: Uint128::new(10_000_000), + refund_policy: DepositRefundPolicy::OnlyPassed, + }), + false, + ), + close_proposal_on_execution_failure: true, + dao_code_hash: "".into(), + query_auth: None, + }; + + let core_contract_info = instantiate_with_staked_balances_governance( + &mut app, + instantiate, + Some(vec![ + InitialBalance { + address: "threshold".to_string(), + amount: Uint128::new(20), + }, + InitialBalance { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(50), + }, + InitialBalance { + address: "overslept_vote".to_string(), + amount: Uint128::new(30), + }, + ]), + ); + let proposal_module = query_single_proposal_module( + &app, + &core_contract_info.address.clone(), + core_contract_info.code_hash.clone(), + ); + let gov_token_info = query_dao_token( + &app, + &core_contract_info.address.clone(), + core_contract_info.code_hash.clone(), + ); + + let query_auth_info: AnyContractInfo = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::QueryAuthInfo {}, + ) + .unwrap(); + + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + + let viewing_key_threshold = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info("threshold", &[]), + ); + + let viewing_key_overslept_vote = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info("overslept_vote", &[]), + ); + + let viewing_key_token = create_snip20_viewing_key( + &mut app, + ContractInfo { + address: gov_token_info.addr.clone(), + code_hash: gov_token_info.code_hash.clone(), + }, + mock_info("threshold", &[]), + ); + + // if the proposal passes, it should mint 100_000_000 tokens to "threshold" + let msg = snip20_reference_impl::msg::ExecuteMsg::Mint { + recipient: "threshold".to_string(), + amount: Uint128::new(100_000_000), + memo: None, + decoys: None, + entropy: None, + padding: None, + }; + + let binary_msg = to_binary(&msg).unwrap(); + + mint_snip20s( + &mut app, + &gov_token_info.addr.clone(), + gov_token_info.code_hash.clone(), + &core_contract_info.address.clone(), + CREATOR_ADDR, + 10_000_000, + ); + let proposal_id = make_proposal( + &mut app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + vec![WasmMsg::Execute { + contract_addr: gov_token_info.addr.clone().to_string(), + msg: binary_msg, + funds: vec![], + code_hash: gov_token_info.code_hash.clone(), + } + .into()], + ); + + // assert that the initial "threshold" address balance is 0 + let balance = query_balance_cw20( + &app, + gov_token_info.addr.clone().to_string(), + gov_token_info.code_hash.clone(), + "threshold", + viewing_key_token.clone(), + ); + assert_eq!(balance, Uint128::zero()); + + // vote enough to pass the proposal + vote_on_proposal( + &mut app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + "threshold", + proposal_id, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key_threshold.clone(), + address: "threshold".into(), + }, + Vote::Yes, + ); + + // assert proposal is passed with 20 votes in favor and none opposed + let proposal = query_proposal( + &app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + proposal_id, + ); + assert_eq!(proposal.proposal.status, Status::Passed); + assert_eq!(proposal.proposal.votes.yes, Uint128::new(20)); + assert_eq!(proposal.proposal.votes.no, Uint128::zero()); + + app.update_block(next_block); + + // the other voters wake up, vote against the proposal + vote_on_proposal( + &mut app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + CREATOR_ADDR, + proposal_id, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + Vote::No, + ); + vote_on_proposal( + &mut app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + "overslept_vote", + proposal_id, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key_overslept_vote.clone(), + address: "overslept_vote".into(), + }, + Vote::No, + ); + + app.update_block(next_block); + + // assert that the late votes have been counted and proposal + // is still in passed state before executing it + let proposal = query_proposal( + &app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + proposal_id, + ); + assert_eq!(proposal.proposal.status, Status::Passed); + assert_eq!(proposal.proposal.votes.yes, Uint128::new(20)); + assert_eq!(proposal.proposal.votes.no, Uint128::new(80)); + + execute_proposal( + &mut app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + proposal_id, + ); + + app.update_block(next_block); + + // make sure that the initial "threshold" address balance is + // 100_000_000 and late votes did not make a difference + let proposal = query_proposal( + &app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + proposal_id, + ); + assert_eq!(proposal.proposal.status, Status::Executed); + assert_eq!(proposal.proposal.votes.yes, Uint128::new(20)); + assert_eq!(proposal.proposal.votes.no, Uint128::new(80)); + let balance = query_balance_cw20( + &app, + gov_token_info.addr.to_string(), + gov_token_info.code_hash, + "threshold", + viewing_key_token, + ); + assert_eq!(balance, Uint128::new(100_000_000)); +} diff --git a/contracts/proposal/dao-proposal-single/src/testing/contracts.rs b/contracts/proposal/dao-proposal-single/src/testing/contracts.rs index 0b6ccdb..668fdd2 100644 --- a/contracts/proposal/dao-proposal-single/src/testing/contracts.rs +++ b/contracts/proposal/dao-proposal-single/src/testing/contracts.rs @@ -1,6 +1,44 @@ use cosmwasm_std::Empty; + +use dao_pre_propose_single as cppbps; use secret_multi_test::{Contract, ContractWrapper}; +pub(crate) fn snip20_base_contract() -> Box> { + let contract = ContractWrapper::new( + snip20_reference_impl::contract::execute, + snip20_reference_impl::contract::instantiate, + snip20_reference_impl::contract::query, + ); + Box::new(contract) +} + +pub(crate) fn cw4_group_contract() -> Box> { + let contract = ContractWrapper::new( + cw4_group::contract::execute, + cw4_group::contract::instantiate, + cw4_group::contract::query, + ); + Box::new(contract) +} + +pub(crate) fn snip721_base_contract() -> Box> { + let contract = ContractWrapper::new( + snip721_reference_impl::contract::execute, + snip721_reference_impl::contract::instantiate, + snip721_reference_impl::contract::query, + ); + Box::new(contract) +} + +pub(crate) fn snip20_stake_contract() -> Box> { + let contract = ContractWrapper::new( + snip20_stake::contract::execute, + snip20_stake::contract::instantiate, + snip20_stake::contract::query, + ); + Box::new(contract) +} + pub(crate) fn proposal_single_contract() -> Box> { let contract = ContractWrapper::new( crate::contract::execute, @@ -12,6 +50,63 @@ pub(crate) fn proposal_single_contract() -> Box> { Box::new(contract) } +pub(crate) fn pre_propose_single_contract() -> Box> { + let contract = ContractWrapper::new( + cppbps::contract::execute, + cppbps::contract::instantiate, + cppbps::contract::query, + ); + Box::new(contract) +} + +pub(crate) fn snip20_staked_balances_voting_contract() -> Box> { + let contract = ContractWrapper::new( + dao_voting_snip20_staked::contract::execute, + dao_voting_snip20_staked::contract::instantiate, + dao_voting_snip20_staked::contract::query, + ) + .with_reply(dao_voting_snip20_staked::contract::reply); + Box::new(contract) +} + +pub(crate) fn native_staked_balances_voting_contract() -> Box> { + let contract = ContractWrapper::new( + dao_voting_token_staked::contract::execute, + dao_voting_token_staked::contract::instantiate, + dao_voting_token_staked::contract::query, + ); + Box::new(contract) +} + +pub(crate) fn snip721_stake_contract() -> Box> { + let contract = ContractWrapper::new( + dao_voting_snip721_staked::contract::execute, + dao_voting_snip721_staked::contract::instantiate, + dao_voting_snip721_staked::contract::query, + ); + Box::new(contract) +} + +pub(crate) fn cw_core_contract() -> Box> { + let contract = ContractWrapper::new( + dao_dao_core::contract::execute, + dao_dao_core::contract::instantiate, + dao_dao_core::contract::query, + ) + .with_reply(dao_dao_core::contract::reply); + Box::new(contract) +} + +pub(crate) fn cw4_voting_contract() -> Box> { + let contract = ContractWrapper::new( + dao_voting_cw4::contract::execute, + dao_voting_cw4::contract::instantiate, + dao_voting_cw4::contract::query, + ) + .with_reply(dao_voting_cw4::contract::reply); + Box::new(contract) +} + pub(crate) fn query_auth_contract() -> Box> { let contract = ContractWrapper::new( query_auth::contract::execute, diff --git a/contracts/proposal/dao-proposal-single/src/testing/do_votes.rs b/contracts/proposal/dao-proposal-single/src/testing/do_votes.rs new file mode 100644 index 0000000..f736c13 --- /dev/null +++ b/contracts/proposal/dao-proposal-single/src/testing/do_votes.rs @@ -0,0 +1,449 @@ +use std::mem::discriminant; + +use cosmwasm_std::{coins, testing::mock_info, Addr, Coin, ContractInfo, Uint128}; + +use dao_interface::{ + msg::InitialBalance, + state::{AnyContractInfo, ProposalModule}, +}; +use dao_pre_propose_single as cppbps; +use secret_multi_test::{App, BankSudo, Executor, SudoMsg}; + +use cw_denom::CheckedDenom; +use dao_testing::{ShouldExecute, TestSingleChoiceVote}; +use dao_voting::{ + deposit::{CheckedDepositInfo, UncheckedDepositInfo}, + status::Status, + threshold::Threshold, +}; + +use crate::{ + msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, + query::{ProposalResponse, VoteInfo, VoteResponse}, + testing::{ + execute::create_viewing_key, instantiate::*, + queries::query_deposit_config_and_pre_propose_module, + }, +}; + +pub(crate) fn do_votes_staked_balances( + votes: Vec, + threshold: Threshold, + expected_status: Status, + total_supply: Option, +) { + do_test_votes( + votes, + threshold, + expected_status, + total_supply, + None::, + instantiate_with_staked_balances_governance, + ); +} + +pub(crate) fn do_votes_native_staked_balances( + votes: Vec, + threshold: Threshold, + expected_status: Status, + total_supply: Option, +) { + do_test_votes( + votes, + threshold, + expected_status, + total_supply, + None, + instantiate_with_native_staked_balances_governance, + ); +} + +pub(crate) fn do_votes_cw4_weights( + votes: Vec, + threshold: Threshold, + expected_status: Status, + total_supply: Option, +) { + do_test_votes( + votes, + threshold, + expected_status, + total_supply, + None::, + instantiate_with_cw4_groups_governance, + ); +} + +fn do_test_votes( + votes: Vec, + threshold: Threshold, + expected_status: Status, + total_supply: Option, + deposit_info: Option, + setup_governance: F, +) -> (App, ContractInfo) +where + F: Fn(&mut App, InstantiateMsg, Option>) -> ContractInfo, +{ + let mut app = App::default(); + + // Mint some ujuno so that it exists for native staking tests + // Otherwise denom validation will fail + app.sudo(SudoMsg::Bank(BankSudo::Mint { + to_address: "sodenomexists".to_string(), + amount: vec![Coin { + amount: Uint128::new(10), + denom: "ujuno".to_string(), + }], + })) + .unwrap(); + + let mut initial_balances = votes + .iter() + .map( + |TestSingleChoiceVote { voter, weight, .. }| InitialBalance { + address: voter.to_string(), + amount: *weight, + }, + ) + .collect::>(); + let initial_balances_supply = votes.iter().fold(Uint128::zero(), |p, n| p + n.weight); + let to_fill = total_supply.map(|total_supply| total_supply - initial_balances_supply); + if let Some(fill) = to_fill { + initial_balances.push(InitialBalance { + address: "filler".to_string(), + amount: fill, + }) + } + + let pre_propose_info = get_pre_propose_info(&mut app, deposit_info, false); + + let proposer = match votes.first() { + Some(vote) => vote.voter.clone(), + None => panic!("do_test_votes must have at least one vote."), + }; + + let max_voting_period = secret_utils::Duration::Height(6); + let instantiate = InstantiateMsg { + veto: None, + threshold, + max_voting_period, + min_voting_period: None, + only_members_execute: false, + allow_revoting: false, + close_proposal_on_execution_failure: true, + pre_propose_info, + dao_code_hash: "".into(), + query_auth: None, + }; + + let core_contract_info = setup_governance(&mut app, instantiate, Some(initial_balances)); + + let governance_modules: Vec = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::ProposalModules { + start_after: None, + limit: None, + }, + ) + .unwrap(); + + assert_eq!(governance_modules.len(), 1); + let proposal_single = governance_modules + .clone() + .into_iter() + .next() + .unwrap() + .address; + let proposal_single_code_hash = governance_modules.into_iter().next().unwrap().code_hash; + + let (deposit_config, pre_propose_module) = query_deposit_config_and_pre_propose_module( + &app, + &proposal_single, + proposal_single_code_hash.clone(), + ); + // Pay the cw20 deposit if needed. + if let Some(CheckedDepositInfo { + denom: CheckedDenom::Snip20(ref token_addr, code_hash), + amount, + .. + }) = deposit_config.deposit_info.clone() + { + app.execute_contract( + Addr::unchecked(&proposer), + &ContractInfo { + address: token_addr.clone(), + code_hash: code_hash.clone(), + }, + &snip20_reference_impl::msg::ExecuteMsg::IncreaseAllowance { + spender: pre_propose_module.address.clone().to_string(), + amount, + expiration: None, + padding: None, + }, + &[], + ) + .unwrap(); + } + + let funds = if let Some(CheckedDepositInfo { + denom: CheckedDenom::Native(ref denom), + amount, + .. + }) = deposit_config.deposit_info.clone() + { + // Mint the needed tokens to create the deposit. + app.sudo(secret_multi_test::SudoMsg::Bank(BankSudo::Mint { + to_address: proposer.clone(), + amount: coins(amount.u128(), denom), + })) + .unwrap(); + coins(amount.u128(), denom) + } else { + vec![] + }; + + let query_auth_info: AnyContractInfo = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::QueryAuthInfo {}, + ) + .unwrap(); + + let viewing_key_proposer = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(&proposer, &[]), + ); + app.execute_contract( + Addr::unchecked(&proposer), + &pre_propose_module, + &cppbps::ExecuteMsg::Propose { + auth: shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key_proposer.clone(), + address: proposer.clone(), + }, + msg: cppbps::ProposeMessage::Propose { + title: "A simple text proposal".to_string(), + description: "This is a simple text proposal".to_string(), + msgs: vec![], + }, + }, + &funds, + ) + .unwrap(); + + // Cast votes. + for vote in votes { + let TestSingleChoiceVote { + voter, + position, + weight, + should_execute, + } = vote; + let viewing_key_voter = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(&voter, &[]), + ); + // Vote on the proposal. + let res = app.execute_contract( + Addr::unchecked(voter.clone()), + &ContractInfo { + address: proposal_single.clone(), + code_hash: proposal_single_code_hash.clone(), + }, + &ExecuteMsg::Vote { + auth: shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key_voter.clone(), + address: voter.clone(), + }, + proposal_id: 1, + vote: position, + rationale: None, + }, + &[], + ); + match should_execute { + ShouldExecute::Yes => { + assert!(res.is_ok()); + // Check that the vote was recorded correctly. + let vote: VoteResponse = app + .wrap() + .query_wasm_smart( + proposal_single_code_hash.clone(), + proposal_single.clone(), + &QueryMsg::GetVote { + proposal_id: 1, + auth: Box::new(shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key_voter.clone(), + address: voter.clone(), + }), + }, + ) + .unwrap(); + let expected = VoteResponse { + vote: Some(VoteInfo { + rationale: None, + voter: Addr::unchecked(&voter), + vote: position, + power: match deposit_config.deposit_info.clone() { + Some(CheckedDepositInfo { + amount, + denom: CheckedDenom::Snip20(_, _), + .. + }) => { + if proposer == voter { + weight - amount + } else { + weight + } + } + // Native token deposits shouldn't impact + // expected voting power. + _ => weight, + }, + }), + }; + assert_eq!(vote, expected) + } + ShouldExecute::No => { + res.unwrap_err(); + } + ShouldExecute::Meh => (), + } + } + + let proposal: ProposalResponse = app + .wrap() + .query_wasm_smart( + proposal_single_code_hash, + proposal_single, + &QueryMsg::Proposal { proposal_id: 1 }, + ) + .unwrap(); + + // We just care about getting the right variant + assert_eq!( + discriminant::(&proposal.proposal.status), + discriminant::(&expected_status) + ); + + (app, core_contract_info) +} + +#[test] +fn test_vote_simple() { + dao_testing::test_simple_votes(do_votes_cw4_weights); + dao_testing::test_simple_votes(do_votes_staked_balances); + dao_testing::test_simple_votes(do_votes_native_staked_balances) +} + +#[test] +fn test_simple_vote_no_overflow() { + dao_testing::test_simple_vote_no_overflow(do_votes_staked_balances); + dao_testing::test_simple_vote_no_overflow(do_votes_native_staked_balances); +} + +#[test] +fn test_vote_no_overflow() { + dao_testing::test_vote_no_overflow(do_votes_staked_balances); + dao_testing::test_vote_no_overflow(do_votes_native_staked_balances); +} + +#[test] +fn test_simple_early_rejection() { + dao_testing::test_simple_early_rejection(do_votes_cw4_weights); + dao_testing::test_simple_early_rejection(do_votes_staked_balances); + dao_testing::test_simple_early_rejection(do_votes_native_staked_balances); +} + +#[test] +fn test_vote_abstain_only() { + dao_testing::test_vote_abstain_only(do_votes_cw4_weights); + dao_testing::test_vote_abstain_only(do_votes_staked_balances); + dao_testing::test_vote_abstain_only(do_votes_native_staked_balances); +} + +#[test] +fn test_tricky_rounding() { + dao_testing::test_tricky_rounding(do_votes_cw4_weights); + dao_testing::test_tricky_rounding(do_votes_staked_balances); + dao_testing::test_tricky_rounding(do_votes_native_staked_balances); +} + +#[test] +fn test_no_double_votes() { + dao_testing::test_no_double_votes(do_votes_cw4_weights); + dao_testing::test_no_double_votes(do_votes_staked_balances); + // dao_testing::test_no_double_votes(do_votes_nft_balances); + dao_testing::test_no_double_votes(do_votes_native_staked_balances); +} + +#[test] +fn test_votes_favor_yes() { + dao_testing::test_votes_favor_yes(do_votes_staked_balances); + // dao_testing::test_votes_favor_yes(do_votes_nft_balances); + dao_testing::test_votes_favor_yes(do_votes_native_staked_balances); +} + +#[test] +fn test_votes_low_threshold() { + dao_testing::test_votes_low_threshold(do_votes_cw4_weights); + dao_testing::test_votes_low_threshold(do_votes_staked_balances); + // dao_testing::test_votes_low_threshold(do_votes_nft_balances); + dao_testing::test_votes_low_threshold(do_votes_native_staked_balances); +} + +#[test] +fn test_majority_vs_half() { + dao_testing::test_majority_vs_half(do_votes_cw4_weights); + dao_testing::test_majority_vs_half(do_votes_staked_balances); + // dao_testing::test_majority_vs_half(do_votes_nft_balances); + dao_testing::test_majority_vs_half(do_votes_native_staked_balances); +} + +#[test] +fn test_pass_threshold_not_quorum() { + dao_testing::test_pass_threshold_not_quorum(do_votes_cw4_weights); + dao_testing::test_pass_threshold_not_quorum(do_votes_staked_balances); + // dao_testing::test_pass_threshold_not_quorum(do_votes_nft_balances); + dao_testing::test_pass_threshold_not_quorum(do_votes_native_staked_balances); +} + +#[test] +fn test_pass_threshold_exactly_quorum() { + dao_testing::test_pass_exactly_quorum(do_votes_cw4_weights); + dao_testing::test_pass_exactly_quorum(do_votes_staked_balances); + // dao_testing::test_pass_exactly_quorum(do_votes_nft_balances); + dao_testing::test_pass_exactly_quorum(do_votes_native_staked_balances); +} + +/// Generate some random voting selections and make sure they behave +/// as expected. We split this test up as these take a while and cargo +/// can parallize tests. +#[test] +fn fuzz_voting_cw4_weights() { + dao_testing::fuzz_voting(do_votes_cw4_weights) +} + +#[test] +fn fuzz_voting_staked_balances() { + dao_testing::fuzz_voting(do_votes_staked_balances) +} + +#[test] +fn fuzz_voting_native_staked_balances() { + dao_testing::fuzz_voting(do_votes_native_staked_balances) +} diff --git a/contracts/proposal/dao-proposal-single/src/testing/execute.rs b/contracts/proposal/dao-proposal-single/src/testing/execute.rs index e0c9fbb..300a1d8 100644 --- a/contracts/proposal/dao-proposal-single/src/testing/execute.rs +++ b/contracts/proposal/dao-proposal-single/src/testing/execute.rs @@ -1,17 +1,28 @@ -use cosmwasm_std::{from_binary, Addr, ContractInfo, CosmosMsg, Decimal, MessageInfo}; -use secret_multi_test::{App, Executor}; +use cosmwasm_std::{ + coins, from_binary, to_binary, Addr, Coin, ContractInfo, CosmosMsg, MessageInfo, Uint128, +}; +use secret_multi_test::{App, BankSudo, Executor}; -use dao_voting::voting::Vote; -use secret_utils::Duration; +use cw_denom::CheckedDenom; +use dao_pre_propose_single as cppbps; +use dao_voting::{ + deposit::CheckedDepositInfo, pre_propose::ProposalCreationPolicy, + proposal::SingleChoiceProposeMsg as ProposeMsg, voting::Vote, +}; use shade_protocol::basic_staking::Auth; +use snip20_reference_impl::msg::InitialBalance; use crate::{ msg::{ExecuteMsg, QueryMsg}, query::ProposalResponse, - testing::queries::query_next_proposal_id, + testing::queries::{query_creation_policy, query_next_proposal_id}, ContractError, }; +use super::{ + contracts::snip20_base_contract, queries::query_pre_proposal_single_config, CREATOR_ADDR, +}; + // Creates a proposal then checks that the proposal was created with // the specified messages and returns the ID of the proposal. // @@ -21,32 +32,91 @@ pub(crate) fn make_proposal( app: &mut App, proposal_single: &Addr, proposal_single_code_hash: String, + proposer: &str, auth: Auth, msgs: Vec, -) { - // let proposal_creation_policy = - // query_creation_policy(app, proposal_single, proposal_single_code_hash.clone()); - let mut proposer = Addr::unchecked(""); - if let Auth::ViewingKey { address, .. } = auth.clone() { - proposer = Addr::unchecked(address); - } - - app.execute_contract( - Addr::unchecked(proposer.clone()), - &ContractInfo { - address: proposal_single.clone(), - code_hash: proposal_single_code_hash.clone(), - }, - &ExecuteMsg::Propose(dao_voting::proposal::SingleChoiceProposeMsg { - title: "title".to_string(), - description: "description".to_string(), - msgs: msgs.clone(), - proposer: None, - }), - &[], - ) - .unwrap(); +) -> u64 { + let proposal_creation_policy = + query_creation_policy(app, proposal_single, proposal_single_code_hash.clone()); + + // Collect the funding. + let funds = match proposal_creation_policy { + ProposalCreationPolicy::Anyone {} => vec![], + ProposalCreationPolicy::Module { + addr: ref pre_propose, + code_hash: ref pre_proposse_code_hash, + } => { + let deposit_config = + query_pre_proposal_single_config(app, pre_propose, pre_proposse_code_hash.clone()); + match deposit_config.deposit_info { + Some(CheckedDepositInfo { + denom, + amount, + refund_policy: _, + }) => match denom { + CheckedDenom::Native(denom) => coins(amount.u128(), denom), + CheckedDenom::Snip20(addr, code_hash) => { + // Give an allowance, no funds. + app.execute_contract( + Addr::unchecked(proposer), + &ContractInfo { + address: addr, + code_hash, + }, + &snip20_reference_impl::msg::ExecuteMsg::IncreaseAllowance { + spender: pre_propose.to_string(), + amount, + expiration: None, + padding: None, + }, + &[], + ) + .unwrap(); + vec![] + } + }, + None => vec![], + } + } + }; + // Make the proposal. + match proposal_creation_policy { + ProposalCreationPolicy::Anyone {} => app + .execute_contract( + Addr::unchecked(proposer), + &ContractInfo { + address: proposal_single.clone(), + code_hash: proposal_single_code_hash.clone(), + }, + &ExecuteMsg::Propose(ProposeMsg { + title: "title".to_string(), + description: "description".to_string(), + msgs: msgs.clone(), + proposer: None, + }), + &[], + ) + .unwrap(), + ProposalCreationPolicy::Module { addr, code_hash } => app + .execute_contract( + Addr::unchecked(proposer), + &ContractInfo { + address: addr, + code_hash, + }, + &cppbps::ExecuteMsg::Propose { + msg: cppbps::ProposeMessage::Propose { + title: "title".to_string(), + description: "description".to_string(), + msgs: msgs.clone(), + }, + auth, + }, + &funds, + ) + .unwrap(), + }; let id = query_next_proposal_id(app, proposal_single, proposal_single_code_hash.clone()); let id = id - 1; @@ -54,8 +124,8 @@ pub(crate) fn make_proposal( let proposal: ProposalResponse = app .wrap() .query_wasm_smart( - proposal_single_code_hash, - proposal_single, + proposal_single_code_hash.clone(), + proposal_single.clone(), &QueryMsg::Proposal { proposal_id: id }, ) .unwrap(); @@ -64,20 +134,19 @@ pub(crate) fn make_proposal( assert_eq!(proposal.proposal.title, "title".to_string()); assert_eq!(proposal.proposal.description, "description".to_string()); assert_eq!(proposal.proposal.msgs, msgs); + + id } -pub(crate) fn _vote_on_proposal( +pub(crate) fn vote_on_proposal( app: &mut App, proposal_single: &Addr, proposal_single_code_hash: String, - auth: Auth, + sender: &str, proposal_id: u64, + auth: Auth, vote: Vote, ) { - let mut sender = Addr::unchecked(""); - if let Auth::ViewingKey { address, .. } = auth.clone() { - sender = Addr::unchecked(address); - } app.execute_contract( Addr::unchecked(sender), &ContractInfo { @@ -99,19 +168,16 @@ pub(crate) fn vote_on_proposal_should_fail( app: &mut App, proposal_single: &Addr, proposal_single_code_hash: String, + sender: &str, auth: Auth, proposal_id: u64, vote: Vote, ) -> ContractError { - let mut sender = Addr::unchecked(""); - if let Auth::ViewingKey { address, .. } = auth.clone() { - sender = Addr::unchecked(address); - } app.execute_contract( Addr::unchecked(sender), &ContractInfo { address: proposal_single.clone(), - code_hash: proposal_single_code_hash, + code_hash: proposal_single_code_hash.clone(), }, &ExecuteMsg::Vote { auth, @@ -130,19 +196,15 @@ pub(crate) fn execute_proposal_should_fail( app: &mut App, proposal_single: &Addr, proposal_single_code_hash: String, + sender: &str, auth: Auth, proposal_id: u64, ) -> ContractError { - let mut sender = Addr::unchecked(""); - if let Auth::ViewingKey { address, .. } = auth.clone() { - sender = Addr::unchecked(address); - } - app.execute_contract( Addr::unchecked(sender), &ContractInfo { address: proposal_single.clone(), - code_hash: proposal_single_code_hash, + code_hash: proposal_single_code_hash.clone(), }, &ExecuteMsg::Execute { auth, proposal_id }, &[], @@ -152,25 +214,21 @@ pub(crate) fn execute_proposal_should_fail( .unwrap() } -pub(crate) fn _vote_on_proposal_with_rationale( +pub(crate) fn vote_on_proposal_with_rationale( app: &mut App, proposal_single: &Addr, proposal_single_code_hash: String, + sender: &str, auth: Auth, proposal_id: u64, vote: Vote, rationale: Option, ) { - let mut sender = Addr::unchecked(""); - if let Auth::ViewingKey { address, .. } = auth.clone() { - sender = Addr::unchecked(address); - } - app.execute_contract( Addr::unchecked(sender), &ContractInfo { address: proposal_single.clone(), - code_hash: proposal_single_code_hash, + code_hash: proposal_single_code_hash.clone(), }, &ExecuteMsg::Vote { auth, @@ -195,7 +253,7 @@ pub(crate) fn update_rationale( Addr::unchecked(sender), &ContractInfo { address: proposal_single.clone(), - code_hash: proposal_single_code_hash, + code_hash: proposal_single_code_hash.clone(), }, &ExecuteMsg::UpdateRationale { proposal_id, @@ -206,23 +264,19 @@ pub(crate) fn update_rationale( .unwrap(); } -pub(crate) fn _execute_proposal( +pub(crate) fn execute_proposal( app: &mut App, proposal_single: &Addr, proposal_single_code_hash: String, + sender: &str, auth: Auth, proposal_id: u64, ) { - let mut sender = Addr::unchecked(""); - if let Auth::ViewingKey { address, .. } = auth.clone() { - sender = Addr::unchecked(address); - } - app.execute_contract( Addr::unchecked(sender), &ContractInfo { address: proposal_single.clone(), - code_hash: proposal_single_code_hash, + code_hash: proposal_single_code_hash.clone(), }, &ExecuteMsg::Execute { auth, proposal_id }, &[], @@ -241,7 +295,7 @@ pub(crate) fn close_proposal_should_fail( Addr::unchecked(sender), &ContractInfo { address: proposal_single.clone(), - code_hash: proposal_single_code_hash, + code_hash: proposal_single_code_hash.clone(), }, &ExecuteMsg::Close { proposal_id }, &[], @@ -251,7 +305,7 @@ pub(crate) fn close_proposal_should_fail( .unwrap() } -pub(crate) fn _close_proposal( +pub(crate) fn close_proposal( app: &mut App, proposal_single: &Addr, proposal_single_code_hash: String, @@ -262,7 +316,7 @@ pub(crate) fn _close_proposal( Addr::unchecked(sender), &ContractInfo { address: proposal_single.clone(), - code_hash: proposal_single_code_hash, + code_hash: proposal_single_code_hash.clone(), }, &ExecuteMsg::Close { proposal_id }, &[], @@ -270,126 +324,64 @@ pub(crate) fn _close_proposal( .unwrap(); } -pub(crate) fn update_config( - app: &mut App, - proposal_single: &Addr, - proposal_single_code_hash: String, - sender: &str, -) { - app.execute_contract( - Addr::unchecked(sender), - &ContractInfo { - address: proposal_single.clone(), - code_hash: proposal_single_code_hash, - }, - &ExecuteMsg::UpdateConfig { - threshold: dao_voting::threshold::Threshold::ThresholdQuorum { - quorum: dao_voting::threshold::PercentageThreshold::Percent(Decimal::percent(15)), - threshold: dao_voting::threshold::PercentageThreshold::Majority {}, - }, - max_voting_period: Duration::Time(604800), // One week. - min_voting_period: None, - only_members_execute: true, - allow_revoting: false, - close_proposal_on_execution_failure: true, - veto: None, - }, - &[], - ) +pub(crate) fn mint_natives(app: &mut App, receiver: &str, amount: Vec) { + app.sudo(secret_multi_test::SudoMsg::Bank(BankSudo::Mint { + to_address: receiver.to_string(), + amount, + })) .unwrap(); } -pub(crate) fn update_config_should_fail( +pub(crate) fn mint_snip20s( app: &mut App, - proposal_single: &Addr, - proposal_single_code_hash: String, - sender: &str, -) -> ContractError { - app.execute_contract( - Addr::unchecked(sender), - &ContractInfo { - address: proposal_single.clone(), - code_hash: proposal_single_code_hash, - }, - &ExecuteMsg::UpdateConfig { - threshold: dao_voting::threshold::Threshold::ThresholdQuorum { - quorum: dao_voting::threshold::PercentageThreshold::Percent(Decimal::percent(15)), - threshold: dao_voting::threshold::PercentageThreshold::Majority {}, - }, - max_voting_period: Duration::Time(604800), // One week. - min_voting_period: None, - only_members_execute: true, - allow_revoting: false, - close_proposal_on_execution_failure: true, - veto: None, - }, - &[], - ) - .unwrap_err() - .downcast() - .unwrap() -} - -pub(crate) fn update_pre_propose_info( - app: &mut App, - proposal_single: &Addr, - proposal_single_code_hash: String, - sender: &str, + snip20_contract: &Addr, + snip20_contract_code_hash: String, + sender: &Addr, + receiver: &str, + amount: u128, ) { app.execute_contract( - Addr::unchecked(sender), + sender.clone(), &ContractInfo { - address: proposal_single.clone(), - code_hash: proposal_single_code_hash, + address: snip20_contract.clone(), + code_hash: snip20_contract_code_hash.clone(), }, - &ExecuteMsg::UpdatePreProposeInfo { - info: dao_voting::pre_propose::PreProposeInfo::AnyoneMayPropose {}, + &snip20_reference_impl::msg::ExecuteMsg::Mint { + recipient: receiver.to_string(), + amount: Uint128::new(amount), + memo: None, + decoys: None, + entropy: None, + padding: None, }, &[], ) .unwrap(); } -pub(crate) fn update_pre_propose_info_should_fail( - app: &mut App, - proposal_single: &Addr, - proposal_single_code_hash: String, - sender: &str, -) -> ContractError { - app.execute_contract( - Addr::unchecked(sender), - &ContractInfo { - address: proposal_single.clone(), - code_hash: proposal_single_code_hash, - }, - &ExecuteMsg::UpdatePreProposeInfo { - info: dao_voting::pre_propose::PreProposeInfo::AnyoneMayPropose {}, - }, - &[], - ) - .unwrap_err() - .downcast() - .unwrap() -} - -pub(crate) fn execute_veto_fails( - app: &mut App, - proposal_single: &Addr, - proposal_single_code_hash: String, - sender: &str, - proposal_id: u64, -) -> ContractError { - app.execute_contract( - Addr::unchecked(sender), - &ContractInfo { - address: proposal_single.clone(), - code_hash: proposal_single_code_hash, - }, - &ExecuteMsg::Veto { proposal_id }, +pub(crate) fn instantiate_sni20_base_default(app: &mut App) -> ContractInfo { + let snip20_info = app.store_code(snip20_base_contract()); + let snip20_instantiate = snip20_reference_impl::msg::InstantiateMsg { + name: "snip20 token".to_string(), + symbol: "sniptwenty".to_string(), + decimals: 6, + initial_balances: Some(vec![InitialBalance { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(10_000_000), + }]), + admin: None, + prng_seed: to_binary(&"seed".to_string()).unwrap(), + config: None, + supported_denoms: None, + }; + app.instantiate_contract( + snip20_info, + Addr::unchecked("ekez"), + &snip20_instantiate, &[], + "snip20-base", + None, ) - .unwrap_err() - .downcast() .unwrap() } @@ -399,17 +391,17 @@ pub(crate) fn add_proposal_hook( proposal_module_code_hash: String, sender: &str, hook_addr: &str, - hook_code_hash: &str, + hook_code_hash: String, ) { app.execute_contract( Addr::unchecked(sender), &ContractInfo { address: proposal_module.clone(), - code_hash: proposal_module_code_hash, + code_hash: proposal_module_code_hash.clone(), }, &ExecuteMsg::AddProposalHook { address: hook_addr.to_string(), - code_hash: hook_code_hash.to_string(), + code_hash: hook_code_hash, }, &[], ) @@ -422,17 +414,17 @@ pub(crate) fn add_proposal_hook_should_fail( proposal_module_code_hash: String, sender: &str, hook_addr: &str, - hook_code_hash: &str, + hook_code_hash: String, ) -> ContractError { app.execute_contract( Addr::unchecked(sender), &ContractInfo { address: proposal_module.clone(), - code_hash: proposal_module_code_hash, + code_hash: proposal_module_code_hash.clone(), }, &ExecuteMsg::AddProposalHook { address: hook_addr.to_string(), - code_hash: hook_code_hash.to_string(), + code_hash: hook_code_hash, }, &[], ) @@ -447,17 +439,17 @@ pub(crate) fn remove_proposal_hook( proposal_module_code_hash: String, sender: &str, hook_addr: &str, - hook_code_hash: &str, + hook_code_hash: String, ) { app.execute_contract( Addr::unchecked(sender), &ContractInfo { address: proposal_module.clone(), - code_hash: proposal_module_code_hash, + code_hash: proposal_module_code_hash.clone(), }, &ExecuteMsg::RemoveProposalHook { address: hook_addr.to_string(), - code_hash: hook_code_hash.to_string(), + code_hash: hook_code_hash, }, &[], ) @@ -470,17 +462,17 @@ pub(crate) fn remove_proposal_hook_should_fail( proposal_module_code_hash: String, sender: &str, hook_addr: &str, - hook_code_hash: &str, + hook_code_hash: String, ) -> ContractError { app.execute_contract( Addr::unchecked(sender), &ContractInfo { address: proposal_module.clone(), - code_hash: proposal_module_code_hash, + code_hash: proposal_module_code_hash.clone(), }, &ExecuteMsg::RemoveProposalHook { address: hook_addr.to_string(), - code_hash: hook_code_hash.to_string(), + code_hash: hook_code_hash, }, &[], ) @@ -495,17 +487,17 @@ pub(crate) fn add_vote_hook( proposal_module_code_hash: String, sender: &str, hook_addr: &str, - hook_code_hash: &str, + hook_code_hash: String, ) { app.execute_contract( Addr::unchecked(sender), &ContractInfo { address: proposal_module.clone(), - code_hash: proposal_module_code_hash, + code_hash: proposal_module_code_hash.clone(), }, &ExecuteMsg::AddVoteHook { address: hook_addr.to_string(), - code_hash: hook_code_hash.to_string(), + code_hash: hook_code_hash, }, &[], ) @@ -518,17 +510,17 @@ pub(crate) fn add_vote_hook_should_fail( proposal_module_code_hash: String, sender: &str, hook_addr: &str, - hook_code_hash: &str, + hook_code_hash: String, ) -> ContractError { app.execute_contract( Addr::unchecked(sender), &ContractInfo { address: proposal_module.clone(), - code_hash: proposal_module_code_hash, + code_hash: proposal_module_code_hash.clone(), }, &ExecuteMsg::AddVoteHook { address: hook_addr.to_string(), - code_hash: hook_code_hash.to_string(), + code_hash: hook_code_hash, }, &[], ) @@ -543,17 +535,17 @@ pub(crate) fn remove_vote_hook( proposal_module_code_hash: String, sender: &str, hook_addr: &str, - hook_code_hash: &str, + hook_code_hash: String, ) { app.execute_contract( Addr::unchecked(sender), &ContractInfo { address: proposal_module.clone(), - code_hash: proposal_module_code_hash, + code_hash: proposal_module_code_hash.clone(), }, &ExecuteMsg::RemoveVoteHook { address: hook_addr.to_string(), - code_hash: hook_code_hash.to_string(), + code_hash: hook_code_hash, }, &[], ) @@ -566,17 +558,17 @@ pub(crate) fn remove_vote_hook_should_fail( proposal_module_code_hash: String, sender: &str, hook_addr: &str, - hook_code_hash: &str, + hook_code_hash: String, ) -> ContractError { app.execute_contract( Addr::unchecked(sender), &ContractInfo { address: proposal_module.clone(), - code_hash: proposal_module_code_hash, + code_hash: proposal_module_code_hash.clone(), }, &ExecuteMsg::RemoveVoteHook { address: hook_addr.to_string(), - code_hash: hook_code_hash.to_string(), + code_hash: hook_code_hash, }, &[], ) @@ -584,7 +576,6 @@ pub(crate) fn remove_vote_hook_should_fail( .downcast() .unwrap() } - pub(crate) fn create_viewing_key( app: &mut App, contract_info: ContractInfo, @@ -608,3 +599,23 @@ pub(crate) fn create_viewing_key( }; viewing_key } + +pub(crate) fn create_snip20_viewing_key( + app: &mut App, + contract_info: ContractInfo, + info: MessageInfo, +) -> String { + let msg = snip20_reference_impl::msg::ExecuteMsg::CreateViewingKey { + entropy: "entropy".to_string(), + padding: None, + }; + let res = app + .execute_contract(info.sender, &contract_info, &msg, &[]) + .unwrap(); + let mut viewing_key = String::new(); + let data: snip20_reference_impl::msg::ExecuteAnswer = from_binary(&res.data.unwrap()).unwrap(); + if let snip20_reference_impl::msg::ExecuteAnswer::CreateViewingKey { key } = data { + viewing_key = key; + }; + viewing_key +} diff --git a/contracts/proposal/dao-proposal-single/src/testing/instantiate.rs b/contracts/proposal/dao-proposal-single/src/testing/instantiate.rs index ca118df..4a066b0 100644 --- a/contracts/proposal/dao-proposal-single/src/testing/instantiate.rs +++ b/contracts/proposal/dao-proposal-single/src/testing/instantiate.rs @@ -1,21 +1,65 @@ -use cosmwasm_std::{to_binary, Addr, ContractInfo, Decimal}; -use secret_multi_test::{App, Executor}; +use cosmwasm_std::{ + testing::mock_info, to_binary, Addr, Coin, ContractInfo, Decimal, Empty, Uint128, +}; + +use dao_interface::{ + msg::InitialBalance, + state::{Admin, AnyContractInfo, ModuleInstantiateInfo}, +}; +use dao_pre_propose_single as cppbps; +use secret_multi_test::{next_block, App, BankSudo, Executor, SudoMsg}; use secret_utils::Duration; use dao_voting::{ + deposit::{DepositRefundPolicy, UncheckedDepositInfo, VotingModuleTokenType}, pre_propose::PreProposeInfo, - threshold::{PercentageThreshold, Threshold::ThresholdQuorum}, + threshold::{ActiveThreshold, PercentageThreshold, Threshold::ThresholdQuorum}, }; -use shade_protocol::utils::asset::RawContract; +use dao_voting_cw4::msg::GroupContract; +use snip721_reference_impl::msg::ReceiverInfo; use crate::msg::InstantiateMsg; -use super::{contracts::query_auth_contract, CREATOR_ADDR}; +use super::{ + contracts::{ + cw4_group_contract, cw4_voting_contract, cw_core_contract, + native_staked_balances_voting_contract, proposal_single_contract, query_auth_contract, + snip20_base_contract, snip20_stake_contract, snip20_staked_balances_voting_contract, + snip721_base_contract, snip721_stake_contract, + }, + execute::create_viewing_key, + CREATOR_ADDR, +}; -pub(crate) fn get_default_token_dao_proposal_module_instantiate( - query_auth: RawContract, - dao_code_hash: String, -) -> InstantiateMsg { +pub(crate) fn get_pre_propose_info( + app: &mut App, + deposit_info: Option, + open_proposal_submission: bool, +) -> PreProposeInfo { + let pre_propose_contract = + app.store_code(crate::testing::contracts::pre_propose_single_contract()); + let proposal_single_contract = + app.store_code(crate::testing::contracts::proposal_single_contract()); + PreProposeInfo::ModuleMayPropose { + info: ModuleInstantiateInfo { + code_id: pre_propose_contract.code_id, + code_hash: pre_propose_contract.code_hash, + msg: to_binary(&cppbps::InstantiateMsg { + deposit_info, + open_proposal_submission, + extension: Empty::default(), + proposal_module_code_hash: proposal_single_contract.code_hash.clone(), + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "pre_propose_contract".to_string(), + }, + } +} + +pub(crate) fn get_default_token_dao_proposal_module_instantiate(app: &mut App) -> InstantiateMsg { + let dao_info = app.store_code(cw_core_contract()); InstantiateMsg { veto: None, threshold: ThresholdQuorum { @@ -26,30 +70,698 @@ pub(crate) fn get_default_token_dao_proposal_module_instantiate( min_voting_period: None, only_members_execute: true, allow_revoting: false, - pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + pre_propose_info: get_pre_propose_info( + app, + Some(UncheckedDepositInfo { + denom: dao_voting::deposit::DepositToken::VotingModuleToken { + token_type: VotingModuleTokenType::Cw20, + }, + amount: Uint128::new(10_000_000), + refund_policy: DepositRefundPolicy::OnlyPassed, + }), + false, + ), close_proposal_on_execution_failure: true, - dao_code_hash, - query_auth: Some(query_auth), + dao_code_hash: dao_info.code_hash, + query_auth: None, + } +} + +// Same as above but no proposal deposit. +pub(crate) fn get_default_non_token_dao_proposal_module_instantiate( + app: &mut App, +) -> InstantiateMsg { + let dao_info = app.store_code(cw_core_contract()); + + InstantiateMsg { + veto: None, + threshold: ThresholdQuorum { + threshold: PercentageThreshold::Percent(Decimal::percent(15)), + quorum: PercentageThreshold::Majority {}, + }, + max_voting_period: Duration::Time(604800), // One week. + min_voting_period: None, + only_members_execute: true, + allow_revoting: false, + pre_propose_info: get_pre_propose_info(app, None, false), + close_proposal_on_execution_failure: true, + dao_code_hash: dao_info.code_hash, + query_auth: None, + } +} + +pub(crate) fn _instantiate_with_staked_snip721_governance( + app: &mut App, + proposal_module_instantiate: InstantiateMsg, + initial_balances: Option>, +) -> ContractInfo { + let proposal_module_info = app.store_code(proposal_single_contract()); + let query_auth = app.store_code(query_auth_contract()); + let snip20_info = app.store_code(snip20_base_contract()); + + let initial_balances = initial_balances.unwrap_or_else(|| { + vec![InitialBalance { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(100_000_000), + }] + }); + + let initial_balances: Vec = { + let mut already_seen = vec![]; + initial_balances + .into_iter() + .filter(|InitialBalance { address, amount: _ }| { + if already_seen.contains(address) { + false + } else { + already_seen.push(address.clone()); + true + } + }) + .collect() + }; + + let snip721_info = app.store_code(snip721_base_contract()); + let snip721_stake_info = app.store_code(snip721_stake_contract()); + let core_info = app.store_code(cw_core_contract()); + + let nft_contract_info = app + .instantiate_contract( + snip721_info.clone(), + Addr::unchecked("ekez"), + &snip721_reference_impl::msg::InstantiateMsg { + symbol: "token".to_string(), + name: "ekez token best token".to_string(), + admin: Some("ekez".to_string()), + entropy: "entropy".to_string(), + royalty_info: None, + config: None, + post_init_callback: None, + }, + &[], + "nft-staking", + None, + ) + .unwrap(); + + let instantiate_core = dao_interface::msg::InstantiateMsg { + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that builds DAOs".to_string(), + dao_uri: None, + image_url: None, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: snip721_stake_info.code_id, + code_hash: snip721_stake_info.code_hash.clone(), + msg: to_binary(&dao_voting_snip721_staked::msg::InstantiateMsg { + unstaking_duration: None, + nft_contract: dao_voting_snip721_staked::msg::NftContract::Existing { + address: nft_contract_info.address.clone().to_string(), + code_hash: nft_contract_info.code_hash.clone(), + }, + active_threshold: None, + dao_code_hash: core_info.code_hash.clone(), + query_auth: None, + }) + .unwrap(), + admin: None, + funds: vec![], + label: "DAO DAO voting module".to_string(), + }, + proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { + code_id: proposal_module_info.code_id, + code_hash: proposal_module_info.code_hash.clone(), + msg: to_binary(&proposal_module_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO governance module.".to_string(), + }], + initial_items: None, + query_auth_code_id: query_auth.code_id, + query_auth_code_hash: query_auth.code_hash, + prng_seed: "seed".to_string(), + snip20_code_hash: snip20_info.code_hash, + snip721_code_hash: snip721_info.code_hash.clone(), + }; + + let core_contract_info = app + .instantiate_contract( + core_info.clone(), + Addr::unchecked(CREATOR_ADDR), + &instantiate_core, + &[], + "DAO DAO", + None, + ) + .unwrap(); + + let core_state: dao_interface::query::DumpStateResponse = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::DumpState {}, + ) + .unwrap(); + let staking_addr = core_state.voting_module; + let staking_code_hash = core_state.voting_module_code_hash; + + for InitialBalance { address, amount } in initial_balances { + for i in 0..amount.u128() { + app.execute_contract( + Addr::unchecked("ekez"), + &nft_contract_info.clone(), + &snip721_reference_impl::msg::ExecuteMsg::MintNft { + token_id: format!("{address}_{i}").into(), + owner: Some(address.clone()), + public_metadata: None, + private_metadata: None, + serial_number: None, + royalty_info: None, + transferable: Some(true), + memo: None, + padding: None, + }, + &[], + ) + .unwrap(); + app.execute_contract( + Addr::unchecked(address.clone()), + &nft_contract_info.clone(), + &snip721_reference_impl::msg::ExecuteMsg::SendNft { + contract: staking_addr.to_string(), + token_id: format!("{address}_{i}"), + msg: Some(to_binary("").unwrap()), + receiver_info: Some(ReceiverInfo { + recipient_code_hash: staking_code_hash.clone(), + also_implements_batch_receive_nft: None, + }), + memo: None, + padding: None, + }, + &[], + ) + .unwrap(); + } + } + + // Update the block so that staked balances appear. + app.update_block(|block| block.height += 1); + + core_contract_info +} + +pub(crate) fn instantiate_with_native_staked_balances_governance( + app: &mut App, + proposal_module_instantiate: InstantiateMsg, + initial_balances: Option>, +) -> ContractInfo { + let proposal_module_info = app.store_code(proposal_single_contract()); + let query_auth = app.store_code(query_auth_contract()); + + let initial_balances = initial_balances.unwrap_or_else(|| { + vec![InitialBalance { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(100_000_000), + }] + }); + + // Collapse balances so that we can test double votes. + let initial_balances: Vec = { + let mut already_seen = vec![]; + initial_balances + .into_iter() + .filter(|InitialBalance { address, amount: _ }| { + if already_seen.contains(address) { + false + } else { + already_seen.push(address.clone()); + true + } + }) + .collect() + }; + + let native_stake_info = app.store_code(native_staked_balances_voting_contract()); + let core_info = app.store_code(cw_core_contract()); + + let instantiate_core = dao_interface::msg::InstantiateMsg { + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that builds DAOs".to_string(), + dao_uri: None, + image_url: None, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: native_stake_info.code_id, + code_hash: native_stake_info.code_hash.clone(), + msg: to_binary(&dao_voting_token_staked::msg::InstantiateMsg { + token_info: dao_voting_token_staked::msg::TokenInfo::Existing { + denom: "ujuno".to_string(), + }, + unstaking_duration: None, + active_threshold: None, + dao_code_hash: core_info.code_hash.clone(), + query_auth: None, + }) + .unwrap(), + admin: None, + funds: vec![], + label: "DAO DAO voting module".to_string(), + }, + proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { + code_id: proposal_module_info.code_id, + code_hash: proposal_module_info.code_hash.clone(), + msg: to_binary(&proposal_module_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO governance module.".to_string(), + }], + initial_items: None, + query_auth_code_id: query_auth.code_id, + query_auth_code_hash: query_auth.code_hash, + prng_seed: "seeed".to_string(), + snip20_code_hash: "".to_string(), + snip721_code_hash: "".to_string(), + }; + + let core_contract_info = app + .instantiate_contract( + core_info, + Addr::unchecked(CREATOR_ADDR), + &instantiate_core, + &[], + "DAO DAO", + None, + ) + .unwrap(); + + let gov_state: dao_interface::query::DumpStateResponse = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::DumpState {}, + ) + .unwrap(); + let native_staking_addr = gov_state.voting_module; + let native_staking_code_hash = gov_state.voting_module_code_hash; + + let query_auth_info: AnyContractInfo = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::QueryAuthInfo {}, + ) + .unwrap(); + + for InitialBalance { address, amount } in initial_balances { + let viewing_key = create_viewing_key( + app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(&address, &[]), + ); + app.sudo(SudoMsg::Bank(BankSudo::Mint { + to_address: address.clone(), + amount: vec![Coin { + denom: "ujuno".to_string(), + amount, + }], + })) + .unwrap(); + app.execute_contract( + Addr::unchecked(&address), + &ContractInfo { + address: native_staking_addr.clone(), + code_hash: native_staking_code_hash.clone(), + }, + &dao_voting_token_staked::msg::ExecuteMsg::Stake { + auth: shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key, + address, + }, + }, + &[Coin { + amount, + denom: "ujuno".to_string(), + }], + ) + .unwrap(); } + + app.update_block(next_block); + + core_contract_info } -pub(crate) fn instantiate_query_auth(app: &mut App) -> ContractInfo { - let query_auth_info = app.store_code(query_auth_contract()); - let msg = shade_protocol::contract_interfaces::query_auth::InstantiateMsg { - admin_auth: shade_protocol::Contract { - address: Addr::unchecked("admin_contract"), - code_hash: "code_hash".to_string(), +pub(crate) fn instantiate_with_staked_balances_governance( + app: &mut App, + proposal_module_instantiate: InstantiateMsg, + initial_balances: Option>, +) -> ContractInfo { + let proposal_module_info = app.store_code(proposal_single_contract()); + let query_auth = app.store_code(query_auth_contract()); + + let initial_balances = initial_balances.unwrap_or_else(|| { + vec![InitialBalance { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(100_000_000), + }] + }); + + // Collapse balances so that we can test double votes. + let initial_balances: Vec = { + let mut already_seen = vec![]; + initial_balances + .into_iter() + .filter(|InitialBalance { address, amount: _ }| { + if already_seen.contains(address) { + false + } else { + already_seen.push(address.clone()); + true + } + }) + .collect() + }; + + let snip20_info = app.store_code(snip20_base_contract()); + let snip20_stake_info = app.store_code(snip20_stake_contract()); + let staked_balances_voting_info = app.store_code(snip20_staked_balances_voting_contract()); + let core_info = app.store_code(cw_core_contract()); + + let instantiate_core = dao_interface::msg::InstantiateMsg { + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that builds DAOs".to_string(), + dao_uri: None, + image_url: None, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: staked_balances_voting_info.code_id, + code_hash: staked_balances_voting_info.code_hash, + msg: to_binary(&dao_voting_snip20_staked::msg::InstantiateMsg { + active_threshold: None, + token_info: dao_voting_snip20_staked::msg::Snip20TokenInfo::New { + code_id: snip20_info.code_id, + code_hash: snip20_info.code_hash.clone(), + name: "DAO DAO".to_string(), + symbol: "DAO".to_string(), + decimals: 6, + initial_balances: initial_balances.clone(), + staking_code_id: snip20_stake_info.code_id, + staking_code_hash: snip20_stake_info.code_hash, + unstaking_duration: Some(Duration::Height(6)), + initial_dao_balance: None, + }, + dao_code_hash: core_info.code_hash.clone(), + query_auth: None, + }) + .unwrap(), + admin: None, + funds: vec![], + label: "DAO DAO voting module".to_string(), }, - prng_seed: to_binary("seed").unwrap(), + proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { + code_id: proposal_module_info.code_id, + code_hash: proposal_module_info.code_hash, + msg: to_binary(&proposal_module_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO governance module.".to_string(), + }], + initial_items: None, + query_auth_code_id: query_auth.code_id, + query_auth_code_hash: query_auth.code_hash, + prng_seed: "seed".to_string(), + snip20_code_hash: snip20_info.code_hash.clone(), + snip721_code_hash: "".to_string(), + }; + + let core_contract_info = app + .instantiate_contract( + core_info, + Addr::unchecked(CREATOR_ADDR), + &instantiate_core, + &[], + "DAO DAO", + None, + ) + .unwrap(); + + let gov_state: dao_interface::query::DumpStateResponse = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.to_string(), + &dao_interface::msg::QueryMsg::DumpState {}, + ) + .unwrap(); + + let query_auth_info: AnyContractInfo = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::QueryAuthInfo {}, + ) + .unwrap(); + + let voting_module = gov_state.voting_module; + let voting_module_code_hash = gov_state.voting_module_code_hash; + + let staking_contract: AnyContractInfo = app + .wrap() + .query_wasm_smart( + voting_module_code_hash.clone(), + voting_module.clone(), + &dao_voting_snip20_staked::msg::QueryMsg::StakingContract {}, + ) + .unwrap(); + let token_contract: AnyContractInfo = app + .wrap() + .query_wasm_smart( + voting_module_code_hash, + voting_module, + &dao_interface::voting::Query::TokenContract {}, + ) + .unwrap(); + + // Stake all the initial balances. + for InitialBalance { address, amount } in initial_balances { + let viewing_key = create_viewing_key( + app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(&address, &[]), + ); + app.execute_contract( + Addr::unchecked(address.clone()), + &ContractInfo { + address: token_contract.addr.clone(), + code_hash: token_contract.code_hash.clone(), + }, + &snip20_reference_impl::msg::ExecuteMsg::Send { + amount, + msg: Some( + to_binary(&snip20_stake::msg::ReceiveMsg::Stake { + auth: Box::new(shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key, + address, + }), + }) + .unwrap(), + ), + recipient: staking_contract.addr.clone().to_string(), + recipient_code_hash: Some(staking_contract.code_hash.clone()), + memo: None, + decoys: None, + entropy: None, + padding: None, + }, + &[], + ) + .unwrap(); + } + + // Update the block so that those staked balances appear. + app.update_block(|block| block.height += 1); + + core_contract_info +} + +pub(crate) fn instantiate_with_staking_active_threshold( + app: &mut App, + proposal_module_instantiate: InstantiateMsg, + initial_balances: Option>, + active_threshold: Option, +) -> ContractInfo { + let proposal_module_info = app.store_code(proposal_single_contract()); + let snip20_info = app.store_code(snip20_base_contract()); + let snip20_staking_info = app.store_code(snip20_stake_contract()); + let core_info = app.store_code(cw_core_contract()); + let votemod_info = app.store_code(snip20_staked_balances_voting_contract()); + let query_auth = app.store_code(query_auth_contract()); + + let initial_balances = initial_balances.unwrap_or_else(|| { + vec![InitialBalance { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(100_000_000), + }] + }); + + let governance_instantiate = dao_interface::msg::InstantiateMsg { + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that builds DAOs".to_string(), + dao_uri: None, + image_url: None, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: votemod_info.code_id, + code_hash: votemod_info.code_hash.clone(), + msg: to_binary(&dao_voting_snip20_staked::msg::InstantiateMsg { + token_info: dao_voting_snip20_staked::msg::Snip20TokenInfo::New { + code_id: snip20_info.code_id, + code_hash: snip20_info.code_hash.clone(), + name: "DAO".to_string(), + symbol: "DAO".to_string(), + decimals: 6, + initial_balances, + staking_code_id: snip20_staking_info.code_id, + unstaking_duration: None, + initial_dao_balance: None, + staking_code_hash: snip20_staking_info.code_hash.clone(), + }, + active_threshold, + dao_code_hash: core_info.code_hash.clone(), + query_auth: None, + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO voting module".to_string(), + }, + proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { + code_id: proposal_module_info.code_id, + code_hash: proposal_module_info.code_hash, + msg: to_binary(&proposal_module_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO governance module".to_string(), + }], + initial_items: None, + query_auth_code_id: query_auth.code_id, + query_auth_code_hash: query_auth.code_hash, + prng_seed: "seed".into(), + snip20_code_hash: "todo!()".to_string(), + snip721_code_hash: "todo!()".to_string(), }; app.instantiate_contract( - query_auth_info, + core_info, Addr::unchecked(CREATOR_ADDR), - &msg, + &governance_instantiate, &[], - "query_auth", + "DAO DAO", None, ) .unwrap() } + +pub(crate) fn instantiate_with_cw4_groups_governance( + app: &mut App, + proposal_module_instantiate: InstantiateMsg, + initial_weights: Option>, +) -> ContractInfo { + let proposal_module_info = app.store_code(proposal_single_contract()); + let cw4_info = app.store_code(cw4_group_contract()); + let core_info = app.store_code(cw_core_contract()); + let votemod_info = app.store_code(cw4_voting_contract()); + let query_auth = app.store_code(query_auth_contract()); + + let initial_weights = initial_weights.unwrap_or_else(|| { + vec![InitialBalance { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(1), + }] + }); + + // Remove duplicates so that we can test duplicate voting. + let initial_weights: Vec = { + let mut already_seen = vec![]; + initial_weights + .into_iter() + .filter(|InitialBalance { address, .. }| { + if already_seen.contains(address) { + false + } else { + already_seen.push(address.clone()); + true + } + }) + .map(|InitialBalance { address, amount }| cw4::Member { + addr: address, + weight: amount.u128() as u64, + }) + .collect() + }; + + let governance_instantiate = dao_interface::msg::InstantiateMsg { + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that builds DAOs".to_string(), + dao_uri: None, + image_url: None, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: votemod_info.code_id, + code_hash: votemod_info.code_hash, + msg: to_binary(&dao_voting_cw4::msg::InstantiateMsg { + group_contract: GroupContract::New { + cw4_group_code_id: cw4_info.code_id, + initial_members: initial_weights, + cw4_group_code_hash: cw4_info.code_hash, + query_auth: None, + }, + dao_code_hash: core_info.code_hash.clone(), + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO voting module".to_string(), + }, + proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { + code_id: proposal_module_info.code_id, + msg: to_binary(&proposal_module_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO governance module".to_string(), + code_hash: proposal_module_info.code_hash, + }], + initial_items: None, + query_auth_code_id: query_auth.code_id, + query_auth_code_hash: query_auth.code_hash, + prng_seed: "todo!()".to_string(), + snip20_code_hash: "todo!()".to_string(), + snip721_code_hash: "todo!()".to_string(), + }; + + let addr = app + .instantiate_contract( + core_info, + Addr::unchecked(CREATOR_ADDR), + &governance_instantiate, + &[], + "DAO DAO", + None, + ) + .unwrap(); + + // Update the block so that weights appear. + app.update_block(|block| block.height += 1); + + addr +} diff --git a/contracts/proposal/dao-proposal-single/src/testing/mod.rs b/contracts/proposal/dao-proposal-single/src/testing/mod.rs index 78a717a..59cf467 100644 --- a/contracts/proposal/dao-proposal-single/src/testing/mod.rs +++ b/contracts/proposal/dao-proposal-single/src/testing/mod.rs @@ -1,8 +1,9 @@ +mod adversarial_tests; mod contracts; +mod do_votes; mod execute; mod instantiate; mod queries; mod tests; pub(crate) const CREATOR_ADDR: &str = "creator"; -pub(crate) const DAO_ADDR: &str = "dao"; diff --git a/contracts/proposal/dao-proposal-single/src/testing/queries.rs b/contracts/proposal/dao-proposal-single/src/testing/queries.rs index 2d2f383..2ff4c8b 100644 --- a/contracts/proposal/dao-proposal-single/src/testing/queries.rs +++ b/contracts/proposal/dao-proposal-single/src/testing/queries.rs @@ -1,17 +1,310 @@ -use cosmwasm_std::Addr; +use cosmwasm_std::{Addr, ContractInfo, Uint128}; +use dao_interface::state::{ + AnyContractInfo, ProposalModule, ProposalModuleStatus, VotingModuleInfo, +}; use secret_multi_test::App; -use crate::msg::QueryMsg; +use cw_hooks::HooksResponse; +use dao_pre_propose_single as cppbps; +use dao_voting::pre_propose::ProposalCreationPolicy; +use shade_protocol::basic_staking::Auth; + +use crate::{ + msg::QueryMsg, + query::{ProposalListResponse, ProposalResponse, VoteResponse}, + state::Config, +}; + +pub(crate) fn query_deposit_config_and_pre_propose_module( + app: &App, + proposal_single_addr: &Addr, + proposal_single_code_hash: String, +) -> (cppbps::Config, ContractInfo) { + let proposal_creation_policy = + query_creation_policy(app, proposal_single_addr, proposal_single_code_hash); + + if let ProposalCreationPolicy::Module { + addr: module_addr, + code_hash, + } = proposal_creation_policy + { + let deposit_config = query_pre_proposal_single_config(app, &module_addr, code_hash.clone()); + + ( + deposit_config, + ContractInfo { + address: module_addr, + code_hash, + }, + ) + } else { + panic!("no pre-propose module.") + } +} + +pub(crate) fn query_proposal_config( + app: &App, + proposal_single_addr: &Addr, + proposal_single_code_hash: String, +) -> Config { + app.wrap() + .query_wasm_smart( + proposal_single_code_hash, + proposal_single_addr, + &QueryMsg::Config {}, + ) + .unwrap() +} + +pub(crate) fn query_creation_policy( + app: &App, + proposal_single_addr: &Addr, + proposal_single_code_hash: String, +) -> ProposalCreationPolicy { + app.wrap() + .query_wasm_smart( + proposal_single_code_hash, + proposal_single_addr, + &QueryMsg::ProposalCreationPolicy {}, + ) + .unwrap() +} + +pub(crate) fn query_list_proposals( + app: &App, + proposal_single_addr: &Addr, + proposal_single_code_hash: String, + start_after: Option, + limit: Option, +) -> ProposalListResponse { + app.wrap() + .query_wasm_smart( + proposal_single_code_hash, + proposal_single_addr, + &QueryMsg::ListProposals { start_after, limit }, + ) + .unwrap() +} + +pub(crate) fn query_vote( + app: &App, + proposal_module_addr: &Addr, + proposal_module_code_hash: String, + auth: Auth, + proposal_id: u64, +) -> VoteResponse { + app.wrap() + .query_wasm_smart( + proposal_module_code_hash, + proposal_module_addr, + &QueryMsg::GetVote { + proposal_id, + auth: Box::new(auth), + }, + ) + .unwrap() +} + +pub(crate) fn query_proposal_hooks( + app: &App, + proposal_single_addr: &Addr, + proposal_single_code_hash: String, +) -> HooksResponse { + app.wrap() + .query_wasm_smart( + proposal_single_code_hash, + proposal_single_addr, + &QueryMsg::ProposalHooks {}, + ) + .unwrap() +} + +pub(crate) fn query_vote_hooks( + app: &App, + proposal_single_addr: &Addr, + proposal_single_code_hash: String, +) -> HooksResponse { + app.wrap() + .query_wasm_smart( + proposal_single_code_hash, + proposal_single_addr, + &QueryMsg::VoteHooks {}, + ) + .unwrap() +} + +pub(crate) fn query_list_proposals_reverse( + app: &App, + proposal_single_addr: &Addr, + proposal_single_code_hash: String, + start_before: Option, + limit: Option, +) -> ProposalListResponse { + app.wrap() + .query_wasm_smart( + proposal_single_code_hash, + proposal_single_addr, + &QueryMsg::ReverseProposals { + start_before, + limit, + }, + ) + .unwrap() +} + +pub(crate) fn query_pre_proposal_single_config( + app: &App, + pre_propose_addr: &Addr, + pre_propose_code_hash: String, +) -> cppbps::Config { + app.wrap() + .query_wasm_smart( + pre_propose_code_hash, + pre_propose_addr, + &cppbps::QueryMsg::Config {}, + ) + .unwrap() +} + +pub(crate) fn query_pre_proposal_single_deposit_info( + app: &App, + pre_propose_addr: &Addr, + pre_propose_code_hash: String, + proposal_id: u64, +) -> cppbps::DepositInfoResponse { + app.wrap() + .query_wasm_smart( + pre_propose_code_hash, + pre_propose_addr, + &cppbps::QueryMsg::DepositInfo { proposal_id }, + ) + .unwrap() +} + +pub(crate) fn query_single_proposal_module( + app: &App, + core_addr: &Addr, + core_code_hash: String, +) -> AnyContractInfo { + let modules: Vec = app + .wrap() + .query_wasm_smart( + core_code_hash, + core_addr, + &dao_interface::msg::QueryMsg::ProposalModules { + start_after: None, + limit: None, + }, + ) + .unwrap(); + + // Filter out disabled modules. + let modules = modules + .into_iter() + .filter(|module| module.status == ProposalModuleStatus::Enabled) + .collect::>(); + + assert_eq!( + modules.len(), + 1, + "wrong proposal module count. expected 1, got {}", + modules.len() + ); + + AnyContractInfo { + addr: modules.clone().into_iter().next().unwrap().address, + code_hash: modules.into_iter().next().unwrap().code_hash, + } +} + +pub(crate) fn query_dao_token( + app: &App, + core_addr: &Addr, + core_code_hash: String, +) -> AnyContractInfo { + let voting_module = query_voting_module(app, core_addr, core_code_hash); + app.wrap() + .query_wasm_smart( + voting_module.code_hash, + voting_module.addr, + &dao_interface::voting::Query::TokenContract {}, + ) + .unwrap() +} + +pub(crate) fn query_voting_module( + app: &App, + core_addr: &Addr, + core_code_hash: String, +) -> VotingModuleInfo { + app.wrap() + .query_wasm_smart( + core_code_hash, + core_addr, + &dao_interface::msg::QueryMsg::VotingModule {}, + ) + .unwrap() +} + +pub(crate) fn query_balance_cw20< + T: Into, + U: Into, + K: Into, + C: Into, +>( + app: &App, + contract_addr: T, + code_hash: C, + address: U, + key: K, +) -> Uint128 { + let msg = snip20_reference_impl::msg::QueryMsg::Balance { + address: address.into(), + key: key.into(), + }; + let mut balance_amount = Uint128::zero(); + let result: snip20_reference_impl::msg::QueryAnswer = app + .wrap() + .query_wasm_smart(code_hash, contract_addr, &msg) + .unwrap(); + match result { + snip20_reference_impl::msg::QueryAnswer::Balance { amount } => { + balance_amount = amount; + } + _ => (), + } + balance_amount +} + +pub(crate) fn query_balance_native(app: &App, who: &str, denom: &str) -> Uint128 { + let res = app.wrap().query_balance(who, denom).unwrap(); + res.amount +} + +pub(crate) fn query_proposal( + app: &App, + proposal_single_addr: &Addr, + proposal_single_code_hash: String, + id: u64, +) -> ProposalResponse { + app.wrap() + .query_wasm_smart( + proposal_single_code_hash, + proposal_single_addr, + &QueryMsg::Proposal { proposal_id: id }, + ) + .unwrap() +} pub(crate) fn query_next_proposal_id( app: &App, - proposal_single: &Addr, + proposal_single_addr: &Addr, proposal_single_code_hash: String, ) -> u64 { app.wrap() .query_wasm_smart( proposal_single_code_hash, - proposal_single, + proposal_single_addr, &QueryMsg::NextProposalId {}, ) .unwrap() diff --git a/contracts/proposal/dao-proposal-single/src/testing/tests.rs b/contracts/proposal/dao-proposal-single/src/testing/tests.rs index 31d2877..f6c54c6 100644 --- a/contracts/proposal/dao-proposal-single/src/testing/tests.rs +++ b/contracts/proposal/dao-proposal-single/src/testing/tests.rs @@ -1,465 +1,6295 @@ -use cosmwasm_std::{testing::mock_info, Addr, ContractInfo}; -use dao_voting::voting::Vote; -use secret_multi_test::{App, Executor}; -use shade_protocol::{basic_staking::Auth, utils::asset::RawContract}; - -use crate::testing::{ - contracts::proposal_single_contract, - execute::{ - add_proposal_hook, add_proposal_hook_should_fail, add_vote_hook, add_vote_hook_should_fail, - close_proposal_should_fail, execute_proposal_should_fail, make_proposal, - remove_proposal_hook, remove_proposal_hook_should_fail, remove_vote_hook, - remove_vote_hook_should_fail, update_rationale, vote_on_proposal_should_fail, - }, - instantiate::get_default_token_dao_proposal_module_instantiate, +use std::ops::Add; + +use cosmwasm_std::{ + coins, + testing::{mock_dependencies, mock_env, mock_info}, + to_binary, Addr, BankMsg, Binary, ContractInfo, ContractInfoResponse, CosmosMsg, Decimal, + Uint128, WasmMsg, WasmQuery, }; +use cw_denom::CheckedDenom; +use cw_hooks::{HookError, HookItem, HooksResponse}; +use dao_interface::{msg::InitialBalance, state::AnyContractInfo, voting::InfoResponse}; +use dao_testing::{ShouldExecute, TestSingleChoiceVote}; +use dao_voting::{ + deposit::{CheckedDepositInfo, UncheckedDepositInfo}, + pre_propose::{PreProposeInfo, ProposalCreationPolicy}, + proposal::{SingleChoiceProposeMsg as ProposeMsg, MAX_PROPOSAL_SIZE}, + status::Status, + threshold::{ActiveThreshold, PercentageThreshold, Threshold}, + veto::{VetoConfig, VetoError}, + voting::{Vote, Votes}, +}; +use secret_cw2::ContractVersion; +use secret_multi_test::{next_block, App, Executor}; +use secret_utils::Duration; +use shade_protocol::{basic_staking::Auth, Contract}; -use super::{ - execute::{ - create_viewing_key, execute_veto_fails, update_config, update_config_should_fail, - update_pre_propose_info, update_pre_propose_info_should_fail, +use crate::{ + contract::{migrate, CONTRACT_NAME, CONTRACT_VERSION}, + msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}, + proposal::SingleChoiceProposal, + query::ProposalResponse, + state::Config, + testing::{ + execute::{ + add_proposal_hook, add_proposal_hook_should_fail, add_vote_hook, + add_vote_hook_should_fail, close_proposal, close_proposal_should_fail, + create_snip20_viewing_key, execute_proposal, execute_proposal_should_fail, + instantiate_sni20_base_default, make_proposal, mint_natives, mint_snip20s, + remove_proposal_hook, remove_proposal_hook_should_fail, remove_vote_hook, + remove_vote_hook_should_fail, update_rationale, vote_on_proposal, + vote_on_proposal_should_fail, + }, + instantiate::{ + get_default_non_token_dao_proposal_module_instantiate, + get_default_token_dao_proposal_module_instantiate, get_pre_propose_info, + instantiate_with_cw4_groups_governance, instantiate_with_staked_balances_governance, + instantiate_with_staking_active_threshold, + }, + queries::{ + query_balance_cw20, query_balance_native, query_creation_policy, query_dao_token, + query_deposit_config_and_pre_propose_module, query_list_proposals, + query_list_proposals_reverse, query_pre_proposal_single_deposit_info, query_proposal, + query_proposal_config, query_proposal_hooks, query_single_proposal_module, + query_vote_hooks, query_voting_module, + }, }, - instantiate::instantiate_query_auth, - CREATOR_ADDR, DAO_ADDR, + ContractError, }; -// The testcases fails for success a we need whole dao dao flow for this -// and due to different implementations for submsg on scrt network ... we had -// to manually add a function called parse_reply_event_for_address to get deployed -// contract addresses from events which is not working in testcases ... -// So only testcases for failure is covered. +use super::{ + do_votes::do_votes_staked_balances, + execute::{create_viewing_key, vote_on_proposal_with_rationale}, + queries::{query_next_proposal_id, query_vote}, + CREATOR_ADDR, +}; struct CommonTest { app: App, - proposal_single_contract_info: ContractInfo, + core_contract_info: ContractInfo, + proposal_module: AnyContractInfo, + gov_token_info: AnyContractInfo, + proposal_id: u64, + query_auth_info: AnyContractInfo, +} + +fn setup_test(messages: Vec) -> CommonTest { + let mut app = App::default(); + let instantiate = get_default_token_dao_proposal_module_instantiate(&mut app); + let core_contract_info = + instantiate_with_staked_balances_governance(&mut app, instantiate, None); + let proposal_module = query_single_proposal_module( + &app, + &core_contract_info.address.clone(), + core_contract_info.code_hash.clone(), + ); + let gov_token_info = query_dao_token( + &app, + &core_contract_info.address.clone(), + core_contract_info.code_hash.clone(), + ); + + let query_auth_info: AnyContractInfo = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::QueryAuthInfo {}, + ) + .unwrap(); + + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + + // Mint some tokens to pay the proposal deposit. + mint_snip20s( + &mut app, + &gov_token_info.addr.clone(), + gov_token_info.code_hash.clone(), + &core_contract_info.address.clone(), + CREATOR_ADDR, + 10_000_000, + ); + let proposal_id = make_proposal( + &mut app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.to_owned(), + }, + messages, + ); + + CommonTest { + app, + core_contract_info, + proposal_module, + gov_token_info, + proposal_id, + query_auth_info, + } +} + +#[test] +fn test_simple_propose_staked_balances() { + let CommonTest { + app, + core_contract_info: _, + proposal_module, + gov_token_info, + proposal_id, + query_auth_info: _, + } = setup_test(vec![]); + + let created = query_proposal( + &app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + proposal_id, + ); + let current_block = app.block_info(); + + // These values just come from the default instantiate message + // values. + let expected = SingleChoiceProposal { + title: "title".to_string(), + description: "description".to_string(), + proposer: Addr::unchecked(CREATOR_ADDR), + start_height: current_block.height, + expiration: Duration::Time(604800).after(¤t_block), + min_voting_period: None, + threshold: Threshold::ThresholdQuorum { + quorum: PercentageThreshold::Percent(Decimal::percent(15)), + threshold: PercentageThreshold::Majority {}, + }, + allow_revoting: false, + total_power: Uint128::new(100_000_000), + msgs: vec![], + status: Status::Open, + veto: None, + votes: Votes::zero(), + }; + + assert_eq!(created.proposal, expected); + assert_eq!(created.id, 1u64); + + // Check that the deposit info for this proposal looks right. + let (_, pre_propose) = query_deposit_config_and_pre_propose_module( + &app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + ); + let deposit_response = query_pre_proposal_single_deposit_info( + &app, + &pre_propose.address.clone(), + pre_propose.code_hash.clone(), + proposal_id, + ); + + assert_eq!(deposit_response.proposer, Addr::unchecked(CREATOR_ADDR)); + assert_eq!( + deposit_response.deposit_info, + Some(CheckedDepositInfo { + denom: cw_denom::CheckedDenom::Snip20( + gov_token_info.addr.clone(), + gov_token_info.code_hash.clone() + ), + amount: Uint128::new(10_000_000), + refund_policy: dao_voting::deposit::DepositRefundPolicy::OnlyPassed + }) + ); +} + +#[test] +fn test_simple_proposal_cw4_voting() { + let mut app = App::default(); + let instantiate = get_default_non_token_dao_proposal_module_instantiate(&mut app); + let core_contract_info = instantiate_with_cw4_groups_governance(&mut app, instantiate, None); + let proposal_module = query_single_proposal_module( + &app, + &core_contract_info.address.clone(), + core_contract_info.code_hash.clone(), + ); + let query_auth_info: AnyContractInfo = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::QueryAuthInfo {}, + ) + .unwrap(); + + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + let id = make_proposal( + &mut app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key, + address: CREATOR_ADDR.into(), + }, + vec![], + ); + + let created = query_proposal( + &app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + id, + ); + let current_block = app.block_info(); + + // These values just come from the default instantiate message + // values. + let expected = SingleChoiceProposal { + title: "title".to_string(), + description: "description".to_string(), + proposer: Addr::unchecked(CREATOR_ADDR), + start_height: current_block.height, + expiration: Duration::Time(604800).after(¤t_block), + min_voting_period: None, + threshold: Threshold::ThresholdQuorum { + threshold: PercentageThreshold::Percent(Decimal::percent(15)), + quorum: PercentageThreshold::Majority {}, + }, + allow_revoting: false, + total_power: Uint128::new(1), + msgs: vec![], + status: Status::Open, + veto: None, + votes: Votes::zero(), + }; + + assert_eq!(created.proposal, expected); + assert_eq!(created.id, 1u64); + + // Check that the deposit info for this proposal looks right. + let (_, pre_propose) = query_deposit_config_and_pre_propose_module( + &app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + ); + let deposit_response = query_pre_proposal_single_deposit_info( + &app, + &pre_propose.address, + pre_propose.code_hash, + id, + ); + + assert_eq!(deposit_response.proposer, Addr::unchecked(CREATOR_ADDR)); + assert_eq!(deposit_response.deposit_info, None,); +} + +#[test] +fn test_propose_supports_stargate_messages() { + // If we can make a proposal with a stargate message, we support + // stargate messages in proposals. + setup_test(vec![CosmosMsg::Stargate { + type_url: "foo_type".to_string(), + value: Binary::default(), + }]); +} + +/// Test that the deposit token is properly set to the voting module +/// token during instantiation. +#[test] +fn test_voting_module_token_instantiate() { + let CommonTest { + app, + core_contract_info: _, + proposal_module, + gov_token_info, + proposal_id, + query_auth_info: _, + } = setup_test(vec![]); + + let (_, pre_propose) = query_deposit_config_and_pre_propose_module( + &app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + ); + let deposit_response = query_pre_proposal_single_deposit_info( + &app, + &pre_propose.address, + pre_propose.code_hash, + proposal_id, + ); + + let deposit_token = if let Some(CheckedDepositInfo { + denom: CheckedDenom::Snip20(addr, ..), + .. + }) = deposit_response.deposit_info + { + addr + } else { + panic!("voting module should have governance token") + }; + assert_eq!(deposit_token, gov_token_info.addr.clone()) +} + +#[test] +#[should_panic( + expected = "Error parsing into type dao_voting_cw4::msg::QueryMsg: unknown variant `token_contract`" +)] +fn test_deposit_token_voting_module_token_fails_if_no_voting_module_token() { + let mut app = App::default(); + let instantiate = get_default_token_dao_proposal_module_instantiate(&mut app); + instantiate_with_cw4_groups_governance(&mut app, instantiate, None); +} + +#[test] +fn test_instantiate_with_non_voting_module_cw20_deposit() { + let mut app = App::default(); + let alt_snip20 = instantiate_sni20_base_default(&mut app); + + let mut instantiate = get_default_non_token_dao_proposal_module_instantiate(&mut app); + // hehehehehehehehe + instantiate.pre_propose_info = get_pre_propose_info( + &mut app, + Some(UncheckedDepositInfo { + denom: dao_voting::deposit::DepositToken::Token { + denom: cw_denom::UncheckedDenom::Snip20( + alt_snip20.address.clone().to_string(), + alt_snip20.code_hash.clone(), + ), + }, + amount: Uint128::new(10_000_000), + refund_policy: dao_voting::deposit::DepositRefundPolicy::OnlyPassed, + }), + false, + ); + + let core_contract_info = instantiate_with_cw4_groups_governance(&mut app, instantiate, None); + let proposal_module = query_single_proposal_module( + &app, + &core_contract_info.address.clone(), + core_contract_info.code_hash.clone(), + ); + + let query_auth_info: AnyContractInfo = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::QueryAuthInfo {}, + ) + .unwrap(); + + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + let proposal_id = make_proposal( + &mut app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + vec![], + ); + + let created = query_proposal( + &app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + proposal_id, + ); + let current_block = app.block_info(); + + // These values just come from the default instantiate message + // values. + let expected = SingleChoiceProposal { + title: "title".to_string(), + description: "description".to_string(), + proposer: Addr::unchecked(CREATOR_ADDR), + start_height: current_block.height, + expiration: Duration::Time(604800).after(¤t_block), + min_voting_period: None, + threshold: Threshold::ThresholdQuorum { + threshold: PercentageThreshold::Percent(Decimal::percent(15)), + quorum: PercentageThreshold::Majority {}, + }, + allow_revoting: false, + total_power: Uint128::new(1), + msgs: vec![], + status: Status::Open, + votes: Votes::zero(), + veto: None, + }; + + assert_eq!(created.proposal, expected); + assert_eq!(created.id, 1u64); + + // Check that the deposit info for this proposal looks right. + let (_, pre_propose) = query_deposit_config_and_pre_propose_module( + &app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + ); + let deposit_response = query_pre_proposal_single_deposit_info( + &app, + &pre_propose.address, + pre_propose.code_hash, + proposal_id, + ); + + assert_eq!(deposit_response.proposer, Addr::unchecked(CREATOR_ADDR)); + assert_eq!( + deposit_response.deposit_info, + Some(CheckedDepositInfo { + denom: cw_denom::CheckedDenom::Snip20( + alt_snip20.address.clone(), + alt_snip20.code_hash.clone() + ), + amount: Uint128::new(10_000_000), + refund_policy: dao_voting::deposit::DepositRefundPolicy::OnlyPassed + }) + ); +} + +#[test] +fn test_proposal_message_execution() { + 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 core_contract_info = + instantiate_with_staked_balances_governance(&mut app, instantiate, None); + let proposal_module = query_single_proposal_module( + &app, + &core_contract_info.address.clone(), + core_contract_info.code_hash.clone(), + ); + let gov_token_info = query_dao_token( + &app, + &core_contract_info.address.clone(), + core_contract_info.code_hash.clone(), + ); + + let query_auth_info: AnyContractInfo = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::QueryAuthInfo {}, + ) + .unwrap(); + + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + + let viewing_key_token = create_snip20_viewing_key( + &mut app, + ContractInfo { + address: gov_token_info.addr.clone(), + code_hash: gov_token_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + + mint_snip20s( + &mut app, + &gov_token_info.addr.clone(), + gov_token_info.code_hash.clone(), + &core_contract_info.address.clone(), + CREATOR_ADDR, + 10_000_000, + ); + let proposal_id = make_proposal( + &mut app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + vec![ + WasmMsg::Execute { + contract_addr: gov_token_info.addr.clone().to_string(), + code_hash: gov_token_info.code_hash.clone(), + msg: to_binary(&snip20_reference_impl::msg::ExecuteMsg::Mint { + recipient: CREATOR_ADDR.to_string(), + amount: Uint128::new(10_000_000), + memo: None, + decoys: None, + entropy: None, + padding: None, + }) + .unwrap(), + funds: vec![], + } + .into(), + BankMsg::Send { + to_address: CREATOR_ADDR.to_string(), + amount: coins(10, "ujuno"), + } + .into(), + ], + ); + let snip20_balance = query_balance_cw20( + &app, + &gov_token_info.addr.clone(), + gov_token_info.code_hash.clone(), + CREATOR_ADDR, + viewing_key_token.clone(), + ); + let native_balance = query_balance_native(&app, CREATOR_ADDR, "ujuno"); + assert_eq!(snip20_balance, Uint128::zero()); + assert_eq!(native_balance, Uint128::zero()); + + vote_on_proposal( + &mut app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + CREATOR_ADDR, + proposal_id, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + Vote::Yes, + ); + let proposal = query_proposal( + &app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + proposal_id, + ); + assert_eq!(proposal.proposal.status, Status::Passed); + + // Can't use library function because we expect this to fail due + // to insufficent balance in the bank module. + app.execute_contract( + Addr::unchecked(CREATOR_ADDR), + &ContractInfo { + address: proposal_module.addr.clone(), + code_hash: proposal_module.code_hash.clone(), + }, + &ExecuteMsg::Execute { + auth: shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + proposal_id, + }, + &[], + ) + .unwrap_err(); + let proposal = query_proposal( + &app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + proposal_id, + ); + assert_eq!(proposal.proposal.status, Status::Passed); + + mint_natives( + &mut app, + core_contract_info.address.clone().as_str(), + coins(10, "ujuno"), + ); + execute_proposal( + &mut app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + proposal_id, + ); + let proposal = query_proposal( + &app, + &proposal_module.addr.clone(), + proposal_module.code_hash.clone(), + proposal_id, + ); + assert_eq!(proposal.proposal.status, Status::Executed); + + let snip20_balance = query_balance_cw20( + &app, + &gov_token_info.addr.clone(), + gov_token_info.code_hash.clone(), + CREATOR_ADDR, + viewing_key_token, + ); + let native_balance = query_balance_native(&app, CREATOR_ADDR, "ujuno"); + assert_eq!(snip20_balance, Uint128::new(20_000_000)); + assert_eq!(native_balance, Uint128::new(10)); + + // Sneak in a check here that proposals can't be executed more + // than once in the on close on execute config suituation. + let err = execute_proposal_should_fail( + &mut app, + &proposal_module.addr, + proposal_module.code_hash, + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + proposal_id, + ); + assert!(matches!(err, ContractError::NotPassed {})) } -fn setup_test(sender: &str) -> CommonTest { + +#[test] +fn test_proposal_message_timelock_execution() -> anyhow::Result<()> { let mut app = App::default(); - let proposal_module_contract_info = app.store_code(proposal_single_contract()); - let query_auth = instantiate_query_auth(&mut app); - let instantiate = get_default_token_dao_proposal_module_instantiate( - RawContract { - address: query_auth.clone().address.to_string(), - code_hash: query_auth.clone().code_hash, + let mut instantiate = get_default_token_dao_proposal_module_instantiate(&mut app); + let veto_config = VetoConfig { + timelock_duration: Duration::Time(100), + vetoer: "oversight".to_string(), + early_execute: false, + veto_before_passed: false, + }; + instantiate.close_proposal_on_execution_failure = false; + instantiate.veto = Some(veto_config.clone()); + let core_contract_info = instantiate_with_staked_balances_governance( + &mut app, + instantiate, + Some(vec![ + InitialBalance { + address: "oversight".to_string(), + amount: Uint128::new(15), + }, + InitialBalance { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(85), + }, + ]), + ); + let proposal_module = query_single_proposal_module( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + let gov_token_info = query_dao_token( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + + let query_auth_info: AnyContractInfo = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::QueryAuthInfo {}, + ) + .unwrap(); + + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + + let viewing_key_oversight = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info("oversight", &[]), + ); + + let viewing_key_token = create_snip20_viewing_key( + &mut app, + ContractInfo { + address: gov_token_info.addr.clone(), + code_hash: gov_token_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + + mint_snip20s( + &mut app, + &gov_token_info.addr, + gov_token_info.code_hash.clone(), + &core_contract_info.address, + CREATOR_ADDR, + 10_000_000, + ); + let proposal_id = make_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + vec![ + WasmMsg::Execute { + contract_addr: gov_token_info.addr.clone().to_string(), + code_hash: gov_token_info.code_hash.clone(), + msg: to_binary(&snip20_reference_impl::msg::ExecuteMsg::Mint { + recipient: CREATOR_ADDR.to_string(), + amount: Uint128::new(10_000_000), + memo: None, + decoys: None, + entropy: None, + padding: None, + }) + .unwrap(), + funds: vec![], + } + .into(), + BankMsg::Send { + to_address: CREATOR_ADDR.to_string(), + amount: coins(10, "ujuno"), + } + .into(), + ], + ); + let snip20_balance = query_balance_cw20( + &app, + &gov_token_info.addr, + gov_token_info.code_hash.clone(), + CREATOR_ADDR, + viewing_key_token.clone(), + ); + let native_balance = query_balance_native(&app, CREATOR_ADDR, "ujuno"); + assert_eq!(snip20_balance, Uint128::zero()); + assert_eq!(native_balance, Uint128::zero()); + + vote_on_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + proposal_id, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), }, - "dao_dao_code_hash".to_string(), + Vote::Yes, + ); + let proposal = query_proposal( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + proposal_id, + ); + + // Proposal is timelocked to the moment of prop expiring + timelock delay + assert_eq!( + proposal.proposal.status, + Status::VetoTimelock { + expiration: proposal + .proposal + .expiration + .add(veto_config.timelock_duration)?, + } + ); + + mint_natives( + &mut app, + core_contract_info.address.clone().as_str(), + coins(10, "ujuno"), ); - let proposal_single_contract_info = app - .instantiate_contract( - proposal_module_contract_info, - Addr::unchecked(sender), - &instantiate, + + // vetoer can't execute when timelock is active and + // early execute not enabled. + let err: ContractError = app + .execute_contract( + Addr::unchecked("oversight"), + &ContractInfo { + address: proposal_module.addr.clone(), + code_hash: proposal_module.code_hash.clone(), + }, + &ExecuteMsg::Execute { + auth: shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key_oversight.clone(), + address: "oversight".into(), + }, + proposal_id, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, ContractError::VetoError(VetoError::NoEarlyExecute {})); + + // Proposal cannot be excuted before timelock expires + let err: ContractError = app + .execute_contract( + Addr::unchecked(CREATOR_ADDR), + &ContractInfo { + address: proposal_module.addr.clone(), + code_hash: proposal_module.code_hash.clone(), + }, + &ExecuteMsg::Execute { + auth: shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + proposal_id, + }, &[], - "proposal_single", - None, + ) + .unwrap_err() + .downcast() + .unwrap(); + + assert_eq!(err, ContractError::VetoError(VetoError::Timelocked {})); + + // Time passes + app.update_block(|block| { + block.time = block.time.plus_seconds(604800 + 200); + }); + + // Proposal executes successfully + execute_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + proposal_id, + ); + let proposal = query_proposal( + &app, + &proposal_module.addr, + proposal_module.code_hash, + proposal_id, + ); + assert_eq!(proposal.proposal.status, Status::Executed); + + Ok(()) +} + +// 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 veto_config = VetoConfig { + timelock_duration: Duration::Time(100), + vetoer: "oversight".to_string(), + early_execute: false, + veto_before_passed: true, + }; + instantiate.veto = Some(veto_config.clone()); + let core_contract_info = instantiate_with_staked_balances_governance( + &mut app, + instantiate, + Some(vec![InitialBalance { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(85), + }]), + ); + let proposal_module = query_single_proposal_module( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + let gov_token_info = query_dao_token( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + + let query_auth_info: AnyContractInfo = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::QueryAuthInfo {}, + ) + .unwrap(); + + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + + mint_snip20s( + &mut app, + &gov_token_info.addr, + gov_token_info.code_hash.clone(), + &core_contract_info.address, + CREATOR_ADDR, + 10_000_000, + ); + let proposal_id = make_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key, + address: CREATOR_ADDR.into(), + }, + vec![ + WasmMsg::Execute { + contract_addr: gov_token_info.addr.clone().to_string(), + code_hash: gov_token_info.code_hash.clone(), + msg: to_binary(&snip20_reference_impl::msg::ExecuteMsg::Mint { + recipient: CREATOR_ADDR.to_string(), + amount: Uint128::new(10_000_000), + memo: None, + decoys: None, + entropy: None, + padding: None, + }) + .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"), + &ContractInfo { + address: proposal_module.addr, + code_hash: proposal_module.code_hash, + }, + &ExecuteMsg::Veto { proposal_id }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, ContractError::VetoError(VetoError::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 veto_config = VetoConfig { + timelock_duration: Duration::Time(100), + vetoer: "oversight".to_string(), + early_execute: false, + veto_before_passed: false, + }; + instantiate.veto = Some(veto_config.clone()); + let core_contract_info = instantiate_with_staked_balances_governance( + &mut app, + instantiate, + Some(vec![InitialBalance { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(85), + }]), + ); + let proposal_module = query_single_proposal_module( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + let gov_token_info = query_dao_token( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + + let query_auth_info: AnyContractInfo = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::QueryAuthInfo {}, + ) + .unwrap(); + + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + + mint_snip20s( + &mut app, + &gov_token_info.addr, + gov_token_info.code_hash.clone(), + &core_contract_info.address, + CREATOR_ADDR, + 10_000_000, + ); + let proposal_id = make_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + vec![ + WasmMsg::Execute { + contract_addr: gov_token_info.addr.clone().to_string(), + code_hash: gov_token_info.code_hash.clone(), + msg: to_binary(&snip20_reference_impl::msg::ExecuteMsg::Mint { + recipient: CREATOR_ADDR.to_string(), + amount: Uint128::new(10_000_000), + memo: None, + decoys: None, + entropy: None, + padding: None, + }) + .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"), + &ContractInfo { + address: proposal_module.addr, + code_hash: proposal_module.code_hash, + }, + &ExecuteMsg::Veto { proposal_id }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + ContractError::VetoError(VetoError::NoVetoBeforePassed {}) + ); +} + +#[test] +fn test_open_proposal_veto_with_no_timelock() { + 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.veto = None; + let core_contract_info = instantiate_with_staked_balances_governance( + &mut app, + instantiate, + Some(vec![InitialBalance { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(85), + }]), + ); + let proposal_module = query_single_proposal_module( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + let gov_token_info = query_dao_token( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + + let query_auth_info: AnyContractInfo = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::QueryAuthInfo {}, + ) + .unwrap(); + + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + + mint_snip20s( + &mut app, + &gov_token_info.addr, + gov_token_info.code_hash.clone(), + &core_contract_info.address, + CREATOR_ADDR, + 10_000_000, + ); + let proposal_id = make_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + vec![ + WasmMsg::Execute { + contract_addr: gov_token_info.addr.clone().to_string(), + code_hash: gov_token_info.code_hash.clone(), + msg: to_binary(&snip20_reference_impl::msg::ExecuteMsg::Mint { + recipient: CREATOR_ADDR.to_string(), + amount: Uint128::new(10_000_000), + memo: None, + decoys: None, + entropy: None, + padding: None, + }) + .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"), + &ContractInfo { + address: proposal_module.addr, + code_hash: proposal_module.code_hash, + }, + &ExecuteMsg::Veto { proposal_id }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + ContractError::VetoError(VetoError::NoVetoConfiguration {}) + ); +} + +// if proposal is not open or timelocked, attempts to veto should +// throw an error +#[test] +fn test_vetoed_proposal_veto() { + 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 veto_config = VetoConfig { + timelock_duration: Duration::Time(100), + vetoer: "oversight".to_string(), + early_execute: false, + veto_before_passed: true, + }; + instantiate.veto = Some(veto_config.clone()); + let core_contract_info = instantiate_with_staked_balances_governance( + &mut app, + instantiate, + Some(vec![InitialBalance { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(85), + }]), + ); + let proposal_module = query_single_proposal_module( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + let gov_token_info = query_dao_token( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + + let query_auth_info: AnyContractInfo = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::QueryAuthInfo {}, + ) + .unwrap(); + + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + + mint_snip20s( + &mut app, + &gov_token_info.addr, + gov_token_info.code_hash.clone(), + &core_contract_info.address, + CREATOR_ADDR, + 10_000_000, + ); + let proposal_id = make_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + vec![ + WasmMsg::Execute { + contract_addr: gov_token_info.addr.clone().to_string(), + code_hash: gov_token_info.code_hash.clone(), + msg: to_binary(&snip20_reference_impl::msg::ExecuteMsg::Mint { + recipient: CREATOR_ADDR.to_string(), + amount: Uint128::new(10_000_000), + memo: None, + decoys: None, + entropy: None, + padding: None, + }) + .unwrap(), + funds: vec![], + } + .into(), + BankMsg::Send { + to_address: CREATOR_ADDR.to_string(), + amount: coins(10, "ujuno"), + } + .into(), + ], + ); + + app.execute_contract( + Addr::unchecked("oversight"), + &ContractInfo { + address: proposal_module.addr.clone(), + code_hash: proposal_module.code_hash.clone(), + }, + &ExecuteMsg::Veto { proposal_id }, + &[], + ) + .unwrap(); + + let proposal = query_proposal( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + proposal_id, + ); + assert_eq!(proposal.proposal.status, Status::Vetoed {}); + + let err: ContractError = app + .execute_contract( + Addr::unchecked("oversight"), + &ContractInfo { + address: proposal_module.addr, + code_hash: proposal_module.code_hash, + }, + &ExecuteMsg::Veto { proposal_id }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + + assert_eq!( + ContractError::VetoError(VetoError::InvalidProposalStatus { + status: "vetoed".to_string() + }), + err, + ); +} + +#[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 veto_config = VetoConfig { + timelock_duration: Duration::Time(100), + vetoer: "oversight".to_string(), + early_execute: false, + veto_before_passed: true, + }; + instantiate.veto = Some(veto_config.clone()); + let core_contract_info = instantiate_with_staked_balances_governance( + &mut app, + instantiate, + Some(vec![InitialBalance { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(85), + }]), + ); + let proposal_module = query_single_proposal_module( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + let gov_token_info = query_dao_token( + &app, + &core_contract_info.address.clone(), + core_contract_info.code_hash.clone(), + ); + + let query_auth_info: AnyContractInfo = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::QueryAuthInfo {}, + ) + .unwrap(); + + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + + mint_snip20s( + &mut app, + &gov_token_info.addr, + gov_token_info.code_hash.clone(), + &core_contract_info.address, + CREATOR_ADDR, + 10_000_000, + ); + let proposal_id = make_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + vec![ + WasmMsg::Execute { + contract_addr: gov_token_info.addr.clone().to_string(), + code_hash: gov_token_info.code_hash.clone(), + msg: to_binary(&snip20_reference_impl::msg::ExecuteMsg::Mint { + recipient: CREATOR_ADDR.to_string(), + amount: Uint128::new(10_000_000), + memo: None, + decoys: None, + entropy: None, + padding: None, + }) + .unwrap(), + funds: vec![], + } + .into(), + BankMsg::Send { + to_address: CREATOR_ADDR.to_string(), + amount: coins(10, "ujuno"), + } + .into(), + ], + ); + + app.execute_contract( + Addr::unchecked("oversight"), + &ContractInfo { + address: proposal_module.addr.clone(), + code_hash: proposal_module.code_hash.clone(), + }, + &ExecuteMsg::Veto { proposal_id }, + &[], + ) + .unwrap(); + + let proposal = query_proposal( + &app, + &proposal_module.addr, + proposal_module.code_hash, + proposal_id, + ); + assert_eq!(proposal.proposal.status, Status::Vetoed {}); +} + +// only the vetoer can veto during timelock period +#[test] +fn test_timelocked_proposal_veto_unauthorized() -> anyhow::Result<()> { + 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 veto_config = VetoConfig { + timelock_duration: Duration::Time(100), + vetoer: "oversight".to_string(), + early_execute: true, + veto_before_passed: false, + }; + instantiate.veto = Some(veto_config.clone()); + let core_contract_info = instantiate_with_staked_balances_governance( + &mut app, + instantiate, + Some(vec![ + InitialBalance { + address: "oversight".to_string(), + amount: Uint128::new(15), + }, + InitialBalance { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(85), + }, + ]), + ); + let proposal_module = query_single_proposal_module( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + let gov_token_info = query_dao_token( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + + let query_auth_info: AnyContractInfo = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::QueryAuthInfo {}, + ) + .unwrap(); + + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + + mint_snip20s( + &mut app, + &gov_token_info.addr, + gov_token_info.code_hash.clone(), + &core_contract_info.address, + CREATOR_ADDR, + 10_000_000, + ); + let proposal_id = make_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + vec![ + WasmMsg::Execute { + contract_addr: gov_token_info.addr.clone().to_string(), + code_hash: gov_token_info.code_hash.clone(), + msg: to_binary(&snip20_reference_impl::msg::ExecuteMsg::Mint { + recipient: CREATOR_ADDR.to_string(), + amount: Uint128::new(10_000_000), + memo: None, + decoys: None, + entropy: None, + padding: None, + }) + .unwrap(), + funds: vec![], + } + .into(), + BankMsg::Send { + to_address: CREATOR_ADDR.to_string(), + amount: coins(10, "ujuno"), + } + .into(), + ], + ); + + vote_on_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + proposal_id, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + Vote::Yes, + ); + let proposal = query_proposal( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + proposal_id, + ); + + // Proposal is timelocked to the moment of prop expiring + timelock delay + assert_eq!( + proposal.proposal.status, + Status::VetoTimelock { + expiration: proposal + .proposal + .expiration + .add(veto_config.timelock_duration)?, + } + ); + + let err: ContractError = app + .execute_contract( + Addr::unchecked("not-oversight"), + &ContractInfo { + address: proposal_module.addr.clone(), + code_hash: proposal_module.code_hash.clone(), + }, + &ExecuteMsg::Veto { proposal_id }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + + assert_eq!(err, ContractError::VetoError(VetoError::Unauthorized {}),); + let proposal = query_proposal( + &app, + &proposal_module.addr, + proposal_module.code_hash, + proposal_id, + ); + assert_eq!( + proposal.proposal.status, + Status::VetoTimelock { + expiration: proposal + .proposal + .expiration + .add(veto_config.timelock_duration)?, + } + ); + + Ok(()) +} + +// vetoer can only veto the proposal before the timelock expires +#[test] +fn test_timelocked_proposal_veto_expired_timelock() -> anyhow::Result<()> { + 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 veto_config = VetoConfig { + timelock_duration: Duration::Time(100), + vetoer: "oversight".to_string(), + early_execute: true, + veto_before_passed: false, + }; + instantiate.veto = Some(veto_config.clone()); + let core_contract_info = instantiate_with_staked_balances_governance( + &mut app, + instantiate, + Some(vec![ + InitialBalance { + address: "oversight".to_string(), + amount: Uint128::new(15), + }, + InitialBalance { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(85), + }, + ]), + ); + let proposal_module = query_single_proposal_module( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + let gov_token_info = query_dao_token( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + + let query_auth_info: AnyContractInfo = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::QueryAuthInfo {}, + ) + .unwrap(); + + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + + mint_snip20s( + &mut app, + &gov_token_info.addr, + gov_token_info.code_hash.clone(), + &core_contract_info.address, + CREATOR_ADDR, + 10_000_000, + ); + let proposal_id = make_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + vec![ + WasmMsg::Execute { + contract_addr: gov_token_info.addr.clone().to_string(), + code_hash: gov_token_info.code_hash.clone(), + msg: to_binary(&snip20_reference_impl::msg::ExecuteMsg::Mint { + recipient: CREATOR_ADDR.to_string(), + amount: Uint128::new(10_000_000), + memo: None, + decoys: None, + entropy: None, + padding: None, + }) + .unwrap(), + funds: vec![], + } + .into(), + BankMsg::Send { + to_address: CREATOR_ADDR.to_string(), + amount: coins(10, "ujuno"), + } + .into(), + ], + ); + + vote_on_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + proposal_id, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + Vote::Yes, + ); + let proposal = query_proposal( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + proposal_id, + ); + + // Proposal is timelocked to the moment of prop expiring + timelock delay + assert_eq!( + proposal.proposal.status, + Status::VetoTimelock { + expiration: proposal + .proposal + .expiration + .add(veto_config.timelock_duration)?, + } + ); + app.update_block(|b| b.time = b.time.plus_seconds(604800 + 200)); + + let err: ContractError = app + .execute_contract( + Addr::unchecked("oversight"), + &ContractInfo { + address: proposal_module.addr.clone(), + code_hash: proposal_module.code_hash.clone(), + }, + &ExecuteMsg::Veto { proposal_id }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + + assert_eq!(err, ContractError::VetoError(VetoError::TimelockExpired {}),); + + Ok(()) +} + +// vetoer can only exec timelocked prop if the early exec flag is enabled +#[test] +fn test_timelocked_proposal_execute_no_early_exec() -> anyhow::Result<()> { + 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 veto_config = VetoConfig { + timelock_duration: Duration::Time(100), + vetoer: "oversight".to_string(), + early_execute: false, + veto_before_passed: false, + }; + instantiate.veto = Some(veto_config.clone()); + let core_contract_info = instantiate_with_staked_balances_governance( + &mut app, + instantiate, + Some(vec![InitialBalance { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(85), + }]), + ); + let proposal_module = query_single_proposal_module( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + let gov_token_info = query_dao_token( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + + let query_auth_info: AnyContractInfo = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::QueryAuthInfo {}, + ) + .unwrap(); + + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + + mint_snip20s( + &mut app, + &gov_token_info.addr, + gov_token_info.code_hash.clone(), + &core_contract_info.address, + CREATOR_ADDR, + 10_000_000, + ); + let proposal_id = make_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + vec![ + WasmMsg::Execute { + contract_addr: gov_token_info.addr.clone().to_string(), + code_hash: gov_token_info.code_hash.clone(), + msg: to_binary(&snip20_reference_impl::msg::ExecuteMsg::Mint { + recipient: CREATOR_ADDR.to_string(), + amount: Uint128::new(10_000_000), + memo: None, + decoys: None, + entropy: None, + padding: None, + }) + .unwrap(), + funds: vec![], + } + .into(), + BankMsg::Send { + to_address: CREATOR_ADDR.to_string(), + amount: coins(10, "ujuno"), + } + .into(), + ], + ); + + vote_on_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + proposal_id, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + Vote::Yes, + ); + let proposal = query_proposal( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + proposal_id, + ); + + // Proposal is timelocked to the moment of prop expiring + timelock delay + assert_eq!( + proposal.proposal.status, + Status::VetoTimelock { + expiration: proposal + .proposal + .expiration + .add(veto_config.timelock_duration)?, + } + ); + + let err: ContractError = app + .execute_contract( + Addr::unchecked("oversight"), + &ContractInfo { + address: proposal_module.addr.clone(), + code_hash: proposal_module.code_hash.clone(), + }, + &ExecuteMsg::Execute { + auth: shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + proposal_id, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + + assert_eq!(err, ContractError::VetoError(VetoError::NoEarlyExecute {}),); + + Ok(()) +} + +#[test] +fn test_timelocked_proposal_execute_early() -> anyhow::Result<()> { + 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 veto_config = VetoConfig { + timelock_duration: Duration::Time(100), + vetoer: "oversight".to_string(), + early_execute: true, + veto_before_passed: false, + }; + instantiate.veto = Some(veto_config.clone()); + let core_contract_info = instantiate_with_staked_balances_governance( + &mut app, + instantiate, + Some(vec![InitialBalance { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(85), + }]), + ); + let proposal_module = query_single_proposal_module( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + let gov_token_info = query_dao_token( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + + let query_auth_info: AnyContractInfo = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::QueryAuthInfo {}, + ) + .unwrap(); + + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + + mint_snip20s( + &mut app, + &gov_token_info.addr, + gov_token_info.code_hash.clone(), + &core_contract_info.address, + CREATOR_ADDR, + 10_000_000, + ); + let proposal_id = make_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + vec![ + WasmMsg::Execute { + contract_addr: gov_token_info.addr.clone().to_string(), + code_hash: gov_token_info.code_hash.clone(), + msg: to_binary(&snip20_reference_impl::msg::ExecuteMsg::Mint { + recipient: CREATOR_ADDR.to_string(), + amount: Uint128::new(10_000_000), + memo: None, + decoys: None, + entropy: None, + padding: None, + }) + .unwrap(), + funds: vec![], + } + .into(), + BankMsg::Send { + to_address: CREATOR_ADDR.to_string(), + amount: coins(10, "ujuno"), + } + .into(), + ], + ); + + vote_on_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + proposal_id, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + Vote::Yes, + ); + let proposal = query_proposal( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + proposal_id, + ); + // Proposal is timelocked to the moment of prop expiring + timelock delay + assert_eq!( + proposal.proposal.status, + Status::VetoTimelock { + expiration: proposal + .proposal + .expiration + .add(veto_config.timelock_duration)?, + } + ); + + // assert timelock is active + assert!(!veto_config + .timelock_duration + .after(&app.block_info()) + .is_expired(&app.block_info())); + mint_natives( + &mut app, + core_contract_info.address.clone().as_str(), + coins(10, "ujuno"), + ); + + app.execute_contract( + Addr::unchecked("oversight"), + &ContractInfo { + address: proposal_module.addr.clone(), + code_hash: proposal_module.code_hash.clone(), + }, + &ExecuteMsg::Execute { + auth: shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key, + address: CREATOR_ADDR.into(), + }, + proposal_id, + }, + &[], + ) + .unwrap(); + + let proposal = query_proposal( + &app, + &proposal_module.addr, + proposal_module.code_hash, + proposal_id, + ); + assert_eq!(proposal.proposal.status, Status::Executed {}); + + Ok(()) +} + +// only vetoer can exec timelocked prop early +#[test] +fn test_timelocked_proposal_execute_active_timelock_unauthorized() -> anyhow::Result<()> { + 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 veto_config = VetoConfig { + timelock_duration: Duration::Time(100), + vetoer: "oversight".to_string(), + early_execute: true, + veto_before_passed: false, + }; + instantiate.veto = Some(veto_config.clone()); + let core_contract_info = instantiate_with_staked_balances_governance( + &mut app, + instantiate, + Some(vec![InitialBalance { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(85), + }]), + ); + let proposal_module = query_single_proposal_module( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + let gov_token_info = query_dao_token( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + + let query_auth_info: AnyContractInfo = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::QueryAuthInfo {}, + ) + .unwrap(); + + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + + mint_snip20s( + &mut app, + &gov_token_info.addr, + gov_token_info.code_hash.clone(), + &core_contract_info.address, + CREATOR_ADDR, + 10_000_000, + ); + let proposal_id = make_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + vec![ + WasmMsg::Execute { + contract_addr: gov_token_info.addr.clone().to_string(), + code_hash: gov_token_info.code_hash.clone(), + msg: to_binary(&snip20_reference_impl::msg::ExecuteMsg::Mint { + recipient: CREATOR_ADDR.to_string(), + amount: Uint128::new(10_000_000), + memo: None, + decoys: None, + entropy: None, + padding: None, + }) + .unwrap(), + funds: vec![], + } + .into(), + BankMsg::Send { + to_address: CREATOR_ADDR.to_string(), + amount: coins(10, "ujuno"), + } + .into(), + ], + ); + + vote_on_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + proposal_id, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + Vote::Yes, + ); + let proposal = query_proposal( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + proposal_id, + ); + // Proposal is timelocked to the moment of prop expiring + timelock delay + assert_eq!( + proposal.proposal.status, + Status::VetoTimelock { + expiration: proposal + .proposal + .expiration + .add(veto_config.timelock_duration)?, + } + ); + + // assert timelock is active + assert!(!veto_config + .timelock_duration + .after(&app.block_info()) + .is_expired(&app.block_info())); + + let err: ContractError = app + .execute_contract( + Addr::unchecked(CREATOR_ADDR), + &ContractInfo { + address: proposal_module.addr.clone(), + code_hash: proposal_module.code_hash.clone(), + }, + &ExecuteMsg::Execute { + auth: shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key, + address: CREATOR_ADDR.into(), + }, + proposal_id, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + + assert_eq!(err, ContractError::VetoError(VetoError::Timelocked {}),); + + Ok(()) +} + +// anyone can exec the prop after the timelock expires +#[test] +fn test_timelocked_proposal_execute_expired_timelock_not_vetoer() -> anyhow::Result<()> { + 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 veto_config = VetoConfig { + timelock_duration: Duration::Time(100), + vetoer: "oversight".to_string(), + early_execute: true, + veto_before_passed: false, + }; + instantiate.veto = Some(veto_config.clone()); + let core_contract_info = instantiate_with_staked_balances_governance( + &mut app, + instantiate, + Some(vec![InitialBalance { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(85), + }]), + ); + let proposal_module = query_single_proposal_module( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + let gov_token_info = query_dao_token( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + + let query_auth_info: AnyContractInfo = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::QueryAuthInfo {}, + ) + .unwrap(); + + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + + mint_snip20s( + &mut app, + &gov_token_info.addr, + gov_token_info.code_hash.clone(), + &core_contract_info.address, + CREATOR_ADDR, + 10_000_000, + ); + let proposal_id = make_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + vec![ + WasmMsg::Execute { + contract_addr: gov_token_info.addr.clone().to_string(), + code_hash: gov_token_info.code_hash.clone(), + msg: to_binary(&snip20_reference_impl::msg::ExecuteMsg::Mint { + recipient: CREATOR_ADDR.to_string(), + amount: Uint128::new(10_000_000), + memo: None, + decoys: None, + entropy: None, + padding: None, + }) + .unwrap(), + funds: vec![], + } + .into(), + BankMsg::Send { + to_address: CREATOR_ADDR.to_string(), + amount: coins(10, "ujuno"), + } + .into(), + ], + ); + + vote_on_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + proposal_id, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + Vote::Yes, + ); + let proposal = query_proposal( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + proposal_id, + ); + + // Proposal is timelocked to the moment of prop expiring + timelock delay + let expiration = proposal + .proposal + .expiration + .add(veto_config.timelock_duration)?; + assert_eq!( + proposal.proposal.status, + Status::VetoTimelock { expiration } + ); + + app.update_block(|b| b.time = b.time.plus_seconds(604800 + 201)); + // assert timelock is expired + assert!(expiration.is_expired(&app.block_info())); + mint_natives( + &mut app, + core_contract_info.address.clone().as_str(), + coins(10, "ujuno"), + ); + + app.execute_contract( + Addr::unchecked(CREATOR_ADDR), + &ContractInfo { + address: proposal_module.addr.clone(), + code_hash: proposal_module.code_hash.clone(), + }, + &ExecuteMsg::Execute { + auth: shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key, + address: CREATOR_ADDR.into(), + }, + proposal_id, + }, + &[], + ) + .unwrap(); + + let proposal = query_proposal( + &app, + &proposal_module.addr, + proposal_module.code_hash, + proposal_id, + ); + assert_eq!(proposal.proposal.status, Status::Executed {},); + + Ok(()) +} + +#[test] +fn test_proposal_message_timelock_veto() -> anyhow::Result<()> { + 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 veto_config = VetoConfig { + timelock_duration: Duration::Time(100), + vetoer: "oversight".to_string(), + early_execute: false, + veto_before_passed: false, + }; + instantiate.veto = Some(veto_config.clone()); + let core_contract_info = instantiate_with_staked_balances_governance( + &mut app, + instantiate, + Some(vec![InitialBalance { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(85), + }]), + ); + let proposal_module = query_single_proposal_module( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + let gov_token_info = query_dao_token( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + + let query_auth_info: AnyContractInfo = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::QueryAuthInfo {}, + ) + .unwrap(); + + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + + let viewing_key_token = create_snip20_viewing_key( + &mut app, + ContractInfo { + address: gov_token_info.addr.clone(), + code_hash: gov_token_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + + mint_snip20s( + &mut app, + &gov_token_info.addr, + gov_token_info.code_hash.clone(), + &core_contract_info.address, + CREATOR_ADDR, + 10_000_000, + ); + let proposal_id = make_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + vec![ + WasmMsg::Execute { + contract_addr: gov_token_info.addr.clone().to_string(), + code_hash: gov_token_info.code_hash.clone(), + msg: to_binary(&snip20_reference_impl::msg::ExecuteMsg::Mint { + recipient: CREATOR_ADDR.to_string(), + amount: Uint128::new(10_000_000), + memo: None, + decoys: None, + entropy: None, + padding: None, + }) + .unwrap(), + funds: vec![], + } + .into(), + BankMsg::Send { + to_address: CREATOR_ADDR.to_string(), + amount: coins(10, "ujuno"), + } + .into(), + ], + ); + let snip20_balance = query_balance_cw20( + &app, + &gov_token_info.addr, + gov_token_info.code_hash.clone(), + CREATOR_ADDR, + viewing_key_token.clone(), + ); + let native_balance = query_balance_native(&app, CREATOR_ADDR, "ujuno"); + assert_eq!(snip20_balance, Uint128::zero()); + assert_eq!(native_balance, Uint128::zero()); + + // Vetoer can't veto early + let err: ContractError = app + .execute_contract( + Addr::unchecked("oversight"), + &ContractInfo { + address: proposal_module.addr.clone(), + code_hash: proposal_module.code_hash.clone(), + }, + &ExecuteMsg::Veto { proposal_id }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + ContractError::VetoError(VetoError::NoVetoBeforePassed {}) + ); + + // Vote on proposal to pass it + vote_on_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + proposal_id, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + Vote::Yes, + ); + let proposal = query_proposal( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + proposal_id, + ); + // Proposal is timelocked to the moment of prop expiring + timelock delay + assert_eq!( + proposal.proposal.status, + Status::VetoTimelock { + expiration: proposal + .proposal + .expiration + .add(veto_config.timelock_duration)?, + } + ); + + mint_natives( + &mut app, + core_contract_info.address.clone().as_str(), + coins(10, "ujuno"), + ); + + // Non-vetoer cannot veto + let err: ContractError = app + .execute_contract( + Addr::unchecked(CREATOR_ADDR), + &ContractInfo { + address: proposal_module.addr.clone(), + code_hash: proposal_module.code_hash.clone(), + }, + &ExecuteMsg::Veto { proposal_id }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, ContractError::VetoError(VetoError::Unauthorized {})); + + // Oversite vetos prop + app.execute_contract( + Addr::unchecked("oversight"), + &ContractInfo { + address: proposal_module.addr.clone(), + code_hash: proposal_module.code_hash.clone(), + }, + &ExecuteMsg::Veto { proposal_id }, + &[], + ) + .unwrap(); + + let proposal = query_proposal( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + proposal_id, + ); + assert_eq!(proposal.proposal.status, Status::Vetoed); + + Ok(()) +} + +#[test] +fn test_proposal_message_timelock_early_execution() -> anyhow::Result<()> { + 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 veto_config = VetoConfig { + timelock_duration: Duration::Time(100), + vetoer: "oversight".to_string(), + early_execute: true, + veto_before_passed: false, + }; + instantiate.veto = Some(veto_config.clone()); + let core_contract_info = instantiate_with_staked_balances_governance( + &mut app, + instantiate, + Some(vec![ + InitialBalance { + address: "oversight".to_string(), + amount: Uint128::new(15), + }, + InitialBalance { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(85), + }, + ]), + ); + let proposal_module = query_single_proposal_module( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + let gov_token_info = query_dao_token( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + + let query_auth_info: AnyContractInfo = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::QueryAuthInfo {}, + ) + .unwrap(); + + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + + let viewing_key_token = create_snip20_viewing_key( + &mut app, + ContractInfo { + address: gov_token_info.addr.clone(), + code_hash: gov_token_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + + let viewing_key_oversight = create_snip20_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info("oversight", &[]), + ); + + mint_snip20s( + &mut app, + &gov_token_info.addr, + gov_token_info.code_hash.clone(), + &core_contract_info.address, + CREATOR_ADDR, + 10_000_000, + ); + let proposal_id = make_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + vec![ + WasmMsg::Execute { + contract_addr: gov_token_info.addr.clone().to_string(), + code_hash: gov_token_info.code_hash.clone(), + msg: to_binary(&snip20_reference_impl::msg::ExecuteMsg::Mint { + recipient: CREATOR_ADDR.to_string(), + amount: Uint128::new(10_000_000), + memo: None, + decoys: None, + entropy: None, + padding: None, + }) + .unwrap(), + funds: vec![], + } + .into(), + BankMsg::Send { + to_address: CREATOR_ADDR.to_string(), + amount: coins(10, "ujuno"), + } + .into(), + ], + ); + let snip20_balance = query_balance_cw20( + &app, + &gov_token_info.addr, + gov_token_info.code_hash.clone(), + CREATOR_ADDR, + viewing_key_token.clone(), + ); + let native_balance = query_balance_native(&app, CREATOR_ADDR, "ujuno"); + assert_eq!(snip20_balance, Uint128::zero()); + assert_eq!(native_balance, Uint128::zero()); + + // Vote on proposal to pass it + vote_on_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + proposal_id, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + Vote::Yes, + ); + let proposal = query_proposal( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + proposal_id, + ); + // Proposal is timelocked to the moment of prop expiring + timelock delay + assert_eq!( + proposal.proposal.status, + Status::VetoTimelock { + expiration: proposal + .proposal + .expiration + .add(veto_config.timelock_duration)?, + } + ); + + mint_natives( + &mut app, + core_contract_info.address.clone().as_str(), + coins(10, "ujuno"), + ); + + // Proposal can be executed early by vetoer + execute_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + "oversight", + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key_oversight.clone(), + address: "oversight".into(), + }, + proposal_id, + ); + let proposal = query_proposal( + &app, + &proposal_module.addr, + proposal_module.code_hash, + proposal_id, + ); + assert_eq!(proposal.proposal.status, Status::Executed); + + Ok(()) +} + +#[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.veto = Some(VetoConfig { + timelock_duration: Duration::Time(100), + vetoer: "oversight".to_string(), + early_execute: false, + veto_before_passed: true, + }); + let core_contract_info = instantiate_with_staked_balances_governance( + &mut app, + instantiate, + Some(vec![ + InitialBalance { + address: "oversight".to_string(), + amount: Uint128::new(15), + }, + InitialBalance { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(85), + }, + ]), + ); + let proposal_module = query_single_proposal_module( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + let gov_token_info = query_dao_token( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + + let query_auth_info: AnyContractInfo = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::QueryAuthInfo {}, + ) + .unwrap(); + + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + + mint_snip20s( + &mut app, + &gov_token_info.addr, + gov_token_info.code_hash.clone(), + &core_contract_info.address, + CREATOR_ADDR, + 10_000_000, + ); + let proposal_id = make_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + vec![ + WasmMsg::Execute { + contract_addr: gov_token_info.addr.clone().to_string(), + code_hash: gov_token_info.code_hash.clone(), + msg: to_binary(&snip20_reference_impl::msg::ExecuteMsg::Mint { + recipient: CREATOR_ADDR.to_string(), + amount: Uint128::new(10_000_000), + memo: None, + decoys: None, + entropy: None, + padding: None, + }) + .unwrap(), + funds: vec![], + } + .into(), + BankMsg::Send { + to_address: CREATOR_ADDR.to_string(), + amount: coins(10, "ujuno"), + } + .into(), + ], + ); + let proposal = query_proposal( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + proposal_id, + ); + + // Proposal is open for voting + assert_eq!(proposal.proposal.status, Status::Open); + + // Oversite vetos prop + app.execute_contract( + Addr::unchecked("oversight"), + &ContractInfo { + address: proposal_module.addr.clone(), + code_hash: proposal_module.code_hash.clone(), + }, + &ExecuteMsg::Veto { proposal_id }, + &[], + ) + .unwrap(); + + let proposal = query_proposal( + &app, + &proposal_module.addr, + proposal_module.code_hash, + proposal_id, + ); + assert_eq!(proposal.proposal.status, Status::Vetoed); + + // mint_natives(&mut app, core_contract_info.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_veto_only_members_execute_proposal() -> anyhow::Result<()> { + 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 veto_config = VetoConfig { + timelock_duration: Duration::Time(100), + vetoer: "oversight".to_string(), + early_execute: true, + veto_before_passed: false, + }; + instantiate.veto = Some(veto_config.clone()); + let core_contract_info = instantiate_with_staked_balances_governance( + &mut app, + instantiate, + Some(vec![InitialBalance { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(85), + }]), + ); + let proposal_module = query_single_proposal_module( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + let gov_token_info = query_dao_token( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + + let query_auth_info: AnyContractInfo = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::QueryAuthInfo {}, + ) + .unwrap(); + + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + + let viewing_key_token = create_snip20_viewing_key( + &mut app, + ContractInfo { + address: gov_token_info.addr.clone(), + code_hash: gov_token_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + + let viewing_key_oversight = create_snip20_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info("oversight", &[]), + ); + + mint_snip20s( + &mut app, + &gov_token_info.addr, + gov_token_info.code_hash.clone(), + &core_contract_info.address, + CREATOR_ADDR, + 10_000_000, + ); + let proposal_id = make_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + vec![ + WasmMsg::Execute { + contract_addr: gov_token_info.addr.clone().to_string(), + code_hash: gov_token_info.code_hash.clone(), + msg: to_binary(&snip20_reference_impl::msg::ExecuteMsg::Mint { + recipient: CREATOR_ADDR.to_string(), + amount: Uint128::new(10_000_000), + memo: None, + decoys: None, + entropy: None, + padding: None, + }) + .unwrap(), + funds: vec![], + } + .into(), + BankMsg::Send { + to_address: CREATOR_ADDR.to_string(), + amount: coins(10, "ujuno"), + } + .into(), + ], + ); + let snip20_balance = query_balance_cw20( + &app, + &gov_token_info.addr, + gov_token_info.code_hash.clone(), + CREATOR_ADDR, + viewing_key_token.clone(), + ); + let native_balance = query_balance_native(&app, CREATOR_ADDR, "ujuno"); + assert_eq!(snip20_balance, Uint128::zero()); + assert_eq!(native_balance, Uint128::zero()); + + vote_on_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + proposal_id, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + Vote::Yes, + ); + let proposal = query_proposal( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + proposal_id, + ); + + // Proposal is timelocked to the moment of prop expiring + timelock delay + let expiration = proposal + .proposal + .expiration + .add(veto_config.timelock_duration)?; + assert_eq!( + proposal.proposal.status, + Status::VetoTimelock { expiration } + ); + + app.update_block(|b| b.time = b.time.plus_seconds(604800 + 101)); + // assert timelock is expired + assert!(expiration.is_expired(&app.block_info())); + mint_natives( + &mut app, + core_contract_info.address.clone().as_str(), + coins(10, "ujuno"), + ); + + let proposal = query_proposal( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + proposal_id, + ); + assert_eq!(proposal.proposal.status, Status::Passed); + + // Proposal cannot be executed by vetoer once timelock expired + let err: ContractError = app + .execute_contract( + Addr::unchecked("oversight"), + &ContractInfo { + address: proposal_module.addr.clone(), + code_hash: proposal_module.code_hash.clone(), + }, + &ExecuteMsg::Execute { + auth: shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key_oversight, + address: "oversight".to_string(), + }, + proposal_id, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, ContractError::Unauthorized {}); + + // Proposal can be executed by member once timelock expired + execute_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key, + address: CREATOR_ADDR.to_string(), + }, + proposal_id, + ); + let proposal = query_proposal( + &app, + &proposal_module.addr, + proposal_module.code_hash, + proposal_id, + ); + assert_eq!(proposal.proposal.status, Status::Executed); + + Ok(()) +} + +#[test] +fn test_proposal_close_after_expiry() { + let CommonTest { + mut app, + core_contract_info, + proposal_module, + gov_token_info: _, + proposal_id, + query_auth_info: _, + } = setup_test(vec![BankMsg::Send { + to_address: CREATOR_ADDR.to_string(), + amount: coins(10, "ujuno"), + } + .into()]); + mint_natives( + &mut app, + core_contract_info.address.as_str(), + coins(10, "ujuno"), + ); + + // Try and close the proposal. This shoudl fail as the proposal is + // open. + let err = close_proposal_should_fail( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + proposal_id, + ); + assert!(matches!(err, ContractError::WrongCloseStatus {})); + + // Expire the proposal. Now it should be closable. + app.update_block(|b| b.time = b.time.plus_seconds(604800)); + close_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + proposal_id, + ); + let proposal = query_proposal( + &app, + &proposal_module.addr, + proposal_module.code_hash, + proposal_id, + ); + assert_eq!(proposal.proposal.status, Status::Closed); +} + +#[test] +fn test_proposal_cant_close_after_expiry_is_passed() { + let mut app = App::default(); + let instantiate = get_default_token_dao_proposal_module_instantiate(&mut app); + let core_contract_info = instantiate_with_staked_balances_governance( + &mut app, + instantiate, + Some(vec![ + InitialBalance { + address: "quorum".to_string(), + amount: Uint128::new(15), + }, + InitialBalance { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(85), + }, + ]), + ); + let proposal_module = query_single_proposal_module( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + let gov_token_info = query_dao_token( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + + mint_snip20s( + &mut app, + &gov_token_info.addr, + gov_token_info.code_hash.clone(), + &core_contract_info.address, + CREATOR_ADDR, + 10_000_000, + ); + mint_natives( + &mut app, + core_contract_info.address.clone().as_str(), + coins(10, "ujuno"), + ); + + let query_auth_info: AnyContractInfo = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::QueryAuthInfo {}, + ) + .unwrap(); + + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + + let viewing_key_quorum = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info("quorum", &[]), + ); + + let proposal_id = make_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + vec![BankMsg::Send { + to_address: CREATOR_ADDR.to_string(), + amount: coins(10, "ujuno"), + } + .into()], + ); + vote_on_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + "quorum", + proposal_id, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key_quorum.clone(), + address: "quorum".into(), + }, + Vote::Yes, + ); + let proposal = query_proposal( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + proposal_id, + ); + assert_eq!(proposal.proposal.status, Status::Open); + + // Expire the proposal. This should pass it. + app.update_block(|b| b.time = b.time.plus_seconds(604800)); + let proposal = query_proposal( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + proposal_id, + ); + assert_eq!(proposal.proposal.status, Status::Passed,); + + // Make sure it can't be closed. + let err = close_proposal_should_fail( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + proposal_id, + ); + assert!(matches!(err, ContractError::WrongCloseStatus {})); + + // Executed proposals may not be closed. + execute_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + proposal_id, + ); + let err = close_proposal_should_fail( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + proposal_id, + ); + assert!(matches!(err, ContractError::WrongCloseStatus {})); + let balance = query_balance_native(&app, CREATOR_ADDR, "ujuno"); + assert_eq!(balance, Uint128::new(10)); + let err = close_proposal_should_fail( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + proposal_id, + ); + assert!(matches!(err, ContractError::WrongCloseStatus {})); +} + +#[test] +fn test_execute_no_non_passed_execution() { + let CommonTest { + mut app, + core_contract_info, + proposal_module, + gov_token_info, + proposal_id, + query_auth_info, + } = setup_test(vec![BankMsg::Send { + to_address: CREATOR_ADDR.to_string(), + amount: coins(10, "ujuno"), + } + .into()]); + mint_natives( + &mut app, + core_contract_info.address.clone().as_str(), + coins(100, "ujuno"), + ); + + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + + let err = execute_proposal_should_fail( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + proposal_id, + ); + assert!(matches!(err, ContractError::NotPassed {})); + + // Expire the proposal. + app.update_block(|b| b.time = b.time.plus_seconds(604800)); + let err = execute_proposal_should_fail( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + proposal_id, + ); + assert!(matches!(err, ContractError::NotPassed {})); + + mint_snip20s( + &mut app, + &gov_token_info.addr, + gov_token_info.code_hash.clone(), + &core_contract_info.address, + CREATOR_ADDR, + 10_000_000, + ); + let proposal_id = make_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + vec![], + ); + vote_on_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + proposal_id, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + Vote::Yes, + ); + execute_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + proposal_id, + ); + // Can't execute more than once. + let err = execute_proposal_should_fail( + &mut app, + &proposal_module.addr, + proposal_module.code_hash, + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + proposal_id, + ); + assert!(matches!(err, ContractError::NotPassed {})); +} + +#[test] +fn test_cant_execute_not_member_when_proposal_created() { + let CommonTest { + mut app, + core_contract_info, + proposal_module, + gov_token_info, + proposal_id, + query_auth_info, + } = setup_test(vec![BankMsg::Send { + to_address: CREATOR_ADDR.to_string(), + amount: coins(10, "ujuno"), + } + .into()]); + mint_natives( + &mut app, + core_contract_info.address.clone().as_str(), + coins(100, "ujuno"), + ); + + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + + let viewing_key_noah = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info("noah", &[]), + ); + + vote_on_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + proposal_id, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + Vote::Yes, + ); + + // Give noah some tokens. + mint_snip20s( + &mut app, + &gov_token_info.addr, + gov_token_info.code_hash.clone(), + &core_contract_info.address, + "noah", + 20_000_000, + ); + // Have noah stake some. + let voting_module = query_voting_module( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + let staking_contract: AnyContractInfo = app + .wrap() + .query_wasm_smart( + voting_module.code_hash.clone(), + voting_module.addr.clone(), + &dao_voting_snip20_staked::msg::QueryMsg::StakingContract {}, + ) + .unwrap(); + app.execute_contract( + Addr::unchecked("noah"), + &ContractInfo { + address: gov_token_info.addr.clone(), + code_hash: gov_token_info.code_hash.clone(), + }, + &snip20_reference_impl::msg::ExecuteMsg::Send { + recipient: staking_contract.addr.clone().to_string(), + recipient_code_hash: Some(staking_contract.code_hash.clone()), + amount: Uint128::new(10_000_000), + msg: Some( + to_binary(&snip20_stake::msg::ReceiveMsg::Stake { + auth: Box::new(shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key_noah.clone(), + address: "noah".into(), + }), + }) + .unwrap(), + ), + memo: None, + decoys: None, + entropy: None, + padding: None, + }, + &[], + ) + .unwrap(); + // Update the block so that the staked balance appears. + app.update_block(|block| block.height += 1); + + // println!("here"); + + // // Can't execute from member who wasn't a member when the proposal was + // // created. + // let err = execute_proposal_should_fail( + // &mut app, + // &proposal_module.addr, + // proposal_module.code_hash.clone(), + // "noah", + // shade_protocol::basic_staking::Auth::ViewingKey { + // key: viewing_key_noah.clone(), + // address: "noah".into(), + // }, + // proposal_id, + // ); + // assert!(matches!(err, ContractError::Unauthorized {})); +} + +#[test] +fn test_update_config() { + let CommonTest { + mut app, + core_contract_info, + proposal_module, + gov_token_info: _, + proposal_id, + query_auth_info, + } = setup_test(vec![]); + + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + + vote_on_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + proposal_id, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + Vote::Yes, + ); + execute_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + proposal_id, + ); + // Make a proposal to update the config. + let proposal_id = make_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + vec![WasmMsg::Execute { + contract_addr: proposal_module.addr.clone().to_string(), + code_hash: proposal_module.code_hash.clone(), + msg: to_binary(&ExecuteMsg::UpdateConfig { + veto: Some(VetoConfig { + timelock_duration: Duration::Height(2), + vetoer: CREATOR_ADDR.to_string(), + early_execute: false, + veto_before_passed: false, + }), + threshold: Threshold::AbsoluteCount { + threshold: Uint128::new(10_000), + }, + max_voting_period: Duration::Height(6), + min_voting_period: None, + only_members_execute: true, + allow_revoting: false, + close_proposal_on_execution_failure: false, + }) + .unwrap(), + funds: vec![], + } + .into()], + ); + vote_on_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + proposal_id, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + Vote::Yes, + ); + execute_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + proposal_id, + ); + + let config = query_proposal_config( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + ); + assert_eq!( + config, + Config { + veto: Some(VetoConfig { + timelock_duration: Duration::Height(2), + vetoer: CREATOR_ADDR.to_string(), + early_execute: false, + veto_before_passed: false, + }), + threshold: Threshold::AbsoluteCount { + threshold: Uint128::new(10_000) + }, + max_voting_period: Duration::Height(6), + min_voting_period: None, + only_members_execute: true, + allow_revoting: false, + close_proposal_on_execution_failure: false, + query_auth: Contract { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone() + } + } + ); + + // Check that non-dao address may not update config. + let err: ContractError = app + .execute_contract( + Addr::unchecked(CREATOR_ADDR), + &ContractInfo { + address: proposal_module.addr.clone(), + code_hash: proposal_module.code_hash.clone(), + }, + &&ExecuteMsg::UpdateConfig { + veto: None, + threshold: Threshold::AbsoluteCount { + threshold: Uint128::new(10_000), + }, + max_voting_period: Duration::Height(6), + min_voting_period: None, + only_members_execute: true, + allow_revoting: false, + close_proposal_on_execution_failure: false, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert!(matches!(err, ContractError::Unauthorized {})); + + // Check that veto config is validated (mismatching duration units). + let err: ContractError = app + .execute_contract( + Addr::unchecked(core_contract_info.address.clone()), + &ContractInfo { + address: proposal_module.addr.clone(), + code_hash: proposal_module.code_hash.clone(), + }, + &&ExecuteMsg::UpdateConfig { + veto: Some(VetoConfig { + timelock_duration: Duration::Time(100), + vetoer: CREATOR_ADDR.to_string(), + early_execute: false, + veto_before_passed: false, + }), + threshold: Threshold::AbsoluteCount { + threshold: Uint128::new(10_000), + }, + max_voting_period: Duration::Height(6), + min_voting_period: None, + only_members_execute: true, + allow_revoting: false, + close_proposal_on_execution_failure: false, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert!(matches!( + err, + ContractError::VetoError(VetoError::TimelockDurationUnitMismatch {}) + )) +} + +#[test] +fn test_anyone_may_propose_and_proposal_listing() { + let mut app = App::default(); + let mut instantiate = get_default_token_dao_proposal_module_instantiate(&mut app); + instantiate.pre_propose_info = PreProposeInfo::AnyoneMayPropose {}; + let core_contract_info = + instantiate_with_staked_balances_governance(&mut app, instantiate, None); + let proposal_module = query_single_proposal_module( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + + let query_auth_info: AnyContractInfo = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::QueryAuthInfo {}, + ) + .unwrap(); + + let viewing_key_creator = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + for addr in 'm'..'z' { + let addr = addr.to_string().repeat(6); + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(&addr, &[]), + ); + let proposal_id = make_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + &addr, + Auth::ViewingKey { + key: viewing_key.clone(), + address: addr.clone(), + }, + vec![], + ); + vote_on_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + proposal_id, + Auth::ViewingKey { + key: viewing_key_creator.clone(), + address: CREATOR_ADDR.into(), + }, + Vote::Yes, + ); + + // Only members can execute still. + let err = execute_proposal_should_fail( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + &addr, + Auth::ViewingKey { + key: viewing_key.clone(), + address: addr.clone(), + }, + proposal_id, + ); + assert!(matches!(err, ContractError::Unauthorized {})); + execute_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + Auth::ViewingKey { + key: viewing_key_creator.clone(), + address: CREATOR_ADDR.into(), + }, + proposal_id, + ); + } + + // Now that we've got all these proposals sitting around, lets + // test that we can query them. + + let proposals_forward = query_list_proposals( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + None, + None, + ); + + let mut proposals_reverse = query_list_proposals_reverse( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + None, + None, + ); + proposals_reverse.proposals.reverse(); + assert_eq!(proposals_reverse, proposals_forward); + + // Check the proposers and (implicitly) the ordering. + for (index, addr) in ('m'..'z').enumerate() { + let addr = addr.to_string().repeat(6); + assert_eq!( + proposals_forward.proposals[index].proposal.proposer, + Addr::unchecked(addr) + ) + } + + let four_and_five = query_list_proposals( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + Some(3), + Some(2), + ); + let mut five_and_four = query_list_proposals_reverse( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + Some(6), + Some(2), + ); + five_and_four.proposals.reverse(); + + assert_eq!(five_and_four, four_and_five); + assert_eq!( + four_and_five.proposals[0].proposal.proposer, + Addr::unchecked("pppppp") + ); + + let current_block = app.block_info(); + assert_eq!( + four_and_five.proposals[0], + ProposalResponse { + id: 4, + proposal: SingleChoiceProposal { + title: "title".to_string(), + description: "description".to_string(), + proposer: Addr::unchecked("pppppp"), + start_height: current_block.height, + min_voting_period: None, + expiration: Duration::Time(604800).after(¤t_block), + threshold: Threshold::ThresholdQuorum { + quorum: PercentageThreshold::Percent(Decimal::percent(15)), + threshold: PercentageThreshold::Majority {}, + }, + allow_revoting: false, + total_power: Uint128::new(100_000_000), + msgs: vec![], + status: Status::Executed, + votes: Votes { + yes: Uint128::new(100_000_000), + no: Uint128::zero(), + abstain: Uint128::zero() + }, + veto: None + } + } + ) +} + +#[test] +fn test_proposal_hook_registration() { + let CommonTest { + mut app, + core_contract_info, + proposal_module, + gov_token_info: _, + proposal_id: _, + .. + } = setup_test(vec![]); + + let proposal_hooks = query_proposal_hooks( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + ); + assert_eq!( + proposal_hooks.hooks.len(), + 0, + "pre-propose deposit module should not show on this listing" + ); + + // non-dao may not add a hook. + let err = add_proposal_hook_should_fail( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + "proposalhook", + "proposalhook_code_hash".to_string(), + ); + assert!(matches!(err, ContractError::Unauthorized {})); + + add_proposal_hook( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + core_contract_info.address.clone().as_str(), + "proposalhook", + "proposalhook_code_hash".into(), + ); + let err = add_proposal_hook_should_fail( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + core_contract_info.address.clone().as_str(), + "proposalhook", + "proposalhook_code_hash".into(), + ); + assert!(matches!( + err, + ContractError::HookError(HookError::HookAlreadyRegistered {}) + )); + + let proposal_hooks = query_proposal_hooks( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + ); + assert_eq!( + proposal_hooks.hooks[0], + HookItem { + addr: Addr::unchecked("proposalhook"), + code_hash: "proposalhook_code_hash".into() + } + ); + + // Only DAO can remove proposal hooks. + let err = remove_proposal_hook_should_fail( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + "proposalhook", + "proposalhook_code_hash".into(), + ); + assert!(matches!(err, ContractError::Unauthorized {})); + remove_proposal_hook( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + core_contract_info.address.clone().as_str(), + "proposalhook", + "proposalhook_code_hash".into(), + ); + let proposal_hooks = query_proposal_hooks( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + ); + assert_eq!(proposal_hooks.hooks.len(), 0); + + // Can not remove that which does not exist. + let err = remove_proposal_hook_should_fail( + &mut app, + &proposal_module.addr, + proposal_module.code_hash, + core_contract_info.address.as_str(), + "proposalhook", + "proposalhook_code_hash".into(), + ); + assert!(matches!( + err, + ContractError::HookError(HookError::HookNotRegistered {}) + )); +} + +#[test] +fn test_vote_hook_registration() { + let CommonTest { + mut app, + core_contract_info, + proposal_module, + gov_token_info: _, + proposal_id: _, + .. + } = setup_test(vec![]); + + let vote_hooks = query_vote_hooks( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + ); + assert!(vote_hooks.hooks.is_empty(),); + + // non-dao may not add a hook. + let err = add_vote_hook_should_fail( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + "votehook", + "votehook_codehash".into(), + ); + assert!(matches!(err, ContractError::Unauthorized {})); + + add_vote_hook( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + core_contract_info.address.clone().as_str(), + "votehook", + "votehook_codehash".into(), + ); + + let vote_hooks = query_vote_hooks( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + ); + assert_eq!( + vote_hooks, + HooksResponse { + hooks: vec![HookItem { + addr: Addr::unchecked("votehook"), + code_hash: "votehook_codehash".into() + }] + } + ); + + let err = add_vote_hook_should_fail( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + core_contract_info.address.clone().as_str(), + "votehook", + "votehook_codehash".into(), + ); + assert!(matches!( + err, + ContractError::HookError(HookError::HookAlreadyRegistered {}) + )); + + let vote_hooks = query_vote_hooks( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + ); + assert_eq!( + vote_hooks.hooks[0], + HookItem { + addr: Addr::unchecked("votehook"), + code_hash: "votehook_codehash".into() + } + ); + + // Only DAO can remove vote hooks. + let err = remove_vote_hook_should_fail( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + "votehook", + "votehook_codehash".into(), + ); + assert!(matches!(err, ContractError::Unauthorized {})); + remove_vote_hook( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + core_contract_info.address.as_str(), + "votehook", + "votehook_codehash".into(), + ); + + let vote_hooks = query_vote_hooks( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + ); + assert!(vote_hooks.hooks.is_empty(),); + + // Can not remove that which does not exist. + let err = remove_vote_hook_should_fail( + &mut app, + &proposal_module.addr, + proposal_module.code_hash, + core_contract_info.address.as_str(), + "votehook", + "votehook_codehash".into(), + ); + assert!(matches!( + err, + ContractError::HookError(HookError::HookNotRegistered {}) + )); +} + +#[test] +fn test_active_threshold_absolute() { + let mut app = App::default(); + let mut instantiate = get_default_token_dao_proposal_module_instantiate(&mut app); + instantiate.pre_propose_info = PreProposeInfo::AnyoneMayPropose {}; + let core_contract_info = instantiate_with_staking_active_threshold( + &mut app, + instantiate, + None, + Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(100), + }), + ); + let gov_token_info = query_dao_token( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + let proposal_module = query_single_proposal_module( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + let voting_module = query_voting_module( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + + let query_auth_info: AnyContractInfo = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::QueryAuthInfo {}, + ) + .unwrap(); + + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + + let staking_contract: AnyContractInfo = app + .wrap() + .query_wasm_smart( + voting_module.code_hash.clone(), + voting_module.addr.clone(), + &dao_voting_snip20_staked::msg::QueryMsg::StakingContract {}, + ) + .unwrap(); + + let err: ContractError = app + .execute_contract( + Addr::unchecked(CREATOR_ADDR), + &ContractInfo { + address: proposal_module.addr.clone(), + code_hash: proposal_module.code_hash.clone(), + }, + &ExecuteMsg::Propose(ProposeMsg { + title: "title".to_string(), + description: "description".to_string(), + msgs: vec![], + proposer: None, + }), + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert!(matches!(err, ContractError::InactiveDao {})); + + let msg = snip20_reference_impl::msg::ExecuteMsg::Send { + recipient: staking_contract.addr.clone().to_string(), + amount: Uint128::new(100), + msg: Some( + to_binary(&snip20_stake::msg::ReceiveMsg::Stake { + auth: Box::new(Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }), + }) + .unwrap(), + ), + recipient_code_hash: Some(staking_contract.code_hash.clone()), + memo: None, + decoys: None, + entropy: None, + padding: None, + }; + app.execute_contract( + Addr::unchecked(CREATOR_ADDR), + &ContractInfo { + address: gov_token_info.addr.clone(), + code_hash: gov_token_info.code_hash.clone(), + }, + &msg, + &[], + ) + .unwrap(); + app.update_block(next_block); + + // Proposal creation now works as tokens have been staked to reach + // active threshold. + make_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + vec![], + ); + + // Unstake some tokens to make it inactive again. + let msg = snip20_stake::msg::ExecuteMsg::Unstake { + auth: Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + amount: Uint128::new(50), + }; + app.execute_contract( + Addr::unchecked(CREATOR_ADDR), + &ContractInfo { + address: staking_contract.addr.clone(), + code_hash: staking_contract.code_hash.clone(), + }, + &msg, + &[], + ) + .unwrap(); + app.update_block(next_block); + + let err: ContractError = app + .execute_contract( + Addr::unchecked(CREATOR_ADDR), + &ContractInfo { + address: proposal_module.addr.clone(), + code_hash: proposal_module.code_hash.clone(), + }, + &ExecuteMsg::Propose(ProposeMsg { + title: "title".to_string(), + description: "description".to_string(), + msgs: vec![], + proposer: None, + }), + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert!(matches!(err, ContractError::InactiveDao {})); +} + +#[test] +fn test_active_threshold_percent() { + let mut app = App::default(); + let mut instantiate = get_default_token_dao_proposal_module_instantiate(&mut app); + instantiate.pre_propose_info = PreProposeInfo::AnyoneMayPropose {}; + let core_contract_info = instantiate_with_staking_active_threshold( + &mut app, + instantiate, + None, + Some(ActiveThreshold::Percentage { + percent: Decimal::percent(20), + }), + ); + let gov_token_info = query_dao_token( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + let proposal_module = query_single_proposal_module( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + let voting_module = query_voting_module( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + + let query_auth_info: AnyContractInfo = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::QueryAuthInfo {}, + ) + .unwrap(); + + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + + let staking_contract: AnyContractInfo = app + .wrap() + .query_wasm_smart( + voting_module.code_hash.clone(), + voting_module.addr.clone(), + &dao_voting_snip20_staked::msg::QueryMsg::StakingContract {}, + ) + .unwrap(); + + let err: ContractError = app + .execute_contract( + Addr::unchecked(CREATOR_ADDR), + &ContractInfo { + address: proposal_module.addr.clone(), + code_hash: proposal_module.code_hash.clone(), + }, + &ExecuteMsg::Propose(ProposeMsg { + title: "title".to_string(), + description: "description".to_string(), + msgs: vec![], + proposer: None, + }), + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert!(matches!(err, ContractError::InactiveDao {})); + + let msg = snip20_reference_impl::msg::ExecuteMsg::Send { + recipient: staking_contract.addr.clone().to_string(), + amount: Uint128::new(20_000_000), + msg: Some( + to_binary(&snip20_stake::msg::ReceiveMsg::Stake { + auth: Box::new(Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }), + }) + .unwrap(), + ), + recipient_code_hash: Some(staking_contract.code_hash.clone()), + memo: None, + decoys: None, + entropy: None, + padding: None, + }; + app.execute_contract( + Addr::unchecked(CREATOR_ADDR), + &ContractInfo { + address: gov_token_info.addr.clone(), + code_hash: gov_token_info.code_hash.clone(), + }, + &msg, + &[], + ) + .unwrap(); + app.update_block(next_block); + + // Proposal creation now works as tokens have been staked to reach + // active threshold. + make_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + vec![], + ); + + // Unstake some tokens to make it inactive again. + let msg = snip20_stake::msg::ExecuteMsg::Unstake { + amount: Uint128::new(1), // Only one is needed as we're right + auth: Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, // on the edge. :) + }; + app.execute_contract( + Addr::unchecked(CREATOR_ADDR), + &ContractInfo { + address: staking_contract.addr.clone(), + code_hash: staking_contract.code_hash.clone(), + }, + &msg, + &[], + ) + .unwrap(); + app.update_block(next_block); + + let err: ContractError = app + .execute_contract( + Addr::unchecked(CREATOR_ADDR), + &ContractInfo { + address: proposal_module.addr, + code_hash: proposal_module.code_hash, + }, + &ExecuteMsg::Propose(ProposeMsg { + title: "title".to_string(), + description: "description".to_string(), + msgs: vec![], + proposer: None, + }), + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert!(matches!(err, ContractError::InactiveDao {})); +} + +#[test] +#[should_panic( + expected = "min_voting_period and max_voting_period must have the same units (height or time)" +)] +fn test_min_duration_unit_missmatch() { + let mut app = App::default(); + let mut instantiate = get_default_token_dao_proposal_module_instantiate(&mut app); + instantiate.min_voting_period = Some(Duration::Height(10)); + instantiate_with_staked_balances_governance(&mut app, instantiate, None); +} + +#[test] +#[should_panic(expected = "Min voting period must be less than or equal to max voting period")] +fn test_min_duration_larger_than_proposal_duration() { + let mut app = App::default(); + let mut instantiate = get_default_token_dao_proposal_module_instantiate(&mut app); + instantiate.min_voting_period = Some(Duration::Time(604801)); + instantiate_with_staked_balances_governance(&mut app, instantiate, None); +} + +#[test] +fn test_min_voting_period_no_early_pass() { + let mut app = App::default(); + let mut instantiate = get_default_token_dao_proposal_module_instantiate(&mut app); + instantiate.min_voting_period = Some(Duration::Height(10)); + instantiate.max_voting_period = Duration::Height(100); + let core_contract_info = + instantiate_with_staked_balances_governance(&mut app, instantiate, None); + let gov_token_info = query_dao_token( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + let proposal_module = query_single_proposal_module( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + + let query_auth_info: AnyContractInfo = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::QueryAuthInfo {}, + ) + .unwrap(); + + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + + mint_snip20s( + &mut app, + &gov_token_info.addr, + gov_token_info.code_hash.clone(), + &core_contract_info.address, + CREATOR_ADDR, + 10_000_000, + ); + let proposal_id = make_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + vec![], + ); + vote_on_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + proposal_id, + Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + Vote::Yes, + ); + let proposal_response = query_proposal( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + proposal_id, + ); + assert_eq!(proposal_response.proposal.status, Status::Open); + + app.update_block(|block| block.height += 10); + let proposal_response = query_proposal( + &app, + &proposal_module.addr, + proposal_module.code_hash, + proposal_id, + ); + assert_eq!(proposal_response.proposal.status, Status::Passed); +} + +// Setting the min duration the same as the proposal duration just +// means that proposals cant close early. +#[test] +fn test_min_duration_same_as_proposal_duration() { + let mut app = App::default(); + let mut instantiate = get_default_token_dao_proposal_module_instantiate(&mut app); + instantiate.min_voting_period = Some(Duration::Height(100)); + instantiate.max_voting_period = Duration::Height(100); + let core_contract_info = instantiate_with_staked_balances_governance( + &mut app, + instantiate, + Some(vec![ + InitialBalance { + address: "ekez".to_string(), + amount: Uint128::new(10), + }, + InitialBalance { + address: "whale".to_string(), + amount: Uint128::new(90), + }, + ]), + ); + let gov_token_info = query_dao_token( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + let proposal_module = query_single_proposal_module( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + + let query_auth_info: AnyContractInfo = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::QueryAuthInfo {}, + ) + .unwrap(); + + let viewing_key_ekez = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info("ekez", &[]), + ); + + let viewing_key_whale = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info("whale", &[]), + ); + + mint_snip20s( + &mut app, + &gov_token_info.addr, + gov_token_info.code_hash.clone(), + &core_contract_info.address, + "ekez", + 10_000_000, + ); + let proposal_id = make_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + "ekez", + Auth::ViewingKey { + key: viewing_key_ekez.clone(), + address: "ekez".into(), + }, + vec![], + ); + + // Whale votes yes. Normally the proposal would just pass and ekez + // would be out of luck. + vote_on_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + "whale", + proposal_id, + Auth::ViewingKey { + key: viewing_key_whale, + address: "whale".into(), + }, + Vote::Yes, + ); + vote_on_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + "ekez", + proposal_id, + Auth::ViewingKey { + key: viewing_key_ekez.clone(), + address: "ekez".into(), + }, + Vote::No, + ); + + app.update_block(|b| b.height += 100); + let proposal_response = query_proposal( + &app, + &proposal_module.addr, + proposal_module.code_hash, + proposal_id, + ); + assert_eq!(proposal_response.proposal.status, Status::Passed); +} + +#[test] +fn test_revoting_playthrough() { + let mut app = App::default(); + let mut instantiate = get_default_token_dao_proposal_module_instantiate(&mut app); + instantiate.allow_revoting = true; + let core_contract_info = + instantiate_with_staked_balances_governance(&mut app, instantiate, None); + let gov_token_info = query_dao_token( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + let proposal_module = query_single_proposal_module( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + + let query_auth_info: AnyContractInfo = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::QueryAuthInfo {}, + ) + .unwrap(); + + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + + mint_snip20s( + &mut app, + &gov_token_info.addr, + gov_token_info.code_hash.clone(), + &core_contract_info.address, + CREATOR_ADDR, + 10_000_000, + ); + let proposal_id = make_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + vec![], + ); + + // Vote and change our minds a couple times. + vote_on_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + proposal_id, + Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + Vote::Yes, + ); + let proposal_response = query_proposal( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + proposal_id, + ); + assert_eq!(proposal_response.proposal.status, Status::Open); + + vote_on_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + proposal_id, + Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + Vote::No, + ); + let proposal_response = query_proposal( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + proposal_id, + ); + assert_eq!(proposal_response.proposal.status, Status::Open); + + vote_on_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + proposal_id, + Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + Vote::Yes, + ); + let proposal_response = query_proposal( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + proposal_id, + ); + assert_eq!(proposal_response.proposal.status, Status::Open); + + // Can't cast the same vote more than once. + let err = vote_on_proposal_should_fail( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + proposal_id, + Vote::Yes, + ); + assert!(matches!(err, ContractError::AlreadyCast {})); + + // Expire the proposal allowing the votes to be tallied. + app.update_block(|b| b.time = b.time.plus_seconds(604800)); + let proposal_response = query_proposal( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + proposal_id, + ); + assert_eq!(proposal_response.proposal.status, Status::Passed); + execute_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + proposal_id, + ); + + // Can't vote once the proposal is passed. + let err = vote_on_proposal_should_fail( + &mut app, + &proposal_module.addr, + proposal_module.code_hash, + CREATOR_ADDR, + Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + proposal_id, + Vote::Yes, + ); + assert!(matches!(err, ContractError::Expired { .. })); +} + +/// Tests that revoting is stored at a per-proposal level. Proposals +/// created while revoting is enabled should not have it disabled if a +/// config change turns if off. +#[test] +fn test_allow_revoting_config_changes() { + let mut app = App::default(); + let mut instantiate = get_default_token_dao_proposal_module_instantiate(&mut app); + instantiate.allow_revoting = true; + let core_contract_info = + instantiate_with_staked_balances_governance(&mut app, instantiate, None); + let gov_token_info = query_dao_token( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + let proposal_module = query_single_proposal_module( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + + let query_auth_info: AnyContractInfo = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::QueryAuthInfo {}, + ) + .unwrap(); + + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + + mint_snip20s( + &mut app, + &gov_token_info.addr, + gov_token_info.code_hash.clone(), + &core_contract_info.address, + CREATOR_ADDR, + 10_000_000, + ); + // This proposal should have revoting enable for its entire + // lifetime. + let revoting_proposal = make_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + vec![], + ); + + // Update the config of the proposal module to disable revoting. + app.execute_contract( + core_contract_info.address.clone(), + &ContractInfo { + address: proposal_module.addr.clone(), + code_hash: proposal_module.code_hash.clone(), + }, + &ExecuteMsg::UpdateConfig { + veto: None, + threshold: Threshold::ThresholdQuorum { + quorum: PercentageThreshold::Percent(Decimal::percent(15)), + threshold: PercentageThreshold::Majority {}, + }, + max_voting_period: Duration::Height(10), + min_voting_period: None, + only_members_execute: true, + // Turn off revoting. + allow_revoting: false, + close_proposal_on_execution_failure: false, + }, + &[], + ) + .unwrap(); + + mint_snip20s( + &mut app, + &gov_token_info.addr, + gov_token_info.code_hash.clone(), + &core_contract_info.address, + CREATOR_ADDR, + 10_000_000, + ); + let no_revoting_proposal = make_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + vec![], + ); + + vote_on_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + revoting_proposal, + Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + Vote::Yes, + ); + vote_on_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + no_revoting_proposal, + Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + Vote::Yes, + ); + + // Proposal without revoting should have passed. + let proposal_resp = query_proposal( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + no_revoting_proposal, + ); + assert_eq!(proposal_resp.proposal.status, Status::Passed); + + // Proposal with revoting should not have passed. + let proposal_resp = query_proposal( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + revoting_proposal, + ); + assert_eq!(proposal_resp.proposal.status, Status::Open); + + // Can change vote on the revoting proposal. + vote_on_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + revoting_proposal, + Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + Vote::No, + ); + // Expire the revoting proposal and close it. + app.update_block(|b| b.time = b.time.plus_seconds(604800)); + close_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash, + CREATOR_ADDR, + revoting_proposal, + ); +} + +/// Tests a simple three of five multisig configuration. +#[test] +fn test_three_of_five_multisig() { + let mut app = App::default(); + let mut instantiate = get_default_non_token_dao_proposal_module_instantiate(&mut app); + instantiate.threshold = Threshold::AbsoluteCount { + threshold: Uint128::new(3), + }; + instantiate.pre_propose_info = PreProposeInfo::AnyoneMayPropose {}; + let core_contract_info = instantiate_with_cw4_groups_governance( + &mut app, + instantiate, + Some(vec![ + InitialBalance { + address: "one".to_string(), + amount: Uint128::new(1), + }, + InitialBalance { + address: "two".to_string(), + amount: Uint128::new(1), + }, + InitialBalance { + address: "three".to_string(), + amount: Uint128::new(1), + }, + InitialBalance { + address: "four".to_string(), + amount: Uint128::new(1), + }, + InitialBalance { + address: "five".to_string(), + amount: Uint128::new(1), + }, + ]), + ); + + let core_state: dao_interface::query::DumpStateResponse = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::DumpState {}, + ) + .unwrap(); + let proposal_module_address = core_state + .proposal_modules + .clone() + .into_iter() + .next() + .unwrap() + .address; + + let proposal_module_codehash = core_state + .proposal_modules + .into_iter() + .next() + .unwrap() + .code_hash; + + let query_auth_info: AnyContractInfo = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::QueryAuthInfo {}, + ) + .unwrap(); + + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + + let viewing_key_one = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info("one", &[]), + ); + + let viewing_key_two = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info("two", &[]), + ); + + let viewing_key_three = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info("three", &[]), + ); + + let viewing_key_four = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info("four", &[]), + ); + + let proposal_id = make_proposal( + &mut app, + &proposal_module_address, + proposal_module_codehash.clone(), + CREATOR_ADDR, + Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + vec![], + ); + + vote_on_proposal( + &mut app, + &proposal_module_address, + proposal_module_codehash.clone(), + "one", + proposal_id, + Auth::ViewingKey { + key: viewing_key_one.clone(), + address: "one".into(), + }, + Vote::Yes, + ); + vote_on_proposal( + &mut app, + &proposal_module_address, + proposal_module_codehash.clone(), + "two", + proposal_id, + Auth::ViewingKey { + key: viewing_key_two.clone(), + address: "two".into(), + }, + Vote::Yes, + ); + + // Make sure it doesn't pass early. + let proposal: ProposalResponse = query_proposal( + &app, + &proposal_module_address, + proposal_module_codehash.clone(), + 1, + ); + assert_eq!(proposal.proposal.status, Status::Open); + + vote_on_proposal( + &mut app, + &proposal_module_address, + proposal_module_codehash.clone(), + "three", + proposal_id, + Auth::ViewingKey { + key: viewing_key_three.clone(), + address: "three".into(), + }, + Vote::Yes, + ); + + let proposal: ProposalResponse = query_proposal( + &app, + &proposal_module_address, + proposal_module_codehash.clone(), + 1, + ); + assert_eq!(proposal.proposal.status, Status::Passed); + + execute_proposal( + &mut app, + &proposal_module_address, + proposal_module_codehash.clone(), + "four", + Auth::ViewingKey { + key: viewing_key_four.clone(), + address: "four".into(), + }, + proposal_id, + ); + + let proposal: ProposalResponse = query_proposal( + &app, + &proposal_module_address, + proposal_module_codehash.clone(), + 1, + ); + assert_eq!(proposal.proposal.status, Status::Executed); + + // Make another proposal which we'll reject. + let proposal_id = make_proposal( + &mut app, + &proposal_module_address, + proposal_module_codehash.clone(), + "one", + Auth::ViewingKey { + key: viewing_key_one.clone(), + address: "one".into(), + }, + vec![], + ); + + vote_on_proposal( + &mut app, + &proposal_module_address, + proposal_module_codehash.clone(), + "one", + proposal_id, + Auth::ViewingKey { + key: viewing_key_one.clone(), + address: "one".into(), + }, + Vote::Yes, + ); + vote_on_proposal( + &mut app, + &proposal_module_address, + proposal_module_codehash.clone(), + "two", + proposal_id, + Auth::ViewingKey { + key: viewing_key_two.clone(), + address: "two".into(), + }, + Vote::No, + ); + vote_on_proposal( + &mut app, + &proposal_module_address, + proposal_module_codehash.clone(), + "three", + proposal_id, + Auth::ViewingKey { + key: viewing_key_three.clone(), + address: "three".into(), + }, + Vote::No, + ); + vote_on_proposal( + &mut app, + &proposal_module_address, + proposal_module_codehash.clone(), + "four", + proposal_id, + Auth::ViewingKey { + key: viewing_key_four.clone(), + address: "four".into(), + }, + Vote::No, + ); + + let proposal = query_proposal( + &app, + &proposal_module_address, + proposal_module_codehash.clone(), + proposal_id, + ); + assert_eq!(proposal.proposal.status, Status::Rejected); + + close_proposal( + &mut app, + &proposal_module_address, + proposal_module_codehash.clone(), + "four", + proposal_id, + ); + let proposal = query_proposal( + &app, + &proposal_module_address, + proposal_module_codehash, + proposal_id, + ); + assert_eq!(proposal.proposal.status, Status::Closed); +} + +#[test] +fn test_three_of_five_multisig_revoting() { + let mut app = App::default(); + let mut instantiate = get_default_non_token_dao_proposal_module_instantiate(&mut app); + instantiate.threshold = Threshold::AbsoluteCount { + threshold: Uint128::new(3), + }; + instantiate.allow_revoting = true; + instantiate.pre_propose_info = PreProposeInfo::AnyoneMayPropose {}; + let core_contract_info = instantiate_with_cw4_groups_governance( + &mut app, + instantiate, + Some(vec![ + InitialBalance { + address: "one".to_string(), + amount: Uint128::new(1), + }, + InitialBalance { + address: "two".to_string(), + amount: Uint128::new(1), + }, + InitialBalance { + address: "three".to_string(), + amount: Uint128::new(1), + }, + InitialBalance { + address: "four".to_string(), + amount: Uint128::new(1), + }, + InitialBalance { + address: "five".to_string(), + amount: Uint128::new(1), + }, + ]), + ); + + let core_state: dao_interface::query::DumpStateResponse = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::DumpState {}, ) .unwrap(); + let proposal_module_address = core_state + .proposal_modules + .clone() + .into_iter() + .next() + .unwrap() + .address; - CommonTest { - app, - proposal_single_contract_info, - } -} + let proposal_module_codehash = core_state + .proposal_modules + .into_iter() + .next() + .unwrap() + .code_hash; -#[test] -fn test_simple_instantiate_proposal() { - let CommonTest { - app: _, - proposal_single_contract_info: _, - } = setup_test(DAO_ADDR); -} + let query_auth_info: AnyContractInfo = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::QueryAuthInfo {}, + ) + .unwrap(); -// this will fail currently as custom function parse_reply_get_contract_address from event is not woring -// for testing and we can't create a normal dao due to it. -#[test] -#[should_panic( - expected = "Generic error: Querier contract error: secret_multi_test::multi::wasm::ContractData not found" -)] -fn test_create_proposal_without_voting_module_will_fail() { - let CommonTest { - mut app, - proposal_single_contract_info, - } = setup_test(DAO_ADDR); + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); - let query_auth = instantiate_query_auth(&mut app); - let viewing_key_creator = - create_viewing_key(&mut app, query_auth.clone(), mock_info(CREATOR_ADDR, &[])); + let viewing_key_one = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info("one", &[]), + ); - // Create Proposal - make_proposal( + let viewing_key_two = create_viewing_key( &mut app, - &proposal_single_contract_info.address, - proposal_single_contract_info.code_hash, - Auth::ViewingKey { - key: viewing_key_creator, - address: CREATOR_ADDR.to_string(), + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), }, - vec![], + mock_info("two", &[]), ); -} -#[test] -fn test_vote_on_proposal_with_invalid_proposal_id_will_fail() { - let CommonTest { - mut app, - proposal_single_contract_info, - } = setup_test(DAO_ADDR); + let viewing_key_three = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info("three", &[]), + ); - let query_auth = instantiate_query_auth(&mut app); - let viewing_key_creator = - create_viewing_key(&mut app, query_auth.clone(), mock_info(CREATOR_ADDR, &[])); + let proposal_id = make_proposal( + &mut app, + &proposal_module_address, + proposal_module_codehash.clone(), + CREATOR_ADDR, + Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + vec![], + ); - // vote on Proposal will fail - vote_on_proposal_should_fail( + vote_on_proposal( &mut app, - &proposal_single_contract_info.address, - proposal_single_contract_info.code_hash, + &proposal_module_address, + proposal_module_codehash.clone(), + "one", + proposal_id, Auth::ViewingKey { - key: viewing_key_creator, - address: CREATOR_ADDR.to_string(), + key: viewing_key_one.clone(), + address: "one".into(), + }, + Vote::Yes, + ); + vote_on_proposal( + &mut app, + &proposal_module_address, + proposal_module_codehash.clone(), + "two", + proposal_id, + Auth::ViewingKey { + key: viewing_key_two.clone(), + address: "two".into(), }, - 1, Vote::Yes, ); -} -#[test] -#[should_panic(expected = "no vote exists for proposal (1) and voter (creator)")] -fn test_update_rational_fails_for_no_proposal() { - let CommonTest { - mut app, - proposal_single_contract_info, - } = setup_test(DAO_ADDR); + // Make sure it doesn't pass early. + let proposal: ProposalResponse = query_proposal( + &app, + &proposal_module_address, + proposal_module_codehash.clone(), + proposal_id, + ); + assert_eq!(proposal.proposal.status, Status::Open); - // update rational fais for no proposal - update_rationale( + vote_on_proposal( &mut app, - &proposal_single_contract_info.address, - proposal_single_contract_info.code_hash, - CREATOR_ADDR, - 1, - Some("new_rational".into()), + &proposal_module_address, + proposal_module_codehash.clone(), + "three", + proposal_id, + Auth::ViewingKey { + key: viewing_key_three.clone(), + address: "three".into(), + }, + Vote::Yes, ); -} -#[test] -fn execute_fails_on_proposal_with_invalid_proposal() { - let CommonTest { - mut app, - proposal_single_contract_info, - } = setup_test(DAO_ADDR); + // Revoting is enabled so the proposal is still open. + let proposal: ProposalResponse = query_proposal( + &app, + &proposal_module_address, + proposal_module_codehash.clone(), + proposal_id, + ); + assert_eq!(proposal.proposal.status, Status::Open); - let query_auth = instantiate_query_auth(&mut app); - let viewing_key_creator = - create_viewing_key(&mut app, query_auth.clone(), mock_info(CREATOR_ADDR, &[])); + // Change our minds. + vote_on_proposal( + &mut app, + &proposal_module_address, + proposal_module_codehash.clone(), + "one", + proposal_id, + Auth::ViewingKey { + key: viewing_key_one.clone(), + address: "one".into(), + }, + Vote::No, + ); + vote_on_proposal( + &mut app, + &proposal_module_address, + proposal_module_codehash.clone(), + "two", + proposal_id, + Auth::ViewingKey { + key: viewing_key_two.clone(), + address: "two".into(), + }, + Vote::No, + ); - // execute on Proposal will fail - execute_proposal_should_fail( + let err = vote_on_proposal_should_fail( &mut app, - &proposal_single_contract_info.address, - proposal_single_contract_info.code_hash, + &proposal_module_address, + proposal_module_codehash.clone(), + "two", Auth::ViewingKey { - key: viewing_key_creator, - address: CREATOR_ADDR.to_string(), + key: viewing_key_two.clone(), + address: "two".into(), }, - 1, + proposal_id, + Vote::No, + ); + assert!(matches!(err, ContractError::AlreadyCast {})); + + // Expire the revoting proposal and close it. + app.update_block(|b| b.time = b.time.plus_seconds(604800)); + let proposal: ProposalResponse = query_proposal( + &app, + &proposal_module_address, + proposal_module_codehash, + proposal_id, ); + assert_eq!(proposal.proposal.status, Status::Rejected); } +/// Tests that absolute count style thresholds work with token style +/// voting. #[test] -fn execute_veto_fails_on_proposal_with_invalid_proposal() { - let CommonTest { - mut app, - proposal_single_contract_info, - } = setup_test(DAO_ADDR); - - // veto on Proposal will fail - execute_veto_fails( - &mut app, - &proposal_single_contract_info.address, - proposal_single_contract_info.code_hash, - CREATOR_ADDR, - 1, +fn test_absolute_count_threshold_non_multisig() { + do_votes_staked_balances( + vec![ + TestSingleChoiceVote { + voter: "one".to_string(), + position: Vote::Yes, + weight: Uint128::new(10), + should_execute: ShouldExecute::Yes, + }, + TestSingleChoiceVote { + voter: "two".to_string(), + position: Vote::No, + weight: Uint128::new(200), + should_execute: ShouldExecute::Yes, + }, + TestSingleChoiceVote { + voter: "three".to_string(), + position: Vote::Yes, + weight: Uint128::new(1), + should_execute: ShouldExecute::Yes, + }, + ], + Threshold::AbsoluteCount { + threshold: Uint128::new(11), + }, + Status::Passed, + None, ); } +/// Tests that we do not overflow when faced with really high token / +/// vote supply. #[test] -fn close_proposal_fails_on_proposal_with_invalid_proposal() { - let CommonTest { - mut app, - proposal_single_contract_info, - } = setup_test(DAO_ADDR); +fn test_large_absolute_count_threshold() { + do_votes_staked_balances( + vec![ + TestSingleChoiceVote { + voter: "two".to_string(), + position: Vote::No, + weight: Uint128::new(1), + should_execute: ShouldExecute::Yes, + }, + // Can vote up to expiration time. + TestSingleChoiceVote { + voter: "one".to_string(), + position: Vote::Yes, + weight: Uint128::new(u128::MAX - 1), + should_execute: ShouldExecute::Yes, + }, + ], + Threshold::AbsoluteCount { + threshold: Uint128::new(u128::MAX), + }, + Status::Rejected, + None, + ); - // close on Proposal will fail - close_proposal_should_fail( - &mut app, - &proposal_single_contract_info.address, - proposal_single_contract_info.code_hash, - CREATOR_ADDR, - 1, + do_votes_staked_balances( + vec![ + TestSingleChoiceVote { + voter: "one".to_string(), + position: Vote::Yes, + weight: Uint128::new(u128::MAX - 1), + should_execute: ShouldExecute::Yes, + }, + TestSingleChoiceVote { + voter: "two".to_string(), + position: Vote::No, + weight: Uint128::new(1), + should_execute: ShouldExecute::Yes, + }, + ], + Threshold::AbsoluteCount { + threshold: Uint128::new(u128::MAX), + }, + Status::Rejected, + None, ); } #[test] -fn update_config_works() { - let CommonTest { - mut app, - proposal_single_contract_info, - } = setup_test(DAO_ADDR); - - // update config - update_config( +fn test_proposal_count_initialized_to_zero() { + let mut app = App::default(); + let pre_propose_info = get_pre_propose_info(&mut app, None, false); + let core_contract_info = instantiate_with_staked_balances_governance( &mut app, - &proposal_single_contract_info.address, - proposal_single_contract_info.code_hash, - DAO_ADDR, + InstantiateMsg { + veto: None, + threshold: Threshold::ThresholdQuorum { + threshold: PercentageThreshold::Majority {}, + quorum: PercentageThreshold::Percent(Decimal::percent(10)), + }, + max_voting_period: Duration::Height(10), + min_voting_period: None, + only_members_execute: true, + allow_revoting: false, + pre_propose_info, + close_proposal_on_execution_failure: true, + dao_code_hash: "todo!()".into(), + query_auth: None, + }, + Some(vec![ + InitialBalance { + address: "ekez".to_string(), + amount: Uint128::new(10), + }, + InitialBalance { + address: "innactive".to_string(), + amount: Uint128::new(90), + }, + ]), ); + + let core_state: dao_interface::query::DumpStateResponse = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::DumpState {}, + ) + .unwrap(); + let proposal_modules = core_state.proposal_modules; + + assert_eq!(proposal_modules.len(), 1); + let proposal_single_addr = proposal_modules.clone().into_iter().next().unwrap().address; + let proposal_single_codehash = proposal_modules + .clone() + .into_iter() + .next() + .unwrap() + .code_hash; + + let proposal_count: u64 = app + .wrap() + .query_wasm_smart( + proposal_single_codehash, + proposal_single_addr, + &QueryMsg::ProposalCount {}, + ) + .unwrap(); + assert_eq!(proposal_count, 0); } #[test] -fn update_config_fails_for_invalid_sender() { - let CommonTest { - mut app, - proposal_single_contract_info, - } = setup_test(DAO_ADDR); +pub fn test_migrate_updates_version() { + let mut deps = mock_dependencies(); + secret_cw2::set_contract_version(&mut deps.storage, "my-contract", "1.0.0").unwrap(); + migrate(deps.as_mut(), mock_env(), MigrateMsg {}).unwrap(); + let version = secret_cw2::get_contract_version(&deps.storage).unwrap(); + assert_eq!(version.version, CONTRACT_VERSION); + assert_eq!(version.contract, CONTRACT_NAME); +} - // update config fails - update_config_should_fail( - &mut app, - &proposal_single_contract_info.address, - proposal_single_contract_info.code_hash, - CREATOR_ADDR, +#[test] +fn test_proposal_too_large() { + let mut app = App::default(); + let mut instantiate = get_default_token_dao_proposal_module_instantiate(&mut app); + instantiate.pre_propose_info = PreProposeInfo::AnyoneMayPropose {}; + let core_contract_info = + instantiate_with_staked_balances_governance(&mut app, instantiate, None); + let proposal_module = query_single_proposal_module( + &app, + &core_contract_info.address, + core_contract_info.code_hash, ); + + let err = app + .execute_contract( + Addr::unchecked(CREATOR_ADDR), + &ContractInfo { + address: proposal_module.addr, + code_hash: proposal_module.code_hash, + }, + &ExecuteMsg::Propose(ProposeMsg { + title: "".to_string(), + description: "a".repeat(MAX_PROPOSAL_SIZE as usize), + msgs: vec![], + proposer: None, + }), + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + + assert!(matches!( + err, + ContractError::ProposalTooLarge { + size: _, + max: MAX_PROPOSAL_SIZE + } + )) } #[test] -fn update_pre_propose_info_works() { +fn test_vote_not_registered() { let CommonTest { mut app, - proposal_single_contract_info, - } = setup_test(DAO_ADDR); + core_contract_info: _, + proposal_module, + gov_token_info: _, + proposal_id, + query_auth_info, + } = setup_test(vec![]); + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr, + code_hash: query_auth_info.code_hash, + }, + mock_info("ekez", &[]), + ); - // update pre-propose - update_pre_propose_info( + let err = vote_on_proposal_should_fail( &mut app, - &proposal_single_contract_info.address, - proposal_single_contract_info.code_hash, - DAO_ADDR, + &proposal_module.addr, + proposal_module.code_hash, + "ekez", + Auth::ViewingKey { + key: viewing_key, + address: "ekez".into(), + }, + proposal_id, + Vote::Yes, ); + assert!(matches!(err, ContractError::NotRegistered {})) } #[test] -fn update_pre_propose_info_fails_for_invalid_sender() { +fn test_proposal_creation_permissions() { let CommonTest { mut app, - proposal_single_contract_info, - } = setup_test(DAO_ADDR); + core_contract_info, + proposal_module, + gov_token_info: _, + proposal_id: _, + query_auth_info, + } = setup_test(vec![]); + + // Non pre-propose may not propose. + let err = app + .execute_contract( + Addr::unchecked("notprepropose"), + &ContractInfo { + address: proposal_module.addr.clone(), + code_hash: proposal_module.code_hash.clone(), + }, + &ExecuteMsg::Propose(ProposeMsg { + title: "title".to_string(), + description: "description".to_string(), + msgs: vec![], + proposer: None, + }), + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert!(matches!(err, ContractError::Unauthorized {})); + + let proposal_creation_policy = query_creation_policy( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + ); + let pre_propose = match proposal_creation_policy { + ProposalCreationPolicy::Anyone {} => panic!("expected a pre-propose module"), + ProposalCreationPolicy::Module { addr, code_hash: _ } => addr, + }; + + // Proposer may not be none when a pre-propose module is making + // the proposal. + let err = app + .execute_contract( + pre_propose, + &ContractInfo { + address: proposal_module.addr.clone(), + code_hash: proposal_module.code_hash.clone(), + }, + &ExecuteMsg::Propose(ProposeMsg { + title: "title".to_string(), + description: "description".to_string(), + msgs: vec![], + proposer: None, + }), + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert!(matches!(err, ContractError::InvalidProposer {})); + + // Allow anyone to propose. + app.execute_contract( + core_contract_info.address.clone(), + &ContractInfo { + address: proposal_module.addr.clone(), + code_hash: proposal_module.code_hash.clone(), + }, + &ExecuteMsg::UpdatePreProposeInfo { + info: PreProposeInfo::AnyoneMayPropose {}, + }, + &[], + ) + .unwrap(); + + // Proposer must be None when non pre-propose module is making the + // proposal. + let err = app + .execute_contract( + Addr::unchecked("ekez"), + &ContractInfo { + address: proposal_module.addr.clone(), + code_hash: proposal_module.code_hash.clone(), + }, + &ExecuteMsg::Propose(ProposeMsg { + title: "title".to_string(), + description: "description".to_string(), + msgs: vec![], + proposer: Some("ekez".to_string()), + }), + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert!(matches!(err, ContractError::InvalidProposer {})); + + let viewing_key_ekez = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info("ekez", &[]), + ); + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); - // update pre-propose fails - update_pre_propose_info_should_fail( + // Works normally. + let proposal_id = make_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + "ekez", + Auth::ViewingKey { + key: viewing_key_ekez, + address: "ekez".into(), + }, + vec![], + ); + let proposal = query_proposal( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + proposal_id, + ); + assert_eq!(proposal.proposal.proposer, Addr::unchecked("ekez")); + vote_on_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + proposal_id, + Auth::ViewingKey { + key: viewing_key, + address: CREATOR_ADDR.into(), + }, + Vote::No, + ); + close_proposal( &mut app, - &proposal_single_contract_info.address, - proposal_single_contract_info.code_hash, + &proposal_module.addr, + proposal_module.code_hash, CREATOR_ADDR, + proposal_id, ); } #[test] -fn add_proposal_hook_works() { +fn test_query_info() { let CommonTest { - mut app, - proposal_single_contract_info, - } = setup_test(DAO_ADDR); - - // add proposal hook - add_proposal_hook( - &mut app, - &proposal_single_contract_info.address, - proposal_single_contract_info.code_hash, - DAO_ADDR, - "hook_addr", - "hook_code_hash", - ); + app, + core_contract_info: _, + proposal_module, + gov_token_info: _, + proposal_id: _, + .. + } = setup_test(vec![]); + let info: InfoResponse = app + .wrap() + .query_wasm_smart( + proposal_module.code_hash, + proposal_module.addr, + &QueryMsg::Info {}, + ) + .unwrap(); + assert_eq!( + info, + InfoResponse { + info: ContractVersion { + contract: CONTRACT_NAME.to_string(), + version: CONTRACT_VERSION.to_string() + } + } + ) } +/// DAO should be admin of the pre-propose contract despite the fact +/// that the proposal module instantiates it. #[test] -fn add_proposal_hook_fails_for_invalid_sender() { +fn test_pre_propose_admin_is_dao() { let CommonTest { - mut app, - proposal_single_contract_info, - } = setup_test(DAO_ADDR); + app, + proposal_module, + gov_token_info: _, + proposal_id: _, + .. + } = setup_test(vec![]); - // add proposal hook fails - add_proposal_hook_should_fail( - &mut app, - &proposal_single_contract_info.address, - proposal_single_contract_info.code_hash, - CREATOR_ADDR, - "hook_addr", - "hook_code_hash", + let proposal_creation_policy = query_creation_policy( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), ); + + // Check that a new creation policy has been birthed. + let pre_propose: AnyContractInfo = match proposal_creation_policy { + ProposalCreationPolicy::Anyone {} => panic!("expected a pre-propose module"), + ProposalCreationPolicy::Module { addr, code_hash } => AnyContractInfo { addr, code_hash }, + }; + + let info: ContractInfoResponse = app + .wrap() + .query(&cosmwasm_std::QueryRequest::Wasm(WasmQuery::ContractInfo { + contract_addr: pre_propose.addr.into_string(), + })) + .unwrap(); + assert_eq!(info.creator, proposal_module.addr.into_string()); } +// I can add a rationale to my vote. My rational is queryable when +// listing votes. I can later change my rationale. #[test] -fn remove_proposal_hook_works() { +fn test_rationale() { let CommonTest { mut app, - proposal_single_contract_info, - } = setup_test(DAO_ADDR); + proposal_module, + proposal_id, + query_auth_info, + .. + } = setup_test(vec![]); - // add proposal hook - add_proposal_hook( + let viewing_key = create_viewing_key( &mut app, - &proposal_single_contract_info.address, - proposal_single_contract_info.clone().code_hash, - DAO_ADDR, - "hook_addr", - "hook_code_hash", + ContractInfo { + address: query_auth_info.addr, + code_hash: query_auth_info.code_hash, + }, + mock_info(CREATOR_ADDR, &[]), ); - // remove proposal hook - remove_proposal_hook( + let rationale = Some("i support dog charities".to_string()); + + vote_on_proposal_with_rationale( &mut app, - &proposal_single_contract_info.address, - proposal_single_contract_info.code_hash, - DAO_ADDR, - "hook_addr", - "hook_code_hash", + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + proposal_id, + Vote::Yes, + rationale.clone(), ); -} -#[test] -fn remove_proposal_hook_fails_for_invalid_sender() { - let CommonTest { - mut app, - proposal_single_contract_info, - } = setup_test(DAO_ADDR); + let vote = query_vote( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + proposal_id, + ); + assert_eq!(vote.vote.unwrap().rationale, rationale); - // remove proposal hook fails - remove_proposal_hook_should_fail( + let rationale_new = + Some("i did not realize that dog charity was gambling with customer funds".to_string()); + + update_rationale( &mut app, - &proposal_single_contract_info.address, - proposal_single_contract_info.code_hash, + &proposal_module.addr, + proposal_module.code_hash.clone(), CREATOR_ADDR, - "hook_addr", - "hook_code_hash", + proposal_id, + rationale_new.clone(), ); + + let vote = query_vote( + &app, + &proposal_module.addr, + proposal_module.code_hash, + Auth::ViewingKey { + key: viewing_key, + address: CREATOR_ADDR.into(), + }, + proposal_id, + ); + assert_eq!(vote.vote.unwrap().rationale, rationale); } +// Revoting should override any previous rationale. If no new +// rationalle is provided, the old one will be wiped regardless. #[test] -fn remove_proposal_hook_fails_for_no_proposal_hook() { - let CommonTest { - mut app, - proposal_single_contract_info, - } = setup_test(DAO_ADDR); +fn test_rational_clobbered_on_revote() { + let mut app = App::default(); + let mut instantiate = get_default_token_dao_proposal_module_instantiate(&mut app); + instantiate.allow_revoting = true; + let core_contract_info = + instantiate_with_staked_balances_governance(&mut app, instantiate, None); + let gov_token_info = query_dao_token( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + let proposal_module = query_single_proposal_module( + &app, + &core_contract_info.address, + core_contract_info.code_hash.clone(), + ); + + let query_auth_info: AnyContractInfo = app + .wrap() + .query_wasm_smart( + core_contract_info.code_hash.clone(), + core_contract_info.address.clone(), + &dao_interface::msg::QueryMsg::QueryAuthInfo {}, + ) + .unwrap(); - // remove proposal hook fails - remove_proposal_hook_should_fail( + let viewing_key = create_viewing_key( &mut app, - &proposal_single_contract_info.address, - proposal_single_contract_info.code_hash, - DAO_ADDR, - "hook_addr", - "hook_code_hash", + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), ); -} - -#[test] -fn add_vote_hook_works() { - let CommonTest { - mut app, - proposal_single_contract_info, - } = setup_test(DAO_ADDR); - // add vote hook - add_vote_hook( + mint_snip20s( &mut app, - &proposal_single_contract_info.address, - proposal_single_contract_info.code_hash, - DAO_ADDR, - "vote_hook_addr", - "vote_hook_code_hash", + &gov_token_info.addr, + gov_token_info.code_hash.clone(), + &core_contract_info.address, + CREATOR_ADDR, + 10_000_000, + ); + let proposal_id = make_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + vec![], ); -} -#[test] -fn add_vote_hook_fails_for_invalid_sender() { - let CommonTest { - mut app, - proposal_single_contract_info, - } = setup_test(DAO_ADDR); + let rationale = Some("to_string".to_string()); - // add vote hook fails - add_vote_hook_should_fail( + vote_on_proposal_with_rationale( &mut app, - &proposal_single_contract_info.address, - proposal_single_contract_info.code_hash, + &proposal_module.addr, + proposal_module.code_hash.clone(), CREATOR_ADDR, - "vote_hook_addr", - "vote_hook_code_hash", + Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + proposal_id, + Vote::Yes, + rationale.clone(), ); -} -#[test] -fn remove_vote_hook_works() { - let CommonTest { - mut app, - proposal_single_contract_info, - } = setup_test(DAO_ADDR); + let vote = query_vote( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + proposal_id, + ); + assert_eq!(vote.vote.unwrap().rationale, rationale); - // add vote hook - add_vote_hook( + let rationale = None; + + // revote and clobber. + vote_on_proposal_with_rationale( &mut app, - &proposal_single_contract_info.address, - proposal_single_contract_info.clone().code_hash, - DAO_ADDR, - "vote_hook_addr", - "vote_hook_code_hash", + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + proposal_id, + Vote::No, + None, ); - // remove vote hook - remove_vote_hook( - &mut app, - &proposal_single_contract_info.address, - proposal_single_contract_info.code_hash, - DAO_ADDR, - "vote_hook_addr", - "vote_hook_code_hash", + let vote = query_vote( + &app, + &proposal_module.addr, + proposal_module.code_hash, + Auth::ViewingKey { + key: viewing_key.clone(), + address: CREATOR_ADDR.into(), + }, + proposal_id, ); + assert_eq!(vote.vote.unwrap().rationale, rationale); } +// Casting votes is only allowed within the proposal expiration timeframe #[test] -fn remove_vote_hook_fails_for_invalid_sender() { +pub fn test_not_allow_voting_on_expired_proposal() { let CommonTest { mut app, - proposal_single_contract_info, - } = setup_test(DAO_ADDR); + core_contract_info: _, + proposal_module, + gov_token_info: _, + proposal_id, + query_auth_info, + } = setup_test(vec![]); - // remove vote hook fails - remove_vote_hook_should_fail( + let viewing_key = create_viewing_key( &mut app, - &proposal_single_contract_info.address, - proposal_single_contract_info.code_hash, - CREATOR_ADDR, - "vote_hook_addr", - "vote_hook_code_hash", + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + + // expire the proposal + app.update_block(|b| b.time = b.time.plus_seconds(604800)); + let proposal = query_proposal( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + proposal_id, + ); + assert_eq!(proposal.proposal.status, Status::Rejected); + assert_eq!(proposal.proposal.votes.yes, Uint128::zero()); + + // attempt to vote past the expiration date + let err: ContractError = app + .execute_contract( + Addr::unchecked(CREATOR_ADDR), + &ContractInfo { + address: proposal_module.addr.clone(), + code_hash: proposal_module.code_hash.clone(), + }, + &ExecuteMsg::Vote { + proposal_id, + vote: Vote::Yes, + rationale: None, + auth: Auth::ViewingKey { + key: viewing_key, + address: CREATOR_ADDR.into(), + }, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + + // assert the vote got rejected and did not count + // towards the votes + let proposal = query_proposal( + &app, + &proposal_module.addr, + proposal_module.code_hash, + proposal_id, ); + assert_eq!(proposal.proposal.status, Status::Rejected); + assert_eq!(proposal.proposal.votes.yes, Uint128::zero()); + assert!(matches!(err, ContractError::Expired { id: _proposal_id })); } #[test] -fn remove_vote_hook_fails_for_no_proposal_hook() { +fn test_proposal_count_goes_up() { let CommonTest { mut app, - proposal_single_contract_info, - } = setup_test(DAO_ADDR); + proposal_module, + gov_token_info, + core_contract_info, + query_auth_info, + .. + } = setup_test(vec![]); + + let next = query_next_proposal_id( + &app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + ); + assert_eq!(next, 2); - // remove vote hook fails - remove_vote_hook_should_fail( + let viewing_key = create_viewing_key( + &mut app, + ContractInfo { + address: query_auth_info.addr.clone(), + code_hash: query_auth_info.code_hash.clone(), + }, + mock_info(CREATOR_ADDR, &[]), + ); + mint_snip20s( &mut app, - &proposal_single_contract_info.address, - proposal_single_contract_info.code_hash, - DAO_ADDR, - "vote_hook_addr", - "vote_hook_code_hash", + &gov_token_info.addr, + gov_token_info.code_hash.clone(), + &core_contract_info.address, + CREATOR_ADDR, + 10_000_000, + ); + make_proposal( + &mut app, + &proposal_module.addr, + proposal_module.code_hash.clone(), + CREATOR_ADDR, + shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key, + address: CREATOR_ADDR.into(), + }, + vec![], ); + + let next = query_next_proposal_id(&app, &proposal_module.addr, proposal_module.code_hash); + assert_eq!(next, 3); } diff --git a/contracts/staking/snip20-stake-reward-distributor/src/snip20_msg.rs b/contracts/staking/snip20-stake-reward-distributor/src/snip20_msg.rs index ffd8820..a76fefe 100644 --- a/contracts/staking/snip20-stake-reward-distributor/src/snip20_msg.rs +++ b/contracts/staking/snip20-stake-reward-distributor/src/snip20_msg.rs @@ -1,17 +1,11 @@ #![allow(clippy::field_reassign_with_default)] // This is triggered in `#[derive(JsonSchema)]` -use cosmwasm_std::{Binary, Uint128}; +use cosmwasm_std::Binary; +use dao_interface::msg::InitialBalance; use schemars::JsonSchema; use secret_toolkit::utils::{HandleCallback, InitCallback}; use serde::{Deserialize, Serialize}; -#[cfg_attr(test, derive(Eq, PartialEq))] -#[derive(Serialize, Deserialize, Clone, JsonSchema)] -pub struct InitialBalance { - pub address: String, - pub amount: Uint128, -} - #[derive(Serialize, Deserialize, JsonSchema)] pub struct InstantiateMsg { pub name: String, diff --git a/contracts/test/dao-proposal-sudo/.gitignore b/contracts/test/dao-proposal-sudo/.gitignore new file mode 100644 index 0000000..dfdaaa6 --- /dev/null +++ b/contracts/test/dao-proposal-sudo/.gitignore @@ -0,0 +1,15 @@ +# Build results +/target + +# Cargo+Git helper file (https://github.com/rust-lang/cargo/blob/0.44.1/src/cargo/sources/git/utils.rs#L320-L327) +.cargo-ok + +# Text file backups +**/*.rs.bk + +# macOS +.DS_Store + +# IDEs +*.iml +.idea diff --git a/contracts/test/dao-proposal-sudo/Cargo.toml b/contracts/test/dao-proposal-sudo/Cargo.toml new file mode 100644 index 0000000..fc9f737 --- /dev/null +++ b/contracts/test/dao-proposal-sudo/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "dao-proposal-sudo" +authors = ["ekez "] +description = "A proposal module that allows direct execution without voting." +edition = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +version = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +secret-storage-plus = { workspace = true } +secret-cw2 = { workspace = true } +thiserror = { workspace = true } +dao-dao-macros = { workspace = true } +dao-interface = { workspace = true } +secret-toolkit ={ workspace = true } + +[dev-dependencies] +secret-multi-test = { workspace = true } diff --git a/contracts/test/dao-proposal-sudo/README b/contracts/test/dao-proposal-sudo/README new file mode 100644 index 0000000..43cfd55 --- /dev/null +++ b/contracts/test/dao-proposal-sudo/README @@ -0,0 +1,5 @@ +# dao-proposal-sudo + +A governance module for the cw-governance contract. Instantiated with +a root address. The root address may indiscriminately cause the module +to execute messages on the DAO. No other user may do this. diff --git a/contracts/test/dao-proposal-sudo/examples/schema.rs b/contracts/test/dao-proposal-sudo/examples/schema.rs new file mode 100644 index 0000000..ff6a305 --- /dev/null +++ b/contracts/test/dao-proposal-sudo/examples/schema.rs @@ -0,0 +1,25 @@ +use std::env::current_dir; +use std::fs::create_dir_all; + +use cosmwasm_schema::{export_schema, export_schema_with_title, remove_schemas, schema_for}; +use cosmwasm_std::Addr; +use dao_interface::voting::InfoResponse; +use dao_proposal_sudo::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + let mut out_dir = current_dir().unwrap(); + out_dir.push("schema"); + create_dir_all(&out_dir).unwrap(); + remove_schemas(&out_dir).unwrap(); + + export_schema(&schema_for!(InstantiateMsg), &out_dir); + export_schema(&schema_for!(ExecuteMsg), &out_dir); + export_schema(&schema_for!(QueryMsg), &out_dir); + + export_schema(&schema_for!(InfoResponse), &out_dir); + + // Auto TS code generation expects the query return type as QueryNameResponse + // Here we map query resonses to the correct name + export_schema_with_title(&schema_for!(Addr), &out_dir, "DaoResponse"); + export_schema_with_title(&schema_for!(Addr), &out_dir, "AdminResponse"); +} diff --git a/contracts/test/dao-proposal-sudo/schema/admin_response.json b/contracts/test/dao-proposal-sudo/schema/admin_response.json new file mode 100644 index 0000000..9209687 --- /dev/null +++ b/contracts/test/dao-proposal-sudo/schema/admin_response.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AdminResponse", + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" +} diff --git a/contracts/test/dao-proposal-sudo/schema/dao_response.json b/contracts/test/dao-proposal-sudo/schema/dao_response.json new file mode 100644 index 0000000..9518ba3 --- /dev/null +++ b/contracts/test/dao-proposal-sudo/schema/dao_response.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "DaoResponse", + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" +} diff --git a/contracts/test/dao-proposal-sudo/schema/execute_msg.json b/contracts/test/dao-proposal-sudo/schema/execute_msg.json new file mode 100644 index 0000000..302a22a --- /dev/null +++ b/contracts/test/dao-proposal-sudo/schema/execute_msg.json @@ -0,0 +1,487 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "execute" + ], + "properties": { + "execute": { + "type": "object", + "required": [ + "msgs" + ], + "properties": { + "msgs": { + "type": "array", + "items": { + "$ref": "#/definitions/CosmosMsg_for_Empty" + } + } + } + } + }, + "additionalProperties": false + } + ], + "definitions": { + "BankMsg": { + "description": "The message types of the bank module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/bank/v1beta1/tx.proto", + "oneOf": [ + { + "description": "Sends native tokens from the contract to the given address.\n\nThis is translated to a [MsgSend](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/bank/v1beta1/tx.proto#L19-L28). `from_address` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "send" + ], + "properties": { + "send": { + "type": "object", + "required": [ + "amount", + "to_address" + ], + "properties": { + "amount": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, + "to_address": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "This will burn the given coins from the contract's account. There is no Cosmos SDK message that performs this, but it can be done by calling the bank keeper. Important if a contract controls significant token supply that must be retired.", + "type": "object", + "required": [ + "burn" + ], + "properties": { + "burn": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + } + } + } + }, + "additionalProperties": false + } + ] + }, + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "CosmosMsg_for_Empty": { + "oneOf": [ + { + "type": "object", + "required": [ + "bank" + ], + "properties": { + "bank": { + "$ref": "#/definitions/BankMsg" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "custom" + ], + "properties": { + "custom": { + "$ref": "#/definitions/Empty" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "staking" + ], + "properties": { + "staking": { + "$ref": "#/definitions/StakingMsg" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "distribution" + ], + "properties": { + "distribution": { + "$ref": "#/definitions/DistributionMsg" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "wasm" + ], + "properties": { + "wasm": { + "$ref": "#/definitions/WasmMsg" + } + }, + "additionalProperties": false + } + ] + }, + "DistributionMsg": { + "description": "The message types of the distribution module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto", + "oneOf": [ + { + "description": "This is translated to a [MsgSetWithdrawAddress](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#L29-L37). `delegator_address` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "set_withdraw_address" + ], + "properties": { + "set_withdraw_address": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "description": "The `withdraw_address`", + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "This is translated to a [[MsgWithdrawDelegatorReward](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#L42-L50). `delegator_address` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "withdraw_delegator_reward" + ], + "properties": { + "withdraw_delegator_reward": { + "type": "object", + "required": [ + "validator" + ], + "properties": { + "validator": { + "description": "The `validator_address`", + "type": "string" + } + } + } + }, + "additionalProperties": false + } + ] + }, + "Empty": { + "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", + "type": "object" + }, + "StakingMsg": { + "description": "The message types of the staking module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto", + "oneOf": [ + { + "description": "This is translated to a [MsgDelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L81-L90). `delegator_address` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "delegate" + ], + "properties": { + "delegate": { + "type": "object", + "required": [ + "amount", + "validator" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Coin" + }, + "validator": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "This is translated to a [MsgUndelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L112-L121). `delegator_address` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "undelegate" + ], + "properties": { + "undelegate": { + "type": "object", + "required": [ + "amount", + "validator" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Coin" + }, + "validator": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "This is translated to a [MsgBeginRedelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L95-L105). `delegator_address` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "redelegate" + ], + "properties": { + "redelegate": { + "type": "object", + "required": [ + "amount", + "dst_validator", + "src_validator" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Coin" + }, + "dst_validator": { + "type": "string" + }, + "src_validator": { + "type": "string" + } + } + } + }, + "additionalProperties": false + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "WasmMsg": { + "description": "The message types of the wasm module.\n\nSee https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto", + "oneOf": [ + { + "description": "Dispatches a call to another contract at a known address (with known ABI).\n\nThis is translated to a [MsgExecuteContract](https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto#L68-L78). `sender` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "execute" + ], + "properties": { + "execute": { + "type": "object", + "required": [ + "contract_addr", + "funds", + "msg" + ], + "properties": { + "contract_addr": { + "type": "string" + }, + "funds": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, + "msg": { + "description": "msg is the json-encoded ExecuteMsg struct (as raw Binary)", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Instantiates a new contracts from previously uploaded Wasm code.\n\nThis is translated to a [MsgInstantiateContract](https://github.com/CosmWasm/wasmd/blob/v0.16.0-alpha1/x/wasm/internal/types/tx.proto#L47-L61). `sender` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "instantiate" + ], + "properties": { + "instantiate": { + "type": "object", + "required": [ + "code_id", + "funds", + "label", + "msg" + ], + "properties": { + "admin": { + "type": [ + "string", + "null" + ] + }, + "code_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "funds": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, + "label": { + "description": "A human-readbale label for the contract", + "type": "string" + }, + "msg": { + "description": "msg is the JSON-encoded InstantiateMsg struct (as raw Binary)", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Migrates a given contracts to use new wasm code. Passes a MigrateMsg to allow us to customize behavior.\n\nOnly the contract admin (as defined in wasmd), if any, is able to make this call.\n\nThis is translated to a [MsgMigrateContract](https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto#L86-L96). `sender` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "migrate" + ], + "properties": { + "migrate": { + "type": "object", + "required": [ + "contract_addr", + "msg", + "new_code_id" + ], + "properties": { + "contract_addr": { + "type": "string" + }, + "msg": { + "description": "msg is the json-encoded MigrateMsg struct that will be passed to the new code", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "new_code_id": { + "description": "the code_id of the new logic to place in the given contract", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Sets a new admin (for migrate) on the given contract. Fails if this contract is not currently admin of the target contract.", + "type": "object", + "required": [ + "update_admin" + ], + "properties": { + "update_admin": { + "type": "object", + "required": [ + "admin", + "contract_addr" + ], + "properties": { + "admin": { + "type": "string" + }, + "contract_addr": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Clears the admin on the given contract, so no more migration possible. Fails if this contract is not currently admin of the target contract.", + "type": "object", + "required": [ + "clear_admin" + ], + "properties": { + "clear_admin": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "type": "string" + } + } + } + }, + "additionalProperties": false + } + ] + } + } +} diff --git a/contracts/test/dao-proposal-sudo/schema/info_response.json b/contracts/test/dao-proposal-sudo/schema/info_response.json new file mode 100644 index 0000000..a051676 --- /dev/null +++ b/contracts/test/dao-proposal-sudo/schema/info_response.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InfoResponse", + "type": "object", + "required": [ + "info" + ], + "properties": { + "info": { + "$ref": "#/definitions/ContractVersion" + } + }, + "definitions": { + "ContractVersion": { + "type": "object", + "required": [ + "contract", + "version" + ], + "properties": { + "contract": { + "description": "contract is the crate name of the implementing contract, eg. `crate:cw20-base` we will use other prefixes for other languages, and their standard global namespacing", + "type": "string" + }, + "version": { + "description": "version is any string that this implementation knows. It may be simple counter \"1\", \"2\". or semantic version on release tags \"v0.7.0\", or some custom feature flag list. the only code that needs to understand the version parsing is code that knows how to migrate from the given contract (and is tied to it's implementation somehow)", + "type": "string" + } + } + } + } +} diff --git a/contracts/test/dao-proposal-sudo/schema/instantiate_msg.json b/contracts/test/dao-proposal-sudo/schema/instantiate_msg.json new file mode 100644 index 0000000..8e3416d --- /dev/null +++ b/contracts/test/dao-proposal-sudo/schema/instantiate_msg.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "root" + ], + "properties": { + "root": { + "type": "string" + } + } +} diff --git a/contracts/test/dao-proposal-sudo/schema/query_msg.json b/contracts/test/dao-proposal-sudo/schema/query_msg.json new file mode 100644 index 0000000..5262b42 --- /dev/null +++ b/contracts/test/dao-proposal-sudo/schema/query_msg.json @@ -0,0 +1,42 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "admin" + ], + "properties": { + "admin": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "dao" + ], + "properties": { + "dao": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "info" + ], + "properties": { + "info": { + "type": "object" + } + }, + "additionalProperties": false + } + ] +} diff --git a/contracts/test/dao-proposal-sudo/src/contract.rs b/contracts/test/dao-proposal-sudo/src/contract.rs new file mode 100644 index 0000000..bde9d22 --- /dev/null +++ b/contracts/test/dao-proposal-sudo/src/contract.rs @@ -0,0 +1,99 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + to_binary, Addr, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Response, StdResult, + WasmMsg, +}; +use dao_interface::state::AnyContractInfo; +use secret_cw2::set_contract_version; + +use crate::{ + error::ContractError, + msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, + state::{DAO, ROOT}, +}; + +const CONTRACT_NAME: &str = "crates.io:cw-govmod-sudo"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + let root = deps.api.addr_validate(&msg.root)?; + ROOT.save(deps.storage, &root)?; + DAO.save( + deps.storage, + &AnyContractInfo { + addr: info.sender, + code_hash: msg.dao_code_hash, + }, + )?; + + Ok(Response::new() + .add_attribute("method", "instantiate") + .add_attribute("root", root)) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::Execute { msgs } => execute_execute(deps.as_ref(), info.sender, msgs), + } +} + +pub fn execute_execute( + deps: Deps, + sender: Addr, + msgs: Vec, +) -> Result { + let root = ROOT.load(deps.storage)?; + let dao = DAO.load(deps.storage)?; + + if sender != root { + return Err(ContractError::Unauthorized {}); + } + + let msg = WasmMsg::Execute { + contract_addr: dao.addr.to_string(), + code_hash: dao.code_hash, + msg: to_binary(&dao_interface::msg::ExecuteMsg::ExecuteProposalHook { msgs })?, + funds: vec![], + }; + + Ok(Response::default() + .add_attribute("action", "execute_execute") + .add_message(msg)) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Admin {} => query_admin(deps), + QueryMsg::Dao {} => query_dao(deps), + QueryMsg::Info {} => query_info(deps), + } +} + +pub fn query_admin(deps: Deps) -> StdResult { + to_binary(&ROOT.load(deps.storage)?) +} + +pub fn query_dao(deps: Deps) -> StdResult { + to_binary(&DAO.load(deps.storage)?) +} + +pub fn query_info(deps: Deps) -> StdResult { + let info = secret_cw2::get_contract_version(deps.storage)?; + to_binary(&dao_interface::voting::InfoResponse { info }) +} diff --git a/contracts/test/dao-proposal-sudo/src/error.rs b/contracts/test/dao-proposal-sudo/src/error.rs new file mode 100644 index 0000000..dc19f10 --- /dev/null +++ b/contracts/test/dao-proposal-sudo/src/error.rs @@ -0,0 +1,11 @@ +use cosmwasm_std::StdError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Unauthorized")] + Unauthorized {}, +} diff --git a/contracts/test/dao-proposal-sudo/src/lib.rs b/contracts/test/dao-proposal-sudo/src/lib.rs new file mode 100644 index 0000000..dfedc9d --- /dev/null +++ b/contracts/test/dao-proposal-sudo/src/lib.rs @@ -0,0 +1,6 @@ +pub mod contract; +mod error; +pub mod msg; +pub mod state; + +pub use crate::error::ContractError; diff --git a/contracts/test/dao-proposal-sudo/src/msg.rs b/contracts/test/dao-proposal-sudo/src/msg.rs new file mode 100644 index 0000000..bd1b0bd --- /dev/null +++ b/contracts/test/dao-proposal-sudo/src/msg.rs @@ -0,0 +1,29 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::CosmosMsg; +use secret_toolkit::utils::InitCallback; + +#[cw_serde] +pub struct InstantiateMsg { + pub root: String, + pub dao_code_hash: String, +} + +impl InitCallback for InstantiateMsg { + const BLOCK_SIZE: usize = 256; +} + +#[cw_serde] +pub enum ExecuteMsg { + Execute { msgs: Vec }, +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(cosmwasm_std::Addr)] + Admin {}, + #[returns(cosmwasm_std::Addr)] + Dao {}, + #[returns(dao_interface::voting::InfoResponse)] + Info {}, +} diff --git a/contracts/test/dao-proposal-sudo/src/state.rs b/contracts/test/dao-proposal-sudo/src/state.rs new file mode 100644 index 0000000..7c7b51a --- /dev/null +++ b/contracts/test/dao-proposal-sudo/src/state.rs @@ -0,0 +1,6 @@ +use cosmwasm_std::Addr; +use dao_interface::state::AnyContractInfo; +use secret_storage_plus::Item; + +pub const ROOT: Item = Item::new("root"); +pub const DAO: Item = Item::new("dao"); diff --git a/contracts/test/dao-voting-snip20-balance/.gitignore b/contracts/test/dao-voting-snip20-balance/.gitignore new file mode 100644 index 0000000..dfdaaa6 --- /dev/null +++ b/contracts/test/dao-voting-snip20-balance/.gitignore @@ -0,0 +1,15 @@ +# Build results +/target + +# Cargo+Git helper file (https://github.com/rust-lang/cargo/blob/0.44.1/src/cargo/sources/git/utils.rs#L320-L327) +.cargo-ok + +# Text file backups +**/*.rs.bk + +# macOS +.DS_Store + +# IDEs +*.iml +.idea diff --git a/contracts/test/dao-voting-snip20-balance/Cargo.toml b/contracts/test/dao-voting-snip20-balance/Cargo.toml new file mode 100644 index 0000000..f219215 --- /dev/null +++ b/contracts/test/dao-voting-snip20-balance/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "dao-voting-snip20-balance" +authors = ["ekez "] +description = "A DAO DAO test contract." +edition = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +version = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +secret-storage-plus = { workspace = true } +secret-cw2 = { workspace = true } +secret-utils = { workspace = true } +thiserror = { workspace = true } +dao-dao-macros = { workspace = true } +dao-interface = { workspace = true } +snip20-reference-impl = { workspace = true} +shade-protocol ={ workspace = true } +secret-toolkit ={ workspace = true } + +[dev-dependencies] +secret-multi-test = { workspace = true } diff --git a/contracts/test/dao-voting-snip20-balance/README.md b/contracts/test/dao-voting-snip20-balance/README.md new file mode 100644 index 0000000..b1e3588 --- /dev/null +++ b/contracts/test/dao-voting-snip20-balance/README.md @@ -0,0 +1,4 @@ +# cw balance voting + +A simple voting power module which determines voting power based on +the token balance of specific addresses. diff --git a/contracts/test/dao-voting-snip20-balance/examples/schema.rs b/contracts/test/dao-voting-snip20-balance/examples/schema.rs new file mode 100644 index 0000000..d83093a --- /dev/null +++ b/contracts/test/dao-voting-snip20-balance/examples/schema.rs @@ -0,0 +1,24 @@ +use std::env::current_dir; +use std::fs::create_dir_all; + +use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; + +use dao_interface::voting::{ + InfoResponse, TotalPowerAtHeightResponse, VotingPowerAtHeightResponse, +}; +use dao_voting_snip20_balance::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + let mut out_dir = current_dir().unwrap(); + out_dir.push("schema"); + create_dir_all(&out_dir).unwrap(); + remove_schemas(&out_dir).unwrap(); + + export_schema(&schema_for!(InstantiateMsg), &out_dir); + export_schema(&schema_for!(ExecuteMsg), &out_dir); + export_schema(&schema_for!(QueryMsg), &out_dir); + + export_schema(&schema_for!(InfoResponse), &out_dir); + export_schema(&schema_for!(TotalPowerAtHeightResponse), &out_dir); + export_schema(&schema_for!(VotingPowerAtHeightResponse), &out_dir); +} diff --git a/contracts/test/dao-voting-snip20-balance/schema/execute_msg.json b/contracts/test/dao-voting-snip20-balance/schema/execute_msg.json new file mode 100644 index 0000000..b3d18b4 --- /dev/null +++ b/contracts/test/dao-voting-snip20-balance/schema/execute_msg.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "type": "string", + "enum": [] +} diff --git a/contracts/test/dao-voting-snip20-balance/schema/info_response.json b/contracts/test/dao-voting-snip20-balance/schema/info_response.json new file mode 100644 index 0000000..a051676 --- /dev/null +++ b/contracts/test/dao-voting-snip20-balance/schema/info_response.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InfoResponse", + "type": "object", + "required": [ + "info" + ], + "properties": { + "info": { + "$ref": "#/definitions/ContractVersion" + } + }, + "definitions": { + "ContractVersion": { + "type": "object", + "required": [ + "contract", + "version" + ], + "properties": { + "contract": { + "description": "contract is the crate name of the implementing contract, eg. `crate:cw20-base` we will use other prefixes for other languages, and their standard global namespacing", + "type": "string" + }, + "version": { + "description": "version is any string that this implementation knows. It may be simple counter \"1\", \"2\". or semantic version on release tags \"v0.7.0\", or some custom feature flag list. the only code that needs to understand the version parsing is code that knows how to migrate from the given contract (and is tied to it's implementation somehow)", + "type": "string" + } + } + } + } +} diff --git a/contracts/test/dao-voting-snip20-balance/schema/instantiate_msg.json b/contracts/test/dao-voting-snip20-balance/schema/instantiate_msg.json new file mode 100644 index 0000000..5922028 --- /dev/null +++ b/contracts/test/dao-voting-snip20-balance/schema/instantiate_msg.json @@ -0,0 +1,214 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "token_info" + ], + "properties": { + "token_info": { + "$ref": "#/definitions/TokenInfo" + } + }, + "definitions": { + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec", + "type": "string" + }, + "Cw20Coin": { + "type": "object", + "required": [ + "address", + "amount" + ], + "properties": { + "address": { + "type": "string" + }, + "amount": { + "$ref": "#/definitions/Uint128" + } + } + }, + "EmbeddedLogo": { + "description": "This is used to store the logo on the blockchain in an accepted format. Enforce maximum size of 5KB on all variants.", + "oneOf": [ + { + "description": "Store the Logo as an SVG file. The content must conform to the spec at https://en.wikipedia.org/wiki/Scalable_Vector_Graphics (The contract should do some light-weight sanity-check validation)", + "type": "object", + "required": [ + "svg" + ], + "properties": { + "svg": { + "$ref": "#/definitions/Binary" + } + }, + "additionalProperties": false + }, + { + "description": "Store the Logo as a PNG file. This will likely only support up to 64x64 or so within the 5KB limit.", + "type": "object", + "required": [ + "png" + ], + "properties": { + "png": { + "$ref": "#/definitions/Binary" + } + }, + "additionalProperties": false + } + ] + }, + "InstantiateMarketingInfo": { + "type": "object", + "properties": { + "description": { + "type": [ + "string", + "null" + ] + }, + "logo": { + "anyOf": [ + { + "$ref": "#/definitions/Logo" + }, + { + "type": "null" + } + ] + }, + "marketing": { + "type": [ + "string", + "null" + ] + }, + "project": { + "type": [ + "string", + "null" + ] + } + } + }, + "Logo": { + "description": "This is used for uploading logo data, or setting it in InstantiateData", + "oneOf": [ + { + "description": "A reference to an externally hosted logo. Must be a valid HTTP or HTTPS URL.", + "type": "object", + "required": [ + "url" + ], + "properties": { + "url": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "description": "Logo content stored on the blockchain. Enforce maximum size of 5KB on all variants", + "type": "object", + "required": [ + "embedded" + ], + "properties": { + "embedded": { + "$ref": "#/definitions/EmbeddedLogo" + } + }, + "additionalProperties": false + } + ] + }, + "TokenInfo": { + "oneOf": [ + { + "type": "object", + "required": [ + "existing" + ], + "properties": { + "existing": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "new" + ], + "properties": { + "new": { + "type": "object", + "required": [ + "code_id", + "decimals", + "initial_balances", + "label", + "name", + "symbol" + ], + "properties": { + "code_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "decimals": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "initial_balances": { + "type": "array", + "items": { + "$ref": "#/definitions/Cw20Coin" + } + }, + "label": { + "type": "string" + }, + "marketing": { + "anyOf": [ + { + "$ref": "#/definitions/InstantiateMarketingInfo" + }, + { + "type": "null" + } + ] + }, + "name": { + "type": "string" + }, + "symbol": { + "type": "string" + } + } + } + }, + "additionalProperties": false + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/test/dao-voting-snip20-balance/schema/query_msg.json b/contracts/test/dao-voting-snip20-balance/schema/query_msg.json new file mode 100644 index 0000000..fdde1e9 --- /dev/null +++ b/contracts/test/dao-voting-snip20-balance/schema/query_msg.json @@ -0,0 +1,80 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "token_contract" + ], + "properties": { + "token_contract": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "voting_power_at_height" + ], + "properties": { + "voting_power_at_height": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + }, + "height": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "total_power_at_height" + ], + "properties": { + "total_power_at_height": { + "type": "object", + "properties": { + "height": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "info" + ], + "properties": { + "info": { + "type": "object" + } + }, + "additionalProperties": false + } + ] +} diff --git a/contracts/test/dao-voting-snip20-balance/schema/total_power_at_height_response.json b/contracts/test/dao-voting-snip20-balance/schema/total_power_at_height_response.json new file mode 100644 index 0000000..8018462 --- /dev/null +++ b/contracts/test/dao-voting-snip20-balance/schema/total_power_at_height_response.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "TotalPowerAtHeightResponse", + "type": "object", + "required": [ + "height", + "power" + ], + "properties": { + "height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "power": { + "$ref": "#/definitions/Uint128" + } + }, + "definitions": { + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/test/dao-voting-snip20-balance/schema/voting_power_at_height_response.json b/contracts/test/dao-voting-snip20-balance/schema/voting_power_at_height_response.json new file mode 100644 index 0000000..15e986b --- /dev/null +++ b/contracts/test/dao-voting-snip20-balance/schema/voting_power_at_height_response.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "VotingPowerAtHeightResponse", + "type": "object", + "required": [ + "height", + "power" + ], + "properties": { + "height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "power": { + "$ref": "#/definitions/Uint128" + } + }, + "definitions": { + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/test/dao-voting-snip20-balance/src/contract.rs b/contracts/test/dao-voting-snip20-balance/src/contract.rs new file mode 100644 index 0000000..fdee66c --- /dev/null +++ b/contracts/test/dao-voting-snip20-balance/src/contract.rs @@ -0,0 +1,211 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + to_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdResult, SubMsg, + SubMsgResult, Uint128, +}; +use dao_interface::replies::parse_reply_address_from_event; +use dao_interface::state::AnyContractInfo; +use secret_cw2::set_contract_version; +use snip20_reference_impl::msg::QueryAnswer; + +use crate::error::ContractError; +use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg, TokenInfo}; +use crate::state::{DAO, TOKEN}; +use secret_toolkit::utils::InitCallback; + +const CONTRACT_NAME: &str = "crates.io:cw20-balance-voting"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +const INSTANTIATE_TOKEN_REPLY_ID: u64 = 0; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + DAO.save( + deps.storage, + &AnyContractInfo { + addr: info.sender.clone(), + code_hash: msg.dao_code_hash, + }, + )?; + + match msg.token_info { + TokenInfo::Existing { address, code_hash } => { + let address = deps.api.addr_validate(&address)?; + TOKEN.save( + deps.storage, + &AnyContractInfo { + addr: address.clone(), + code_hash, + }, + )?; + Ok(Response::default() + .add_attribute("action", "instantiate") + .add_attribute("token", "existing_token") + .add_attribute("token_address", address)) + } + TokenInfo::New { + code_id, + code_hash, + label, + name, + symbol, + decimals, + initial_balances, + } => { + let initial_supply = initial_balances + .iter() + .fold(Uint128::zero(), |p, n| p + n.amount); + if initial_supply.is_zero() { + return Err(ContractError::InitialBalancesError {}); + } + + let init_msg = snip20_reference_impl::msg::InstantiateMsg { + name, + symbol, + decimals, + initial_balances: Some(initial_balances), + admin: None, + prng_seed: to_binary("seed")?, + config: None, + supported_denoms: None, + }; + let sub_msg = SubMsg::reply_on_success( + init_msg.to_cosmos_msg(None, label, code_id, code_hash.clone(), None)?, + INSTANTIATE_TOKEN_REPLY_ID, + ); + + TOKEN.save( + deps.storage, + &AnyContractInfo { + addr: Addr::unchecked(""), + code_hash, + }, + )?; + + Ok(Response::default() + .add_attribute("action", "instantiate") + .add_attribute("token", "new_token") + .add_submessage(sub_msg)) + } + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + _deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg {} +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::TokenContract {} => query_token_contract(deps), + QueryMsg::VotingPowerAtHeight { auth, height: _ } => { + let mut viewing_key = String::new(); + let mut addr = String::new(); + match auth { + shade_protocol::basic_staking::Auth::ViewingKey { key, address } => { + viewing_key = key; + addr = address; + } + shade_protocol::basic_staking::Auth::Permit(_) => (), + }; + query_voting_power_at_height(deps, env, addr, viewing_key) + } + QueryMsg::TotalPowerAtHeight { height: _ } => query_total_power_at_height(deps, env), + QueryMsg::Info {} => query_info(deps), + QueryMsg::Dao {} => query_dao(deps), + } +} + +pub fn query_dao(deps: Deps) -> StdResult { + let dao = DAO.load(deps.storage)?; + to_binary(&dao) +} + +pub fn query_token_contract(deps: Deps) -> StdResult { + let token = TOKEN.load(deps.storage)?; + to_binary(&token) +} + +pub fn query_voting_power_at_height( + deps: Deps, + env: Env, + address: String, + key: String, +) -> StdResult { + let token = TOKEN.load(deps.storage)?; + let address = deps.api.addr_validate(&address)?; + let mut balance_amount = Uint128::zero(); + let balance: snip20_reference_impl::msg::QueryAnswer = deps.querier.query_wasm_smart( + token.code_hash, + token.addr, + &snip20_reference_impl::msg::QueryMsg::Balance { + address: address.to_string(), + key, + }, + )?; + if let QueryAnswer::Balance { amount } = balance { + balance_amount = amount + } + println!("balance : {}", balance_amount); + to_binary(&dao_interface::voting::VotingPowerAtHeightResponse { + power: balance_amount, + height: env.block.height, + }) +} + +pub fn query_total_power_at_height(deps: Deps, env: Env) -> StdResult { + let token = TOKEN.load(deps.storage)?; + let mut supply = Uint128::zero(); + let info: snip20_reference_impl::msg::QueryAnswer = deps.querier.query_wasm_smart( + token.code_hash, + token.addr, + &snip20_reference_impl::msg::QueryMsg::TokenInfo {}, + )?; + if let QueryAnswer::TokenInfo { total_supply, .. } = info { + supply = total_supply.unwrap_or_default(); + } + to_binary(&dao_interface::voting::TotalPowerAtHeightResponse { + power: supply, + height: env.block.height, + }) +} + +pub fn query_info(deps: Deps) -> StdResult { + let info = secret_cw2::get_contract_version(deps.storage)?; + to_binary(&dao_interface::voting::InfoResponse { info }) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result { + match msg.id { + INSTANTIATE_TOKEN_REPLY_ID => match msg.result { + SubMsgResult::Ok(sub_msg_response) => { + let mut token_info = TOKEN.load(deps.storage)?; + if token_info.addr != Addr::unchecked("") { + return Err(ContractError::DuplicateToken {}); + } + + let token = parse_reply_address_from_event(sub_msg_response); + token_info.addr = deps.api.addr_validate(&token)?; + TOKEN.save(deps.storage, &token_info)?; + Ok(Response::default().add_attribute("token_address", token)) + } + SubMsgResult::Err(_) => Err(ContractError::TokenInstantiateError {}), + }, + _ => Err(ContractError::UnknownReplyId { id: msg.id }), + } +} diff --git a/contracts/test/dao-voting-snip20-balance/src/error.rs b/contracts/test/dao-voting-snip20-balance/src/error.rs new file mode 100644 index 0000000..dfa7335 --- /dev/null +++ b/contracts/test/dao-voting-snip20-balance/src/error.rs @@ -0,0 +1,23 @@ +use cosmwasm_std::StdError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("Initial governance token balances must not be empty")] + InitialBalancesError {}, + + #[error("Can not change the contract's token after it has been set")] + DuplicateToken {}, + + #[error("Error instantiating token")] + TokenInstantiateError {}, + + #[error("Got a submessage reply with unknown id: {id}")] + UnknownReplyId { id: u64 }, +} diff --git a/contracts/test/dao-voting-snip20-balance/src/lib.rs b/contracts/test/dao-voting-snip20-balance/src/lib.rs new file mode 100644 index 0000000..8e58a2f --- /dev/null +++ b/contracts/test/dao-voting-snip20-balance/src/lib.rs @@ -0,0 +1,9 @@ +pub mod contract; +mod error; +pub mod msg; +pub mod state; + +#[cfg(test)] +mod tests; + +pub use crate::error::ContractError; diff --git a/contracts/test/dao-voting-snip20-balance/src/msg.rs b/contracts/test/dao-voting-snip20-balance/src/msg.rs new file mode 100644 index 0000000..9773004 --- /dev/null +++ b/contracts/test/dao-voting-snip20-balance/src/msg.rs @@ -0,0 +1,39 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use dao_dao_macros::{cw20_token_query, voting_module_query}; +use secret_toolkit::utils::InitCallback; + +#[cw_serde] +pub enum TokenInfo { + Existing { + address: String, + code_hash: String, + }, + New { + code_id: u64, + code_hash: String, + label: String, + name: String, + symbol: String, + decimals: u8, + initial_balances: Vec, + }, +} + +#[cw_serde] +pub struct InstantiateMsg { + pub token_info: TokenInfo, + pub dao_code_hash: String, +} + +impl InitCallback for InstantiateMsg { + const BLOCK_SIZE: usize = 256; +} + +#[cw_serde] +pub enum ExecuteMsg {} + +#[cw20_token_query] +#[voting_module_query] +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg {} diff --git a/contracts/test/dao-voting-snip20-balance/src/state.rs b/contracts/test/dao-voting-snip20-balance/src/state.rs new file mode 100644 index 0000000..069091f --- /dev/null +++ b/contracts/test/dao-voting-snip20-balance/src/state.rs @@ -0,0 +1,5 @@ +use dao_interface::state::AnyContractInfo; +use secret_storage_plus::Item; + +pub const DAO: Item = Item::new("dao"); +pub const TOKEN: Item = Item::new("token"); diff --git a/contracts/test/dao-voting-snip20-balance/src/tests.rs b/contracts/test/dao-voting-snip20-balance/src/tests.rs new file mode 100644 index 0000000..8ab0e5e --- /dev/null +++ b/contracts/test/dao-voting-snip20-balance/src/tests.rs @@ -0,0 +1,140 @@ +use cosmwasm_std::{Addr, ContractInfo, Empty, Uint128}; +use dao_interface::voting::InfoResponse; +use secret_cw2::ContractVersion; +use secret_multi_test::{App, Contract, ContractInstantiationInfo, ContractWrapper, Executor}; +use snip20_reference_impl::msg::InitialBalance; + +use crate::msg::{InstantiateMsg, QueryMsg}; + +const DAO_ADDR: &str = "dao"; +const CREATOR_ADDR: &str = "creator"; + +fn snip20_contract() -> Box> { + let contract = ContractWrapper::new( + snip20_reference_impl::contract::execute, + snip20_reference_impl::contract::instantiate, + snip20_reference_impl::contract::query, + ); + Box::new(contract) +} + +fn balance_voting_contract() -> Box> { + let contract = ContractWrapper::new( + crate::contract::execute, + crate::contract::instantiate, + crate::contract::query, + ) + .with_reply(crate::contract::reply); + Box::new(contract) +} + +fn instantiate_voting( + app: &mut App, + voting_info: ContractInstantiationInfo, + msg: InstantiateMsg, +) -> ContractInfo { + app.instantiate_contract( + voting_info, + Addr::unchecked(DAO_ADDR), + &msg, + &[], + "voting module", + None, + ) + .unwrap() +} + +#[test] +#[should_panic(expected = "Initial governance token balances must not be empty")] +fn test_instantiate_zero_supply() { + let mut app = App::default(); + let snip20_info = app.store_code(snip20_contract()); + let voting_info = app.store_code(balance_voting_contract()); + instantiate_voting( + &mut app, + voting_info, + InstantiateMsg { + token_info: crate::msg::TokenInfo::New { + code_id: snip20_info.code_id, + code_hash: snip20_info.code_hash, + label: "DAO DAO voting".to_string(), + name: "DAO DAO".to_string(), + symbol: "DAO".to_string(), + decimals: 6, + initial_balances: vec![InitialBalance { + address: CREATOR_ADDR.to_string(), + amount: Uint128::zero(), + }], + }, + dao_code_hash: "dao_code_hash".to_string(), + }, + ); +} + +#[test] +#[should_panic(expected = "Initial governance token balances must not be empty")] +fn test_instantiate_no_balances() { + let mut app = App::default(); + let snip20_info = app.store_code(snip20_contract()); + let voting_info = app.store_code(balance_voting_contract()); + instantiate_voting( + &mut app, + voting_info, + InstantiateMsg { + token_info: crate::msg::TokenInfo::New { + code_id: snip20_info.code_id, + code_hash: snip20_info.code_hash, + label: "DAO DAO voting".to_string(), + name: "DAO DAO".to_string(), + symbol: "DAO".to_string(), + decimals: 6, + initial_balances: vec![], + }, + dao_code_hash: "dao_code_hash".to_string(), + }, + ); +} + +#[test] +fn test_contract_info() { + let mut app = App::default(); + let snip20_info = app.store_code(snip20_contract()); + let voting_info = app.store_code(balance_voting_contract()); + let voting_contract_info = instantiate_voting( + &mut app, + voting_info, + InstantiateMsg { + token_info: crate::msg::TokenInfo::New { + code_id: snip20_info.code_id, + code_hash: snip20_info.code_hash, + label: "DAO DAO voting".to_string(), + name: "DAO DAO".to_string(), + symbol: "DAO".to_string(), + decimals: 6, + initial_balances: vec![InitialBalance { + address: CREATOR_ADDR.to_string(), + amount: Uint128::from(2u64), + }], + }, + dao_code_hash: "dao_code_hash".to_string(), + }, + ); + + let info: InfoResponse = app + .wrap() + .query_wasm_smart( + voting_contract_info.code_hash, + voting_contract_info.address, + &QueryMsg::Info {}, + ) + .unwrap(); + assert_eq!( + info, + InfoResponse { + info: ContractVersion { + contract: "crates.io:cw20-balance-voting".to_string(), + version: env!("CARGO_PKG_VERSION").to_string() + } + } + ) +} diff --git a/contracts/voting/dao-voting-snip20-staked/src/contract.rs b/contracts/voting/dao-voting-snip20-staked/src/contract.rs index 23f21ed..4357927 100644 --- a/contracts/voting/dao-voting-snip20-staked/src/contract.rs +++ b/contracts/voting/dao-voting-snip20-staked/src/contract.rs @@ -1,5 +1,6 @@ use crate::error::ContractError; use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, Snip20TokenInfo, StakingInfo}; +use crate::snip20_msg::InitConfig; use crate::state::{ ACTIVE_THRESHOLD, DAO, QUERY_AUTH, STAKING_CONTRACT, STAKING_CONTRACT_CODE_HASH, STAKING_CONTRACT_CODE_ID, STAKING_CONTRACT_UNSTAKING_DURATION, TOKEN_CONTRACT, @@ -11,6 +12,7 @@ use cosmwasm_std::{ to_binary, Addr, Binary, Decimal, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdResult, SubMsg, SubMsgResult, Uint128, Uint256, }; +use dao_interface::msg::InitialBalance; use dao_interface::replies::parse_reply_address_from_event; use dao_interface::state::AnyContractInfo; use dao_interface::voting::IsActiveResponse; @@ -173,7 +175,7 @@ pub fn instantiate( // Add DAO initial balance to initial_balances vector if defined. if let Some(initial_dao_balance) = initial_dao_balance { if initial_dao_balance > Uint128::zero() { - let intitial_balance = snip20_msg::InitialBalance { + let intitial_balance = InitialBalance { address: info.sender.to_string(), amount: initial_dao_balance, }; @@ -192,7 +194,14 @@ pub fn instantiate( decimals, admin: Some(info.sender.clone().to_string()), prng_seed: to_binary(&"snip20")?, - config: None, + config: Some(InitConfig { + public_total_supply: Some(true), + enable_deposit: Some(true), + enable_redeem: Some(true), + enable_mint: Some(true), + enable_burn: Some(true), + can_modify_denoms: Some(true), + }), supported_denoms: None, initial_balances: Some(initial_balances), }; diff --git a/contracts/voting/dao-voting-snip20-staked/src/msg.rs b/contracts/voting/dao-voting-snip20-staked/src/msg.rs index 0d0b795..523a9b6 100644 --- a/contracts/voting/dao-voting-snip20-staked/src/msg.rs +++ b/contracts/voting/dao-voting-snip20-staked/src/msg.rs @@ -1,14 +1,13 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::Uint128; use dao_dao_macros::{active_query, cw20_token_query, voting_module_query}; +use dao_interface::msg::InitialBalance; use dao_voting::threshold::ActiveThreshold; use schemars::JsonSchema; use secret_utils::Duration; use serde::{Deserialize, Serialize}; use shade_protocol::utils::asset::RawContract; -use crate::snip20_msg::InitialBalance; - /// Information about the staking contract to be used with this voting /// module. #[cw_serde] diff --git a/contracts/voting/dao-voting-snip20-staked/src/snip20_msg.rs b/contracts/voting/dao-voting-snip20-staked/src/snip20_msg.rs index 880879a..5872aa4 100644 --- a/contracts/voting/dao-voting-snip20-staked/src/snip20_msg.rs +++ b/contracts/voting/dao-voting-snip20-staked/src/snip20_msg.rs @@ -1,17 +1,10 @@ #![allow(clippy::field_reassign_with_default)] // This is triggered in `#[derive(JsonSchema)]` - -use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Binary, Uint128}; +use cosmwasm_std::Binary; +use dao_interface::msg::InitialBalance; use schemars::JsonSchema; use secret_toolkit::utils::InitCallback; use serde::{Deserialize, Serialize}; -#[cw_serde] -pub struct InitialBalance { - pub address: String, - pub amount: Uint128, -} - #[derive(Serialize, Deserialize, JsonSchema)] pub struct InstantiateMsg { pub name: String, @@ -35,20 +28,20 @@ impl InitCallback for InstantiateMsg { pub struct InitConfig { /// Indicates whether the total supply is public or should be kept secret. /// default: False - public_total_supply: Option, + pub public_total_supply: Option, /// Indicates whether deposit functionality should be enabled /// default: False - enable_deposit: Option, + pub enable_deposit: Option, /// Indicates whether redeem functionality should be enabled /// default: False - enable_redeem: Option, + pub enable_redeem: Option, /// Indicates whether mint functionality should be enabled /// default: False - enable_mint: Option, + pub enable_mint: Option, /// Indicates whether burn functionality should be enabled /// default: False - enable_burn: Option, + pub enable_burn: Option, /// Indicated whether an admin can modify supported denoms /// default: False - can_modify_denoms: Option, + pub can_modify_denoms: Option, } diff --git a/contracts/voting/dao-voting-snip20-staked/src/tests.rs b/contracts/voting/dao-voting-snip20-staked/src/tests.rs index 5b921b2..fe1b27e 100644 --- a/contracts/voting/dao-voting-snip20-staked/src/tests.rs +++ b/contracts/voting/dao-voting-snip20-staked/src/tests.rs @@ -4,6 +4,7 @@ use cosmwasm_std::{ to_binary, Addr, ContractInfo, Decimal, Empty, MessageInfo, Uint128, }; use dao_interface::{ + msg::InitialBalance, state::AnyContractInfo, voting::{InfoResponse, IsActiveResponse, VotingPowerAtHeightResponse}, }; @@ -21,7 +22,6 @@ use snip20_reference_impl::msg::InitialBalance as Snip20InitialBalance; use crate::{ contract::{migrate, CONTRACT_NAME, CONTRACT_VERSION}, msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, StakingInfo}, - snip20_msg::InitialBalance, }; const DAO_ADDR: &str = "dao"; diff --git a/packages/dao-interface/src/msg.rs b/packages/dao-interface/src/msg.rs index 9493d3d..d146cef 100644 --- a/packages/dao-interface/src/msg.rs +++ b/packages/dao-interface/src/msg.rs @@ -1,5 +1,5 @@ -use crate::state::Config; -use crate::{migrate_msg::MigrateParams, query::SubDao, state::ModuleInstantiateInfo}; +use crate::state::{AnyContractInfo, Config}; +use crate::{query::SubDao, state::ModuleInstantiateInfo}; use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Binary, CosmosMsg, Empty, Uint128}; use schemars::JsonSchema; @@ -51,6 +51,8 @@ pub struct InstantiateMsg { pub query_auth_code_id: u64, pub query_auth_code_hash: String, pub prng_seed: String, + pub snip20_code_hash: String, + pub snip721_code_hash: String, } /// Snip20ReceiveMsg should be de/serialized under `Receive()` variant in a HandleMsg @@ -84,11 +86,13 @@ pub enum ExecuteMsg { /// Executed when the contract receives a cw721 token. Depending /// on the contract's configuration the contract will /// automatically add the token to its treasury. - ReceiveNft { - /// previous owner of sent token + BatchReceiveNft { + /// address that sent the tokens. There is no ReceiveNft field equivalent to this sender: Addr, - /// token that was sent - token_id: String, + /// previous owner of sent tokens. This is equivalent to the ReceiveNft `sender` field + from: Addr, + /// tokens that were sent + token_ids: Vec, /// optional message to control receiving logic msg: Option, }, @@ -173,21 +177,21 @@ pub enum QueryMsg { /// Gets the token balance for each cw20 registered with the /// contract. #[returns(Vec)] - Cw20Balances { + Snip20Balances { start_after: Option, limit: Option, }, /// Lists the addresses of the cw20 tokens in this contract's /// treasury. #[returns(Vec)] - Cw20TokenList { + Snip20TokenList { start_after: Option, limit: Option, }, /// Lists the addresses of the cw721 tokens in this contract's /// treasury. #[returns(Vec)] - Cw721TokenList { + Snip721TokenList { start_after: Option, limit: Option, }, @@ -251,18 +255,14 @@ pub enum QueryMsg { /// Returns the total voting power at a given block height. #[returns(crate::voting::TotalPowerAtHeightResponse)] TotalPowerAtHeight { height: Option }, + #[returns(AnyContractInfo)] + QueryAuthInfo {}, } #[allow(clippy::large_enum_variant)] #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] #[serde(rename_all = "snake_case")] -pub enum MigrateMsg { - FromV1 { - dao_uri: Option, - params: Option, - }, - FromCompatible {}, -} +pub struct MigrateMsg {} #[cw_serde] pub enum GroupContract { @@ -283,3 +283,9 @@ pub struct VotingCw4InstantiateMsg { pub group_contract: GroupContract, pub dao_code_hash: String, } + +#[cw_serde] +pub struct InitialBalance { + pub address: String, + pub amount: Uint128, +} diff --git a/packages/dao-utils/Cargo.toml b/packages/dao-utils/Cargo.toml index 8b0ce6e..e353634 100644 --- a/packages/dao-utils/Cargo.toml +++ b/packages/dao-utils/Cargo.toml @@ -25,6 +25,7 @@ cw4 ={ workspace = true } dao-snip721-extensions = { workspace = true } snip721-roles-impl ={ workspace = true } anybuf = { workspace = true } +dao-interface ={ workspace = true} [dev-dependencies] diff --git a/packages/dao-utils/src/msg.rs b/packages/dao-utils/src/msg.rs index e8d29d6..6458d36 100644 --- a/packages/dao-utils/src/msg.rs +++ b/packages/dao-utils/src/msg.rs @@ -1,5 +1,6 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{Binary, Uint128}; +use dao_interface::msg::InitialBalance; use dao_snip721_extensions::roles::MetadataExt; use dao_voting::threshold::ActiveThreshold; use dao_voting::threshold::PercentageThreshold; @@ -215,12 +216,6 @@ pub struct Snip20StakedInstantiateMsg { pub query_auth: Option, } -#[cw_serde] -pub struct InitialBalance { - pub address: String, - pub amount: Uint128, -} - impl InitCallback for Snip20StakedInstantiateMsg { const BLOCK_SIZE: usize = 256; } diff --git a/packages/dao-voting/src/pre_propose.rs b/packages/dao-voting/src/pre_propose.rs index 4a5a0f7..c97af44 100644 --- a/packages/dao-voting/src/pre_propose.rs +++ b/packages/dao-voting/src/pre_propose.rs @@ -1,12 +1,11 @@ //! Types related to the pre-propose module. Motivation: //! . -use cosmwasm_std::{Addr, Empty, StdResult, Storage, SubMsg}; +use crate::reply::pre_propose_module_instantiation_id; +use cosmwasm_std::{Addr, Empty, StdResult, SubMsg}; use dao_interface::state::ModuleInstantiateInfo; -use dao_interface::{ReplyEvent, ReplyIds}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; - #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] #[serde(rename_all = "snake_case")] pub enum PreProposeInfo { @@ -42,28 +41,21 @@ impl ProposalCreationPolicy { impl PreProposeInfo { pub fn into_initial_policy_and_messages( self, - store: &mut dyn Storage, dao: Addr, - reply_id: &ReplyIds, - ) -> StdResult<(ProposalCreationPolicy, Vec>)> { + ) -> StdResult<(ProposalCreationPolicy, Vec>, String)> { Ok(match self { - Self::AnyoneMayPropose {} => (ProposalCreationPolicy::Anyone {}, vec![]), + Self::AnyoneMayPropose {} => (ProposalCreationPolicy::Anyone {}, vec![], String::new()), Self::ModuleMayPropose { info } => { - let reply_id = reply_id.add_event( - store, - ReplyEvent::PreProposalModuleInstantiate { - code_hash: info.clone().code_hash, - }, - ); ( // Anyone can propose will be set until instantiation succeeds, then // `ModuleMayPropose` will be set. This ensures that we fail open // upon instantiation failure. ProposalCreationPolicy::Anyone {}, vec![SubMsg::reply_on_success( - info.to_cosmos_msg(dao), - reply_id.unwrap(), + info.clone().to_cosmos_msg(dao), + pre_propose_module_instantiation_id(), )], + info.code_hash, ) } })