diff --git a/.gitignore b/.gitignore index 896509b50..8a3fdd1cc 100644 --- a/.gitignore +++ b/.gitignore @@ -6,9 +6,12 @@ # Build results target/ -# this is moved to vm/testdata, others are present locally -contracts/hackatom/contract.wasm -contracts/hackatom/hash.txt + +# Testing configs +*.json + +# Code coverage stuff +*.profraw # IDEs .vscode/ diff --git a/.gitmodules b/.gitmodules index 14cc8eabc..c7b84edb4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,7 +2,3 @@ path = contracts/snip20 url = https://github.com/scrtlabs/snip20-reference-impl.git branch = master -[submodule "contracts/shd_staking"] - path = contracts/shd_staking - url = https://github.com/securesecrets/SPIP-STKN-0 - branch = staking-implementation diff --git a/contracts/governance/Cargo.toml b/contracts/governance/Cargo.toml index 53c810871..484d18890 100644 --- a/contracts/governance/Cargo.toml +++ b/contracts/governance/Cargo.toml @@ -29,7 +29,8 @@ cosmwasm-schema = "0.10.1" secret-toolkit = { version = "0.2" } cosmwasm-math-compat = { path = "../../packages/cosmwasm_math_compat" } shade-protocol = { version = "0.1.0", path = "../../packages/shade_protocol", features = [ - "governance", + "governance-impl", + "snip20_staking" ] } schemars = "0.7" serde = { version = "1.0.103", default-features = false, features = ["derive"] } @@ -39,3 +40,8 @@ snafu = { version = "0.6.3" } serde_json = { version = "1.0.67" } mockall = "0.10.2" mockall_double = "0.2.0" +fadroma-ensemble = { branch = "v100", git = "https://github.com/hackbg/fadroma.git" } +fadroma-platform-scrt = { branch = "v100", git = "https://github.com/hackbg/fadroma.git" } +contract_harness = { version = "0.1.0", path = "../../packages/contract_harness", features = [ "governance", "snip20_staking", "snip20" ] } +snip20-reference-impl = { version = "0.1.0", path = "../snip20" } +spip_stkd_0 = { version = "0.1.0", path = "../snip20_staking" } \ No newline at end of file diff --git a/contracts/governance/src/contract.rs b/contracts/governance/src/contract.rs index 388f64958..3292d1d40 100644 --- a/contracts/governance/src/contract.rs +++ b/contracts/governance/src/contract.rs @@ -1,8 +1,26 @@ use crate::{ - handle, - proposal_state::total_proposals_w, + handle::{ + assembly::{try_add_assembly, try_assembly_proposal, try_assembly_vote, try_set_assembly}, + assembly_msg::{ + try_add_assembly_msg, + try_add_assembly_msg_assemblies, + try_set_assembly_msg, + }, + contract::{try_add_contract, try_add_contract_assemblies, try_set_contract}, + profile::{try_add_profile, try_set_profile}, + proposal::{ + try_cancel, + try_claim_funding, + try_proposal, + try_receive, + try_receive_balance, + try_trigger, + try_update, + }, + try_set_config, + try_set_runtime_state, + }, query, - state::{admin_commands_list_w, config_w, supported_contracts_list_w}, }; use cosmwasm_math_compat::Uint128; use cosmwasm_std::{ @@ -14,47 +32,149 @@ use cosmwasm_std::{ HandleResponse, InitResponse, Querier, + StdError, StdResult, Storage, }; -use secret_toolkit::snip20::register_receive_msg; -use shade_protocol::contract_interfaces::governance::{Config, HandleMsg, InitMsg, QueryMsg}; +use secret_toolkit::{ + snip20::register_receive_msg, + utils::{pad_handle_result, pad_query_result}, +}; +use shade_protocol::{ + contract_interfaces::governance::{ + assembly::{Assembly, AssemblyMsg}, + contract::AllowedContract, + stored_id::ID, + Config, + HandleMsg, + InitMsg, + QueryMsg, + MSG_VARIABLE, + }, + utils::{ + asset::Contract, + flexible_msg::FlexibleMsg, + storage::default::{BucketStorage, SingletonStorage}, + }, +}; + +// Used to pad up responses for better privacy. +pub const RESPONSE_BLOCK_SIZE: usize = 256; pub fn init( deps: &mut Extern, env: Env, msg: InitMsg, ) -> StdResult { - let state = Config { - admin: match msg.admin { - None => env.message.sender.clone(), - Some(admin) => admin, - }, - staker: msg.staker, + // Setup config + Config { + treasury: msg.treasury.clone(), + vote_token: msg.vote_token.clone(), funding_token: msg.funding_token.clone(), - funding_amount: msg.funding_amount, - funding_deadline: msg.funding_deadline, - voting_deadline: msg.voting_deadline, - minimum_votes: msg.quorum, - }; + } + .save(&mut deps.storage)?; + + let mut messages = vec![]; + if let Some(vote_token) = msg.vote_token.clone() { + messages.push(register_receive_msg( + env.contract_code_hash.clone(), + None, + 255, + vote_token.code_hash, + vote_token.address, + )?); + } + if let Some(funding_token) = msg.funding_token.clone() { + messages.push(register_receive_msg( + env.contract_code_hash.clone(), + None, + 255, + funding_token.code_hash, + funding_token.address, + )?); + } + + // Setups IDs + ID::set_assembly(&mut deps.storage, Uint128::new(1))?; + ID::set_profile(&mut deps.storage, Uint128::new(1))?; + ID::set_assembly_msg(&mut deps.storage, Uint128::zero())?; + ID::set_contract(&mut deps.storage, Uint128::zero())?; + + // Setup public profile + msg.public_profile + .save(&mut deps.storage, &Uint128::zero())?; + + if msg.public_profile.funding.is_some() { + if msg.funding_token.is_none() { + return Err(StdError::generic_err("Funding token must be set")); + } + } + + if msg.public_profile.token.is_some() { + if msg.vote_token.is_none() { + return Err(StdError::generic_err("Voting token must be set")); + } + } + + // Setup public assembly + Assembly { + name: "public".to_string(), + metadata: "All inclusive assembly, acts like traditional governance".to_string(), + members: vec![], + profile: Uint128::zero(), + } + .save(&mut deps.storage, &Uint128::zero())?; + + // Setup admin profile + msg.admin_profile + .save(&mut deps.storage, &Uint128::new(1))?; - config_w(&mut deps.storage).save(&state)?; + if msg.admin_profile.funding.is_some() { + if msg.funding_token.is_none() { + return Err(StdError::generic_err("Funding token must be set")); + } + } + + if msg.admin_profile.token.is_some() { + if msg.vote_token.is_none() { + return Err(StdError::generic_err("Voting token must be set")); + } + } + + // Setup admin assembly + Assembly { + name: "admin".to_string(), + metadata: "Assembly of DAO admins.".to_string(), + members: msg.admin_members, + profile: Uint128::new(1), + } + .save(&mut deps.storage, &Uint128::new(1))?; - // Initialize total proposal counter - total_proposals_w(&mut deps.storage).save(&Uint128::zero())?; + // Setup generic command + AssemblyMsg { + name: "blank message".to_string(), + assemblies: vec![Uint128::zero(), Uint128::new(1)], + msg: FlexibleMsg { + msg: MSG_VARIABLE.to_string(), + arguments: 1, + }, + } + .save(&mut deps.storage, &Uint128::zero())?; - // Initialize lists - admin_commands_list_w(&mut deps.storage).save(&vec![])?; - supported_contracts_list_w(&mut deps.storage).save(&vec![])?; + // Setup self contract + AllowedContract { + name: "Governance".to_string(), + metadata: "Current governance contract, this one".to_string(), + assemblies: None, + contract: Contract { + address: env.contract.address, + code_hash: env.contract_code_hash, + }, + } + .save(&mut deps.storage, &Uint128::zero())?; Ok(InitResponse { - messages: vec![register_receive_msg( - env.contract_code_hash, - None, - 256, - msg.funding_token.code_hash, - msg.funding_token.address, - )?], + messages, log: vec![], }) } @@ -64,120 +184,170 @@ pub fn handle( env: Env, msg: HandleMsg, ) -> StdResult { - match msg { - // Proposals - HandleMsg::CreateProposal { - target_contract, - proposal, - description, - } => handle::try_create_proposal( - deps, - &env, - target_contract, - Binary::from(proposal.as_bytes()), - description, - ), - - HandleMsg::Receive { - sender, - amount, - msg, - } => handle::try_fund_proposal(deps, &env, sender, amount, msg), - - // Self interactions - // Config - HandleMsg::UpdateConfig { - admin, - staker, - proposal_deadline, - funding_amount, - funding_deadline, - minimum_votes, - } => handle::try_update_config( - deps, - &env, - admin, - staker, - proposal_deadline, - funding_amount, - funding_deadline, - minimum_votes, - ), - - HandleMsg::DisableStaker {} => handle::try_disable_staker(deps, &env), - - // Supported contract - HandleMsg::AddSupportedContract { name, contract } => { - handle::try_add_supported_contract(deps, &env, name, contract) - } + pad_handle_result( + match msg { + // State setups + HandleMsg::SetConfig { + treasury, + vote_token, + funding_token, + .. + } => try_set_config(deps, env, treasury, vote_token, funding_token), - HandleMsg::RemoveSupportedContract { name } => { - handle::try_remove_supported_contract(deps, &env, name) - } + // TODO: set this, must be discussed with team + HandleMsg::SetRuntimeState { state, .. } => try_set_runtime_state(deps, env, state), - HandleMsg::UpdateSupportedContract { name, contract } => { - handle::try_update_supported_contract(deps, &env, name, contract) - } + // Proposals + HandleMsg::Proposal { + title, + metadata, + contract, + msg, + coins, + .. + } => try_proposal(deps, env, title, metadata, contract, msg, coins), - // Admin command - HandleMsg::AddAdminCommand { name, proposal } => { - handle::try_add_admin_command(deps, &env, name, proposal) - } + HandleMsg::Trigger { proposal, .. } => try_trigger(deps, env, proposal), + HandleMsg::Cancel { proposal, .. } => try_cancel(deps, env, proposal), + HandleMsg::Update { proposal, .. } => try_update(deps, env, proposal), + HandleMsg::Receive { + sender, + from, + amount, + msg, + memo, + .. + } => try_receive(deps, env, sender, from, amount, msg, memo), + HandleMsg::ClaimFunding { id } => try_claim_funding(deps, env, id), - HandleMsg::RemoveAdminCommand { name } => { - handle::try_remove_admin_command(deps, &env, name) - } + HandleMsg::ReceiveBalance { + sender, + msg, + balance, + memo, + } => try_receive_balance(deps, env, sender, msg, balance, memo), - HandleMsg::UpdateAdminCommand { name, proposal } => { - handle::try_update_admin_command(deps, &env, name, proposal) - } + // Assemblies + HandleMsg::AssemblyVote { proposal, vote, .. } => { + try_assembly_vote(deps, env, proposal, vote) + } - // User interaction - HandleMsg::MakeVote { - voter, - proposal_id, - votes, - } => handle::try_vote(deps, &env, voter, proposal_id, votes), + HandleMsg::AssemblyProposal { + assembly, + title, + metadata, + msgs, + .. + } => try_assembly_proposal(deps, env, assembly, title, metadata, msgs), - HandleMsg::TriggerProposal { proposal_id } => { - handle::try_trigger_proposal(deps, &env, proposal_id) - } + HandleMsg::AddAssembly { + name, + metadata, + members, + profile, + .. + } => try_add_assembly(deps, env, name, metadata, members, profile), - // Admin interactions - HandleMsg::TriggerAdminCommand { - target, - command, - variables, - description, - } => handle::try_trigger_admin_command(deps, &env, target, command, variables, description), - } + HandleMsg::SetAssembly { + id, + name, + metadata, + members, + profile, + .. + } => try_set_assembly(deps, env, id, name, metadata, members, profile), + + // Assembly Msgs + HandleMsg::AddAssemblyMsg { + name, + msg, + assemblies, + .. + } => try_add_assembly_msg(deps, env, name, msg, assemblies), + + HandleMsg::SetAssemblyMsg { + id, + name, + msg, + assemblies, + .. + } => try_set_assembly_msg(deps, env, id, name, msg, assemblies), + + HandleMsg::AddAssemblyMsgAssemblies { id, assemblies } => { + try_add_assembly_msg_assemblies(deps, env, id, assemblies) + } + + // Profiles + HandleMsg::AddProfile { profile, .. } => try_add_profile(deps, env, profile), + + HandleMsg::SetProfile { id, profile, .. } => try_set_profile(deps, env, id, profile), + + // Contracts + HandleMsg::AddContract { + name, + metadata, + contract, + assemblies, + .. + } => try_add_contract(deps, env, name, metadata, contract, assemblies), + + HandleMsg::SetContract { + id, + name, + metadata, + contract, + disable_assemblies, + assemblies, + .. + } => try_set_contract( + deps, + env, + id, + name, + metadata, + contract, + disable_assemblies, + assemblies, + ), + + HandleMsg::AddContractAssemblies { id, assemblies } => { + try_add_contract_assemblies(deps, env, id, assemblies) + } + }, + RESPONSE_BLOCK_SIZE, + ) } pub fn query( deps: &Extern, msg: QueryMsg, ) -> StdResult { - match msg { - QueryMsg::GetProposals { start, end, status } => { - to_binary(&query::proposals(deps, start, end, status)?) - } + pad_query_result( + match msg { + QueryMsg::TotalProposals {} => to_binary(&query::total_proposals(deps)?), - QueryMsg::GetProposal { proposal_id } => to_binary(&query::proposal(deps, proposal_id)?), + QueryMsg::Proposals { start, end } => to_binary(&query::proposals(deps, start, end)?), - QueryMsg::GetTotalProposals {} => to_binary(&query::total_proposals(deps)?), + QueryMsg::TotalAssemblies {} => to_binary(&query::total_assemblies(deps)?), - QueryMsg::GetProposalVotes { proposal_id } => { - to_binary(&query::proposal_votes(deps, proposal_id)?) - } + QueryMsg::Assemblies { start, end } => to_binary(&query::assemblies(deps, start, end)?), - QueryMsg::GetSupportedContracts {} => to_binary(&query::supported_contracts(deps)?), + QueryMsg::TotalAssemblyMsgs {} => to_binary(&query::total_assembly_msgs(deps)?), - QueryMsg::GetSupportedContract { name } => { - to_binary(&query::supported_contract(deps, name)?) - } + QueryMsg::AssemblyMsgs { start, end } => { + to_binary(&query::assembly_msgs(deps, start, end)?) + } - QueryMsg::GetAdminCommands {} => to_binary(&query::admin_commands(deps)?), + QueryMsg::TotalProfiles {} => to_binary(&query::total_profiles(deps)?), - QueryMsg::GetAdminCommand { name } => to_binary(&query::admin_command(deps, name)?), - } + QueryMsg::Profiles { start, end } => to_binary(&query::profiles(deps, start, end)?), + + QueryMsg::TotalContracts {} => to_binary(&query::total_contracts(deps)?), + + QueryMsg::Contracts { start, end } => to_binary(&query::contracts(deps, start, end)?), + + QueryMsg::Config {} => to_binary(&query::config(deps)?), + }, + RESPONSE_BLOCK_SIZE, + ) } diff --git a/contracts/governance/src/handle.rs b/contracts/governance/src/handle.rs deleted file mode 100644 index fec2bab98..000000000 --- a/contracts/governance/src/handle.rs +++ /dev/null @@ -1,799 +0,0 @@ -use crate::{ - proposal_state::{ - proposal_funding_batch_w, - proposal_funding_deadline_r, - proposal_funding_deadline_w, - proposal_funding_r, - proposal_funding_w, - proposal_r, - proposal_run_status_w, - proposal_status_r, - proposal_status_w, - proposal_votes_r, - proposal_votes_w, - proposal_voting_deadline_r, - proposal_voting_deadline_w, - proposal_w, - total_proposal_votes_r, - total_proposal_votes_w, - total_proposals_w, - }, - state::{ - admin_commands_list_w, - admin_commands_r, - admin_commands_w, - config_r, - config_w, - supported_contract_r, - supported_contract_w, - supported_contracts_list_w, - }, -}; -use cosmwasm_math_compat::Uint128; -use cosmwasm_std::{ - from_binary, - to_binary, - Api, - Binary, - CosmosMsg, - Env, - Extern, - HandleResponse, - HumanAddr, - Querier, - StdError, - StdResult, - Storage, - WasmMsg, -}; -use secret_toolkit::snip20::{batch::SendAction, batch_send_msg, send_msg}; -use shade_protocol::{ - contract_interfaces::governance::{ - proposal::{Proposal, ProposalStatus}, - vote::VoteTally, - AdminCommand, - HandleAnswer, - ADMIN_COMMAND_VARIABLE, - GOVERNANCE_SELF, - }, - utils::{ - asset::Contract, - generic_response::{ - ResponseStatus, - ResponseStatus::{Failure, Success}, - }, - }, -}; - -pub fn create_proposal( - deps: &mut Extern, - env: &Env, - target_contract: String, - proposal: Binary, - description: String, -) -> StdResult { - // Check that the target contract is neither the governance or a supported contract - if supported_contract_r(&deps.storage) - .may_load(target_contract.as_bytes())? - .is_none() - && target_contract != *GOVERNANCE_SELF - { - return Err(StdError::NotFound { - kind: "contract is not found".to_string(), - backtrace: None, - }); - } - - // Create new proposal ID - let proposal_id = total_proposals_w(&mut deps.storage).update(|mut id| { - id += Uint128::new(1u128); - Ok(id) - })?; - - // Create proposal - let proposal = Proposal { - id: proposal_id, - target: target_contract, - msg: proposal, - description, - }; - - let config = config_r(&deps.storage).load()?; - - // Store the proposal - proposal_w(&mut deps.storage).save(proposal_id.to_string().as_bytes(), &proposal)?; - // Initialize deadline - proposal_funding_deadline_w(&mut deps.storage).save( - proposal_id.to_string().as_bytes(), - &(env.block.time + config.funding_deadline), - )?; - proposal_status_w(&mut deps.storage) - .save(proposal_id.to_string().as_bytes(), &ProposalStatus::Funding)?; - - // Initialize total funding - proposal_funding_w(&mut deps.storage) - .save(proposal_id.to_string().as_bytes(), &Uint128::zero())?; - // Initialize the funding batch - proposal_funding_batch_w(&mut deps.storage) - .save(proposal_id.to_string().as_bytes(), &vec![])?; - - // Create proposal votes - total_proposal_votes_w(&mut deps.storage).save( - proposal_id.to_string().as_bytes(), - &VoteTally { - yes: Uint128::zero(), - no: Uint128::zero(), - abstain: Uint128::zero(), - }, - )?; - - Ok(proposal_id) -} - -pub fn try_fund_proposal( - deps: &mut Extern, - env: &Env, - sender: HumanAddr, - amount: Uint128, - msg: Option, -) -> StdResult { - let proposal_id: Uint128 = - from_binary(&msg.ok_or_else(|| StdError::not_found("Proposal ID in msg"))?)?; - - // Check if proposal is in funding - let status = proposal_status_r(&deps.storage) - .may_load(proposal_id.to_string().as_bytes())? - .ok_or_else(|| StdError::not_found("Proposal"))?; - if status != ProposalStatus::Funding { - return Err(StdError::unauthorized()); - } - - let mut total = proposal_funding_r(&deps.storage).load(proposal_id.to_string().as_bytes())?; - - let config = config_r(&deps.storage).load()?; - let mut messages = vec![]; - - // Check if deadline is reached - if env.block.time - >= proposal_funding_deadline_r(&deps.storage).load(proposal_id.to_string().as_bytes())? - { - proposal_status_w(&mut deps.storage) - .save(proposal_id.to_string().as_bytes(), &ProposalStatus::Expired)?; - - // Send back amount - messages.push(send_msg( - sender, - amount.into(), - None, - None, - None, - 1, - config.funding_token.code_hash.clone(), - config.funding_token.address, - )?); - - // TODO: send total over to treasury - - return Ok(HandleResponse { - messages, - log: vec![], - data: Some(to_binary(&HandleAnswer::FundProposal { - status: Failure, - total_funding: total, - })?), - }); - } - - // Sum amount - total += amount; - - let mut adjusted_amount = amount; - - // return the excess - if total > config.funding_amount { - let excess = total.checked_sub(config.funding_amount)?; - adjusted_amount = adjusted_amount.checked_sub(excess)?; - // Set total to max - total = config.funding_amount; - - messages.push(send_msg( - sender.clone(), - excess.into(), - None, - None, - None, - 1, - config.funding_token.code_hash.clone(), - config.funding_token.address.clone(), - )?); - } - - // Update list of people that funded - let amounts = proposal_funding_batch_w(&mut deps.storage).update( - proposal_id.to_string().as_bytes(), - |amounts| { - if let Some(mut amounts) = amounts { - amounts.push(SendAction { - recipient: sender.clone(), - recipient_code_hash: None, - amount: adjusted_amount.into(), - msg: None, - memo: None, - }); - - return Ok(amounts); - } - - Err(StdError::not_found("Funding batch")) - }, - )?; - - // Update proposal status - if total == config.funding_amount { - // Update proposal status - proposal_status_w(&mut deps.storage) - .save(proposal_id.to_string().as_bytes(), &ProposalStatus::Voting)?; - // Set vote deadline - proposal_voting_deadline_w(&mut deps.storage).save( - proposal_id.to_string().as_bytes(), - &(env.block.time + config.voting_deadline), - )?; - - // Send back all of the invested prop amount - messages.push(batch_send_msg( - amounts, - None, - 1, - config.funding_token.code_hash, - config.funding_token.address, - )?) - } - - proposal_funding_w(&mut deps.storage).save(proposal_id.to_string().as_bytes(), &total)?; - - Ok(HandleResponse { - messages, - log: vec![], - data: Some(to_binary(&HandleAnswer::FundProposal { - status: Success, - total_funding: total, - })?), - }) -} - -pub fn try_trigger_proposal( - deps: &mut Extern, - env: &Env, - proposal_id: Uint128, -) -> StdResult { - // Get proposal - let proposal = proposal_r(&deps.storage).load(proposal_id.to_string().as_bytes())?; - let run_status: ResponseStatus; - let mut vote_status = - proposal_status_r(&deps.storage).load(proposal_id.to_string().as_bytes())?; - - // Check if proposal has run - // TODO: This might not be needed - // if proposal_run_status_r(&deps.storage).may_load(proposal_id.to_string().as_bytes())?.is_some() { - // return Err(StdError::generic_err("Proposal has already been executed")) - // } - - // Change proposal behavior according to stake availability - let config = config_r(&deps.storage).load()?; - vote_status = match config.staker { - Some(_) => { - // When staking is enabled funding is required - if vote_status != ProposalStatus::Voting { - return Err(StdError::unauthorized()); - } - - let total_votes = - total_proposal_votes_r(&deps.storage).load(proposal_id.to_string().as_bytes())?; - - // Check if proposal can be run - let voting_deadline = proposal_voting_deadline_r(&deps.storage) - .may_load(proposal_id.to_string().as_bytes())? - .ok_or_else(|| StdError::generic_err("No deadline set"))?; - if voting_deadline > env.block.time { - Err(StdError::unauthorized()) - } else if total_votes.yes + total_votes.no + total_votes.abstain < config.minimum_votes - { - Ok(ProposalStatus::Expired) - } else if total_votes.yes > total_votes.no { - Ok(ProposalStatus::Passed) - } else { - Ok(ProposalStatus::Rejected) - } - } - None => { - // Check if user is an admin in order to trigger the proposal - if config.admin == env.message.sender { - Ok(ProposalStatus::Passed) - } else { - Err(StdError::unauthorized()) - } - } - }?; - - let mut messages: Vec = vec![]; - - let target: Option; - if proposal.target == GOVERNANCE_SELF { - target = Some(Contract { - address: env.contract.address.clone(), - code_hash: env.contract_code_hash.clone(), - }) - } else { - target = supported_contract_r(&deps.storage).may_load(proposal.target.as_bytes())?; - } - - // Check if proposal passed or has a valid target contract - if vote_status != ProposalStatus::Passed { - run_status = Failure; - } else if let Some(target) = target { - run_status = match try_execute_msg(target, proposal.msg) { - Ok(msg) => { - messages.push(msg); - Success - } - Err(_) => Failure, - }; - } else { - run_status = Failure; - } - - // Overwrite - proposal_run_status_w(&mut deps.storage) - .save(proposal_id.to_string().as_bytes(), &run_status)?; - proposal_status_w(&mut deps.storage).save(proposal_id.to_string().as_bytes(), &vote_status)?; - - Ok(HandleResponse { - messages, - log: vec![], - data: Some(to_binary(&HandleAnswer::TriggerProposal { - status: run_status, - })?), - }) -} - -pub fn try_execute_msg(contract: Contract, msg: Binary) -> StdResult { - let execute = WasmMsg::Execute { - msg, - contract_addr: contract.address, - callback_code_hash: contract.code_hash, - send: vec![], - }; - Ok(execute.into()) -} - -pub fn try_vote( - deps: &mut Extern, - env: &Env, - voter: HumanAddr, - proposal_id: Uint128, - votes: VoteTally, -) -> StdResult { - // Check that sender is staking contract and staking is enabled - let config = config_r(&deps.storage).load()?; - if config.staker.is_none() || config.staker.unwrap().address != env.message.sender { - return Err(StdError::Unauthorized { backtrace: None }); - } - - // Check that proposal is votable - let vote_status = proposal_status_r(&deps.storage) - .may_load(proposal_id.to_string().as_bytes())? - .ok_or_else(|| StdError::not_found("Proposal"))?; - let voting_deadline = proposal_voting_deadline_r(&deps.storage) - .may_load(proposal_id.to_string().as_bytes())? - .ok_or_else(|| StdError::generic_err("No deadline set"))?; - - if vote_status != ProposalStatus::Voting || voting_deadline <= env.block.time { - return Err(StdError::unauthorized()); - } - - // Get proposal voting state - let mut proposal_voting_state = - total_proposal_votes_r(&deps.storage).load(proposal_id.to_string().as_bytes())?; - - // Check if user has already voted - match proposal_votes_r(&deps.storage, proposal_id).may_load(voter.to_string().as_bytes())? { - None => {} - Some(old_votes) => { - // Remove those votes from state - proposal_voting_state.yes = proposal_voting_state.yes.checked_sub(old_votes.yes)?; - proposal_voting_state.no = proposal_voting_state.no.checked_sub(old_votes.no)?; - proposal_voting_state.abstain = proposal_voting_state - .abstain - .checked_sub(old_votes.abstain)?; - } - } - - // Update state - proposal_voting_state.yes += votes.yes; - proposal_voting_state.no += votes.no; - proposal_voting_state.abstain += votes.abstain; - - // Save staker info - total_proposal_votes_w(&mut deps.storage) - .save(proposal_id.to_string().as_bytes(), &proposal_voting_state)?; - proposal_votes_w(&mut deps.storage, proposal_id).save(voter.to_string().as_bytes(), &votes)?; - - Ok(HandleResponse { - messages: vec![], - log: vec![], - data: Some(to_binary(&HandleAnswer::MakeVote { status: Success })?), - }) -} - -pub fn try_trigger_admin_command( - deps: &mut Extern, - env: &Env, - target: String, - command: String, - variables: Vec, - description: String, -) -> StdResult { - // Check that user is admin - if config_r(&deps.storage).load()?.admin != env.message.sender { - return Err(StdError::Unauthorized { backtrace: None }); - } - - // First validate that the contract exists - let target_contract = match supported_contract_r(&deps.storage).may_load(target.as_bytes())? { - None => { - return Err(StdError::NotFound { - kind: "Contract not found".to_string(), - backtrace: None, - }); - } - Some(contract) => contract, - }; - - // Check that command exists - let admin_command = match admin_commands_r(&deps.storage).may_load(command.as_bytes())? { - None => { - return Err(StdError::NotFound { - kind: "Command not found".to_string(), - backtrace: None, - }); - } - Some(admin_c) => admin_c, - }; - - // With command validate that number of variables is equal - if admin_command.total_arguments != variables.len() as u16 { - return Err(StdError::GenericErr { - msg: "Variable number doesnt match up".to_string(), - backtrace: None, - }); - } - - // Replace variable spaces - let mut finished_command = admin_command.msg; - for item in variables.iter() { - finished_command = finished_command.replacen(ADMIN_COMMAND_VARIABLE, item, 1); - } - - let mut messages = vec![]; - - // Create new proposal ID - let proposal_id = total_proposals_w(&mut deps.storage).update(|mut id| { - id += Uint128::new(1u128); - Ok(id) - })?; - - // Try to run - let proposal = Proposal { - id: proposal_id, - target, - msg: Binary::from(finished_command.as_bytes()), - description, - }; - - // Store the proposal - proposal_w(&mut deps.storage).save(proposal_id.to_string().as_bytes(), &proposal)?; - proposal_funding_deadline_w(&mut deps.storage) - .save(proposal_id.to_string().as_bytes(), &env.block.time)?; - proposal_voting_deadline_w(&mut deps.storage) - .save(proposal_id.to_string().as_bytes(), &env.block.time)?; - proposal_status_w(&mut deps.storage).save( - proposal_id.to_string().as_bytes(), - &ProposalStatus::AdminRequested, - )?; - let run_status = - match try_execute_msg(target_contract, Binary::from(finished_command.as_bytes())) { - Ok(executed_msg) => { - messages.push(executed_msg); - Success - } - Err(_) => Failure, - }; - proposal_run_status_w(&mut deps.storage) - .save(proposal_id.to_string().as_bytes(), &run_status)?; - - Ok(HandleResponse { - messages, - log: vec![], - data: Some(to_binary(&HandleAnswer::TriggerAdminCommand { - status: run_status, - proposal_id, - })?), - }) -} - -/// SELF only interactions - -pub fn try_create_proposal( - deps: &mut Extern, - env: &Env, - target_contract: String, - proposal: Binary, - description: String, -) -> StdResult { - let proposal_id = create_proposal(deps, env, target_contract, proposal, description)?; - - Ok(HandleResponse { - messages: vec![], - log: vec![], - data: Some(to_binary(&HandleAnswer::CreateProposal { - status: Success, - proposal_id, - })?), - }) -} - -#[allow(clippy::too_many_arguments)] -pub fn try_update_config( - deps: &mut Extern, - env: &Env, - admin: Option, - staker: Option, - proposal_deadline: Option, - funding_amount: Option, - funding_deadline: Option, - minimum_votes: Option, -) -> StdResult { - // It has to be self - if env.contract.address != env.message.sender { - return Err(StdError::Unauthorized { backtrace: None }); - } - - config_w(&mut deps.storage).update(|mut state| { - if let Some(admin) = admin { - state.admin = admin; - } - if staker.is_some() { - state.staker = staker; - } - if let Some(proposal_deadline) = proposal_deadline { - state.voting_deadline = proposal_deadline; - } - if let Some(funding_amount) = funding_amount { - state.funding_amount = funding_amount; - } - if let Some(funding_deadline) = funding_deadline { - state.funding_deadline = funding_deadline; - } - if let Some(minimum_votes) = minimum_votes { - state.minimum_votes = minimum_votes; - } - - Ok(state) - })?; - - Ok(HandleResponse { - messages: vec![], - log: vec![], - data: Some(to_binary(&HandleAnswer::UpdateConfig { status: Success })?), - }) -} - -pub fn try_disable_staker( - deps: &mut Extern, - _env: &Env, -) -> StdResult { - config_w(&mut deps.storage).update(|mut state| { - state.staker = None; - Ok(state) - })?; - - Ok(HandleResponse { - messages: vec![], - log: vec![], - data: Some(to_binary(&HandleAnswer::DisableStaker { status: Success })?), - }) -} - -pub fn try_add_supported_contract( - deps: &mut Extern, - env: &Env, - name: String, - contract: Contract, -) -> StdResult { - // It has to be self - if env.contract.address != env.message.sender { - return Err(StdError::Unauthorized { backtrace: None }); - } - - // Cannot be the same name as governance default - if name == *GOVERNANCE_SELF { - return Err(StdError::Unauthorized { backtrace: None }); - } - - // Supported contract cannot exist - if supported_contract_r(&deps.storage) - .may_load(name.as_bytes())? - .is_some() - { - return Err(StdError::Unauthorized { backtrace: None }); - } - - // Save contract - supported_contract_w(&mut deps.storage).save(name.as_bytes(), &contract)?; - - // Update command list - supported_contracts_list_w(&mut deps.storage).update(|mut arr| { - arr.push(name); - Ok(arr) - })?; - - Ok(HandleResponse { - messages: vec![], - log: vec![], - data: Some(to_binary(&HandleAnswer::AddSupportedContract { - status: Success, - })?), - }) -} - -pub fn try_remove_supported_contract( - deps: &mut Extern, - env: &Env, - name: String, -) -> StdResult { - // It has to be self - if env.contract.address != env.message.sender { - return Err(StdError::Unauthorized { backtrace: None }); - } - - // Cannot be the same name as governance default - if name == *GOVERNANCE_SELF { - return Err(StdError::Unauthorized { backtrace: None }); - } - - // Remove contract - supported_contract_w(&mut deps.storage).remove(name.as_bytes()); - - // Remove from array - supported_contracts_list_w(&mut deps.storage).update(|mut arr| { - arr.retain(|value| *value != name); - Ok(arr) - })?; - - Ok(HandleResponse { - messages: vec![], - log: vec![], - data: Some(to_binary(&HandleAnswer::RemoveSupportedContract { - status: Success, - })?), - }) -} - -pub fn try_update_supported_contract( - deps: &mut Extern, - env: &Env, - name: String, - contract: Contract, -) -> StdResult { - // It has to be self and cannot be the same name as governance default - if env.contract.address != env.message.sender || name == *GOVERNANCE_SELF { - return Err(StdError::Unauthorized { backtrace: None }); - } - - // Replace contract - supported_contract_w(&mut deps.storage).update(name.as_bytes(), |_state| Ok(contract))?; - - Ok(HandleResponse { - messages: vec![], - log: vec![], - data: Some(to_binary(&HandleAnswer::UpdateSupportedContract { - status: Success, - })?), - }) -} - -pub fn try_add_admin_command( - deps: &mut Extern, - env: &Env, - name: String, - proposal: String, -) -> StdResult { - // It has to be self - if env.contract.address != env.message.sender { - return Err(StdError::Unauthorized { backtrace: None }); - } - - // Admin command cannot exist - if admin_commands_r(&deps.storage) - .may_load(name.as_bytes())? - .is_some() - { - return Err(StdError::Unauthorized { backtrace: None }); - } - - // Save command - admin_commands_w(&mut deps.storage).save(name.as_bytes(), &AdminCommand { - msg: proposal.clone(), - total_arguments: proposal.matches(ADMIN_COMMAND_VARIABLE).count() as u16, - })?; - - // Update command list - admin_commands_list_w(&mut deps.storage).update(|mut arr| { - arr.push(name); - Ok(arr) - })?; - - Ok(HandleResponse { - messages: vec![], - log: vec![], - data: Some(to_binary(&HandleAnswer::AddAdminCommand { - status: Success, - })?), - }) -} - -pub fn try_remove_admin_command( - deps: &mut Extern, - env: &Env, - name: String, -) -> StdResult { - // It has to be self - if env.contract.address != env.message.sender { - return Err(StdError::Unauthorized { backtrace: None }); - } - - // Remove command - admin_commands_w(&mut deps.storage).remove(name.as_bytes()); - - // Remove from array - admin_commands_list_w(&mut deps.storage).update(|mut arr| { - arr.retain(|value| *value != name); - Ok(arr) - })?; - - Ok(HandleResponse { - messages: vec![], - log: vec![], - data: Some(to_binary(&HandleAnswer::RemoveAdminCommand { - status: Success, - })?), - }) -} - -pub fn try_update_admin_command( - deps: &mut Extern, - env: &Env, - name: String, - proposal: String, -) -> StdResult { - // It has to be self - if env.contract.address != env.message.sender { - return Err(StdError::Unauthorized { backtrace: None }); - } - - // Replace contract - admin_commands_w(&mut deps.storage).update(name.as_bytes(), |_state| { - Ok(AdminCommand { - msg: proposal.clone(), - total_arguments: proposal.matches(ADMIN_COMMAND_VARIABLE).count() as u16, - }) - })?; - - Ok(HandleResponse { - messages: vec![], - log: vec![], - data: Some(to_binary(&HandleAnswer::UpdateAdminCommand { - status: Success, - })?), - }) -} diff --git a/contracts/governance/src/handle/assembly.rs b/contracts/governance/src/handle/assembly.rs new file mode 100644 index 000000000..6709c0968 --- /dev/null +++ b/contracts/governance/src/handle/assembly.rs @@ -0,0 +1,284 @@ +use cosmwasm_math_compat::Uint128; +use cosmwasm_std::{ + from_binary, + to_binary, + Api, + Binary, + Coin, + Env, + Extern, + HandleResponse, + HumanAddr, + Querier, + StdError, + StdResult, + Storage, +}; +use shade_protocol::{ + contract_interfaces::governance::{ + assembly::{Assembly, AssemblyMsg}, + contract::AllowedContract, + profile::{Profile, VoteProfile}, + proposal::{Proposal, ProposalMsg, Status}, + stored_id::ID, + vote::Vote, + HandleAnswer, + MSG_VARIABLE, + }, + utils::{generic_response::ResponseStatus, storage::default::BucketStorage}, +}; +use std::convert::TryInto; + +pub fn try_assembly_vote( + deps: &mut Extern, + env: Env, + proposal: Uint128, + vote: Vote, +) -> StdResult { + let sender = env.message.sender; + + // Check if proposal in assembly voting + if let Status::AssemblyVote { end, .. } = Proposal::status(&deps.storage, &proposal)? { + if end <= env.block.time { + return Err(StdError::generic_err("Voting time has been reached")); + } + } else { + return Err(StdError::generic_err("Not in assembly vote phase")); + } + // Check if user in assembly + if !Assembly::data( + &deps.storage, + &Proposal::assembly(&deps.storage, &proposal)?, + )? + .members + .contains(&sender) + { + return Err(StdError::unauthorized()); + } + + let mut tally = Proposal::assembly_votes(&deps.storage, &proposal)?; + + // Assembly votes can only be = 1 uint + if vote.total_count()? != Uint128::new(1) { + return Err(StdError::generic_err("Assembly vote can only be one")); + } + + // Check if user voted + if let Some(old_vote) = Proposal::assembly_vote(&deps.storage, &proposal, &sender)? { + tally = tally.checked_sub(&old_vote)?; + } + + Proposal::save_assembly_vote(&mut deps.storage, &proposal, &sender, &vote)?; + Proposal::save_assembly_votes(&mut deps.storage, &proposal, &tally.checked_add(&vote)?)?; + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::AssemblyVote { + status: ResponseStatus::Success, + })?), + }) +} + +pub fn try_assembly_proposal( + deps: &mut Extern, + env: Env, + assembly_id: Uint128, + title: String, + metadata: String, + msgs: Option>, +) -> StdResult { + // Get assembly + let assembly_data = Assembly::data(&deps.storage, &assembly_id)?; + + // Check if public; everyone is allowed + if assembly_data.profile != Uint128::zero() { + if !assembly_data.members.contains(&env.message.sender) { + return Err(StdError::unauthorized()); + } + } + + // Get profile + // Check if assembly is enabled + let profile = Profile::data(&deps.storage, &assembly_data.profile)?; + if !profile.enabled { + return Err(StdError::generic_err("Assembly is disabled")); + } + + let status: Status; + + // Check if assembly voting + if let Some(vote_settings) = Profile::assembly_voting(&deps.storage, &assembly_data.profile)? { + status = Status::AssemblyVote { + start: env.block.time, + end: env.block.time + vote_settings.deadline, + } + } + // Check if funding + else if let Some(fund_settings) = Profile::funding(&deps.storage, &assembly_data.profile)? { + status = Status::Funding { + amount: Uint128::zero(), + start: env.block.time, + end: env.block.time + fund_settings.deadline, + } + } + // Check if token voting + else if let Some(vote_settings) = + Profile::public_voting(&deps.storage, &assembly_data.profile)? + { + status = Status::Voting { + start: env.block.time, + end: env.block.time + vote_settings.deadline, + } + } + // Else push directly to passed + else { + status = Status::Passed { + start: env.block.time, + end: env.block.time + profile.cancel_deadline, + } + } + + let processed_msgs: Option>; + if let Some(msgs) = msgs.clone() { + let mut new_msgs = vec![]; + for msg in msgs.iter() { + // Check if msg is allowed in assembly + let assembly_msg = AssemblyMsg::data(&deps.storage, &msg.assembly_msg)?; + if !assembly_msg.assemblies.contains(&assembly_id) { + return Err(StdError::unauthorized()); + } + + // Check if msg is allowed in contract + let contract = AllowedContract::data(&deps.storage, &msg.target)?; + if let Some(assemblies) = contract.assemblies { + if !assemblies.contains(&msg.target) { + return Err(StdError::unauthorized()); + } + } + + let vars: Vec = from_binary(&msg.msg)?; + let binary_msg = + Binary::from(assembly_msg.msg.create_msg(vars, MSG_VARIABLE)?.as_bytes()); + + new_msgs.push(ProposalMsg { + target: msg.target, + assembly_msg: msg.assembly_msg, + msg: binary_msg, + send: msg.send.clone(), + }); + } + processed_msgs = Some(new_msgs); + } else { + processed_msgs = None; + } + + let prop = Proposal { + proposer: env.message.sender, + title, + metadata, + msgs: processed_msgs, + assembly: assembly_id, + assembly_vote_tally: None, + public_vote_tally: None, + status, + status_history: vec![], + funders: None, + }; + + let prop_id = ID::add_proposal(&mut deps.storage)?; + prop.save(&mut deps.storage, &prop_id)?; + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::AssemblyProposal { + status: ResponseStatus::Success, + })?), + }) +} + +pub fn try_add_assembly( + deps: &mut Extern, + env: Env, + name: String, + metadata: String, + members: Vec, + profile: Uint128, +) -> StdResult { + if env.message.sender != env.contract.address { + return Err(StdError::unauthorized()); + } + + let id = ID::add_assembly(&mut deps.storage)?; + + // Check that profile exists + if profile > ID::profile(&deps.storage)? { + return Err(StdError::generic_err("Profile not found")); + } + + Assembly { + name, + metadata, + members, + profile, + } + .save(&mut deps.storage, &id)?; + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::AddAssembly { + status: ResponseStatus::Success, + })?), + }) +} + +pub fn try_set_assembly( + deps: &mut Extern, + env: Env, + id: Uint128, + name: Option, + metadata: Option, + members: Option>, + profile: Option, +) -> StdResult { + if env.message.sender != env.contract.address { + return Err(StdError::unauthorized()); + } + + let mut assembly = match Assembly::may_load(&mut deps.storage, &id)? { + None => return Err(StdError::generic_err("Assembly not found")), + Some(c) => c, + }; + + if let Some(name) = name { + assembly.name = name; + } + + if let Some(metadata) = metadata { + assembly.metadata = metadata + } + + if let Some(members) = members { + assembly.members = members + } + + if let Some(profile) = profile { + // Check that profile exists + if profile > ID::profile(&deps.storage)? { + return Err(StdError::generic_err("Profile not found")); + } + assembly.profile = profile + } + + assembly.save(&mut deps.storage, &id)?; + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::SetAssembly { + status: ResponseStatus::Success, + })?), + }) +} diff --git a/contracts/governance/src/handle/assembly_msg.rs b/contracts/governance/src/handle/assembly_msg.rs new file mode 100644 index 000000000..e0f891a59 --- /dev/null +++ b/contracts/governance/src/handle/assembly_msg.rs @@ -0,0 +1,132 @@ +use cosmwasm_math_compat::Uint128; +use cosmwasm_std::{ + to_binary, + Api, + Env, + Extern, + HandleResponse, + HumanAddr, + Querier, + StdError, + StdResult, + Storage, +}; +use shade_protocol::{ + contract_interfaces::governance::{ + assembly::AssemblyMsg, + stored_id::ID, + HandleAnswer, + MSG_VARIABLE, + }, + utils::{ + flexible_msg::FlexibleMsg, + generic_response::ResponseStatus, + storage::default::BucketStorage, + }, +}; + +pub fn try_add_assembly_msg( + deps: &mut Extern, + env: Env, + name: String, + msg: String, + assemblies: Vec, +) -> StdResult { + if env.message.sender != env.contract.address { + return Err(StdError::unauthorized()); + } + + let id = ID::add_assembly_msg(&mut deps.storage)?; + + // Check that assemblys exist + for assembly in assemblies.iter() { + if *assembly > ID::assembly(&deps.storage)? { + return Err(StdError::generic_err("Given assembly does not exist")); + } + } + + AssemblyMsg { + name, + assemblies, + msg: FlexibleMsg::new(msg, MSG_VARIABLE), + } + .save(&mut deps.storage, &id)?; + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::AddAssemblyMsg { + status: ResponseStatus::Success, + })?), + }) +} + +pub fn try_set_assembly_msg( + deps: &mut Extern, + env: Env, + id: Uint128, + name: Option, + msg: Option, + assemblies: Option>, +) -> StdResult { + if env.message.sender != env.contract.address { + return Err(StdError::unauthorized()); + } + + let mut assembly_msg = match AssemblyMsg::may_load(&mut deps.storage, &id)? { + None => return Err(StdError::generic_err("AssemblyMsg not found")), + Some(c) => c, + }; + + if let Some(name) = name { + assembly_msg.name = name; + } + + if let Some(msg) = msg { + assembly_msg.msg = FlexibleMsg::new(msg, MSG_VARIABLE); + } + + if let Some(assemblies) = assemblies { + assembly_msg.assemblies = assemblies; + } + + assembly_msg.save(&mut deps.storage, &id)?; + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::SetAssemblyMsg { + status: ResponseStatus::Success, + })?), + }) +} + +pub fn try_add_assembly_msg_assemblies( + deps: &mut Extern, + env: Env, + id: Uint128, + assemblies: Vec, +) -> StdResult { + if env.message.sender != env.contract.address { + return Err(StdError::unauthorized()); + } + + let mut assembly_msg = AssemblyMsg::data(&mut deps.storage, &id)?; + + let assembly_id = ID::assembly(&deps.storage)?; + for assembly in assemblies.iter() { + if assembly < &assembly_id && !assembly_msg.assemblies.contains(assembly) { + assembly_msg.assemblies.push(assembly.clone()); + } + } + + AssemblyMsg::save_data(&mut deps.storage, &id, assembly_msg)?; + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::SetAssemblyMsg { + status: ResponseStatus::Success, + })?), + }) +} diff --git a/contracts/governance/src/handle/contract.rs b/contracts/governance/src/handle/contract.rs new file mode 100644 index 000000000..525a7c532 --- /dev/null +++ b/contracts/governance/src/handle/contract.rs @@ -0,0 +1,154 @@ +use cosmwasm_math_compat::Uint128; +use cosmwasm_std::{ + to_binary, + Api, + Env, + Extern, + HandleResponse, + Querier, + StdError, + StdResult, + Storage, +}; +use shade_protocol::{ + contract_interfaces::governance::{contract::AllowedContract, stored_id::ID, HandleAnswer}, + utils::{asset::Contract, generic_response::ResponseStatus}, +}; + +pub fn try_add_contract( + deps: &mut Extern, + env: Env, + name: String, + metadata: String, + contract: Contract, + assemblies: Option>, +) -> StdResult { + if env.message.sender != env.contract.address { + return Err(StdError::unauthorized()); + } + + let id = ID::add_contract(&mut deps.storage)?; + + if let Some(ref assemblies) = assemblies { + let assembly_id = ID::assembly(&deps.storage)?; + for assembly in assemblies.iter() { + if assembly > &assembly_id { + return Err(StdError::generic_err("Assembly does not exist")); + } + } + } + + AllowedContract { + name, + metadata, + contract, + assemblies, + } + .save(&mut deps.storage, &id)?; + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::AddContract { + status: ResponseStatus::Success, + })?), + }) +} + +pub fn try_set_contract( + deps: &mut Extern, + env: Env, + id: Uint128, + name: Option, + metadata: Option, + contract: Option, + disable_assemblies: bool, + assemblies: Option>, +) -> StdResult { + if env.message.sender != env.contract.address { + return Err(StdError::unauthorized()); + } + + if id > ID::contract(&deps.storage)? { + return Err(StdError::generic_err("AllowedContract not found")); + } + + let mut allowed_contract = AllowedContract::load(&mut deps.storage, &id)?; + + if let Some(name) = name { + allowed_contract.name = name; + } + + if let Some(metadata) = metadata { + allowed_contract.metadata = metadata; + } + + if let Some(contract) = contract { + allowed_contract.contract = contract; + } + + if disable_assemblies { + allowed_contract.assemblies = None; + } else { + if let Some(assemblies) = assemblies { + let assembly_id = ID::assembly(&deps.storage)?; + for assembly in assemblies.iter() { + if assembly > &assembly_id { + return Err(StdError::generic_err("Assembly does not exist")); + } + } + allowed_contract.assemblies = Some(assemblies); + } + } + + allowed_contract.save(&mut deps.storage, &id)?; + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::AddContract { + status: ResponseStatus::Success, + })?), + }) +} + +pub fn try_add_contract_assemblies( + deps: &mut Extern, + env: Env, + id: Uint128, + assemblies: Vec, +) -> StdResult { + if env.message.sender != env.contract.address { + return Err(StdError::unauthorized()); + } + + if id > ID::contract(&deps.storage)? { + return Err(StdError::generic_err("AllowedContract not found")); + } + + let mut allowed_contract = AllowedContract::data(&mut deps.storage, &id)?; + + if let Some(mut old_assemblies) = allowed_contract.assemblies { + let assembly_id = ID::assembly(&deps.storage)?; + for assembly in assemblies.iter() { + if assembly <= &assembly_id && !old_assemblies.contains(assembly) { + old_assemblies.push(assembly.clone()); + } + } + allowed_contract.assemblies = Some(old_assemblies); + } else { + return Err(StdError::generic_err( + "Assembly support is disabled in this contract", + )); + } + + AllowedContract::save_data(&mut deps.storage, &id, allowed_contract)?; + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::AddContract { + status: ResponseStatus::Success, + })?), + }) +} diff --git a/contracts/governance/src/handle/mod.rs b/contracts/governance/src/handle/mod.rs new file mode 100644 index 000000000..5ffd09f44 --- /dev/null +++ b/contracts/governance/src/handle/mod.rs @@ -0,0 +1,93 @@ +use cosmwasm_std::{ + to_binary, + Api, + Env, + Extern, + HandleResponse, + HumanAddr, + Querier, + StdError, + StdResult, + Storage, +}; +use secret_toolkit::snip20::register_receive_msg; +use shade_protocol::{ + contract_interfaces::governance::{Config, HandleAnswer, RuntimeState}, + utils::{ + asset::Contract, + generic_response::ResponseStatus, + storage::default::SingletonStorage, + }, +}; + +pub mod assembly; +pub mod assembly_msg; +pub mod contract; +pub mod profile; +pub mod proposal; + +pub fn try_set_config( + deps: &mut Extern, + env: Env, + treasury: Option, + vote_token: Option, + funding_token: Option, +) -> StdResult { + if env.message.sender != env.contract.address { + return Err(StdError::unauthorized()); + } + + let mut messages = vec![]; + let mut config = Config::load(&deps.storage)?; + + // Vote and funding tokens cannot be set to none after being set + if let Some(vote_token) = vote_token { + config.vote_token = Some(vote_token.clone()); + messages.push(register_receive_msg( + env.contract_code_hash.clone(), + None, + 255, + vote_token.code_hash, + vote_token.address, + )?); + } + + if let Some(funding_token) = funding_token { + config.funding_token = Some(funding_token.clone()); + messages.push(register_receive_msg( + env.contract_code_hash.clone(), + None, + 255, + funding_token.code_hash, + funding_token.address, + )?); + } + + if let Some(treasury) = treasury { + config.treasury = treasury; + } + + config.save(&mut deps.storage)?; + Ok(HandleResponse { + messages, + log: vec![], + data: Some(to_binary(&HandleAnswer::SetConfig { + status: ResponseStatus::Success, + })?), + }) +} + +pub fn try_set_runtime_state( + deps: &mut Extern, + env: Env, + state: RuntimeState, +) -> StdResult { + todo!(); + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::SetRuntimeState { + status: ResponseStatus::Success, + })?), + }) +} diff --git a/contracts/governance/src/handle/profile.rs b/contracts/governance/src/handle/profile.rs new file mode 100644 index 000000000..e64a03fae --- /dev/null +++ b/contracts/governance/src/handle/profile.rs @@ -0,0 +1,98 @@ +use cosmwasm_math_compat::Uint128; +use cosmwasm_std::{ + to_binary, + Api, + Env, + Extern, + HandleResponse, + HumanAddr, + Querier, + StdError, + StdResult, + Storage, +}; +use shade_protocol::{ + contract_interfaces::governance::{ + profile::{Profile, UpdateProfile, UpdateVoteProfile, VoteProfile}, + stored_id::ID, + HandleAnswer, + }, + utils::{generic_response::ResponseStatus, storage::default::BucketStorage}, +}; + +pub fn try_add_profile( + deps: &mut Extern, + env: Env, + profile: Profile, +) -> StdResult { + if env.message.sender != env.contract.address { + return Err(StdError::unauthorized()); + } + + let id = ID::add_profile(&mut deps.storage)?; + profile.save(&mut deps.storage, &id)?; + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::AddProfile { + status: ResponseStatus::Success, + })?), + }) +} + +pub fn try_set_profile( + deps: &mut Extern, + env: Env, + id: Uint128, + new_profile: UpdateProfile, +) -> StdResult { + if env.message.sender != env.contract.address { + return Err(StdError::unauthorized()); + } + + let mut profile = match Profile::may_load(&mut deps.storage, &id)? { + None => return Err(StdError::generic_err("Profile not found")), + Some(p) => p, + }; + + if let Some(name) = new_profile.name { + profile.name = name; + } + + if let Some(enabled) = new_profile.enabled { + profile.enabled = enabled; + } + + if new_profile.disable_assembly { + profile.assembly = None; + } else if let Some(assembly) = new_profile.assembly { + profile.assembly = Some(assembly.update_profile(&profile.assembly)?) + } + + if new_profile.disable_funding { + profile.funding = None; + } else if let Some(funding) = new_profile.funding { + profile.funding = Some(funding.update_profile(&profile.funding)?) + } + + if new_profile.disable_token { + profile.token = None; + } else if let Some(token) = new_profile.token { + profile.token = Some(token.update_profile(&profile.token)?) + } + + if let Some(cancel_deadline) = new_profile.cancel_deadline { + profile.cancel_deadline = cancel_deadline; + } + + profile.save(&mut deps.storage, &id)?; + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::SetProfile { + status: ResponseStatus::Success, + })?), + }) +} diff --git a/contracts/governance/src/handle/proposal.rs b/contracts/governance/src/handle/proposal.rs new file mode 100644 index 000000000..f36200bd4 --- /dev/null +++ b/contracts/governance/src/handle/proposal.rs @@ -0,0 +1,591 @@ +use crate::handle::assembly::try_assembly_proposal; +use cosmwasm_math_compat::Uint128; +use cosmwasm_std::{ + from_binary, + to_binary, + Api, + Binary, + Coin, + Env, + Extern, + HandleResponse, + HumanAddr, + Querier, + StdError, + StdResult, + Storage, + WasmMsg, +}; +use secret_toolkit::{snip20::send_msg, utils::Query}; +use shade_protocol::{ + contract_interfaces::{ + governance::{ + assembly::Assembly, + contract::AllowedContract, + profile::{Count, Profile, VoteProfile}, + proposal::{Funding, Proposal, ProposalMsg, Status}, + vote::{ReceiveBalanceMsg, TalliedVotes, Vote}, + Config, + HandleAnswer, + HandleMsg::Receive, + }, + staking::snip20_staking, + }, + utils::{ + asset::Contract, + generic_response::ResponseStatus, + storage::default::SingletonStorage, + }, +}; + +// Initializes a proposal on the public assembly with the blank command +pub fn try_proposal( + deps: &mut Extern, + env: Env, + title: String, + metadata: String, + contract: Option, + msg: Option, + coins: Option>, +) -> StdResult { + let msgs: Option>; + + if contract.is_some() && msg.is_some() { + msgs = Some(vec![ProposalMsg { + target: contract.unwrap(), + assembly_msg: Uint128::zero(), + msg: to_binary(&msg.unwrap())?, + send: match coins { + None => vec![], + Some(c) => c, + }, + }]); + } else { + msgs = None; + } + + try_assembly_proposal(deps, env, Uint128::zero(), title, metadata, msgs)?; + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::Proposal { + status: ResponseStatus::Success, + })?), + }) +} + +pub fn try_trigger( + deps: &mut Extern, + env: Env, + proposal: Uint128, +) -> StdResult { + let mut messages = vec![]; + let status = Proposal::status(&deps.storage, &proposal)?; + if let Status::Passed { .. } = status { + let mut history = Proposal::status_history(&mut deps.storage, &proposal)?; + history.push(status); + Proposal::save_status_history(&mut deps.storage, &proposal, history)?; + Proposal::save_status(&mut deps.storage, &proposal, Status::Success)?; + + // Trigger the msg + let proposal_msg = Proposal::msg(&deps.storage, &proposal)?; + if let Some(prop_msgs) = proposal_msg { + for prop_msg in prop_msgs.iter() { + let contract = AllowedContract::data(&deps.storage, &prop_msg.target)?.contract; + messages.push( + WasmMsg::Execute { + contract_addr: contract.address, + callback_code_hash: contract.code_hash, + msg: prop_msg.msg.clone(), + send: prop_msg.send.clone(), + } + .into(), + ); + } + } + } else { + return Err(StdError::unauthorized()); + } + Ok(HandleResponse { + messages, + log: vec![], + data: Some(to_binary(&HandleAnswer::Trigger { + status: ResponseStatus::Success, + })?), + }) +} + +pub fn try_cancel( + deps: &mut Extern, + env: Env, + proposal: Uint128, +) -> StdResult { + // Check if passed, and check if current time > cancel time + let status = Proposal::status(&deps.storage, &proposal)?; + if let Status::Passed { start, end } = status { + if env.block.time < end { + return Err(StdError::unauthorized()); + } + let mut history = Proposal::status_history(&mut deps.storage, &proposal)?; + history.push(status); + Proposal::save_status_history(&mut deps.storage, &proposal, history)?; + Proposal::save_status(&mut deps.storage, &proposal, Status::Canceled)?; + } else { + return Err(StdError::unauthorized()); + } + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::Cancel { + status: ResponseStatus::Success, + })?), + }) +} + +fn validate_votes(votes: Vote, total_power: Uint128, settings: VoteProfile) -> Status { + let tally = TalliedVotes::tally(votes); + + let threshold = match settings.threshold { + Count::Percentage { percent } => total_power.multiply_ratio(percent, Uint128::new(10000)), + Count::LiteralCount { count } => count, + }; + + let yes_threshold = match settings.yes_threshold { + Count::Percentage { percent } => { + (tally.yes + tally.no).multiply_ratio(percent, Uint128::new(10000)) + } + Count::LiteralCount { count } => count, + }; + + let veto_threshold = match settings.veto_threshold { + Count::Percentage { percent } => { + (tally.yes + tally.no).multiply_ratio(percent, Uint128::new(10000)) + } + Count::LiteralCount { count } => count, + }; + + let new_status: Status; + + if tally.total < threshold { + new_status = Status::Expired; + } else if tally.veto >= veto_threshold { + new_status = Status::Vetoed { + slash_percent: Uint128::zero(), + }; + } else if tally.yes < yes_threshold { + new_status = Status::Rejected; + } else { + new_status = Status::Success; + } + + return new_status; +} + +pub fn try_update( + deps: &mut Extern, + env: Env, + proposal: Uint128, +) -> StdResult { + let mut history = Proposal::status_history(&deps.storage, &proposal)?; + let status = Proposal::status(&deps.storage, &proposal)?; + let mut new_status: Status; + + let assembly = Proposal::assembly(&deps.storage, &proposal)?; + let profile = Assembly::data(&deps.storage, &assembly)?.profile; + + let mut messages = vec![]; + + match status.clone() { + Status::AssemblyVote { start, end } => { + if end > env.block.time { + return Err(StdError::unauthorized()); + } + + let votes = Proposal::assembly_votes(&deps.storage, &proposal)?; + + // Total power is equal to the total amount of assembly members + let total_power = + Uint128::new(Assembly::data(&deps.storage, &assembly)?.members.len() as u128); + + // Try to load, if not then assume it was updated after proposal creation but before section end + let mut vote_conclusion: Status; + if let Some(settings) = Profile::assembly_voting(&deps.storage, &profile)? { + vote_conclusion = validate_votes(votes, total_power, settings); + } else { + vote_conclusion = Status::Success + } + + if let Status::Vetoed { .. } = vote_conclusion { + // Cant veto an assembly vote + vote_conclusion = Status::Rejected; + } + + // Try to load the next steps, if all are none then pass + if let Status::Success = vote_conclusion { + if let Some(setting) = Profile::funding(&deps.storage, &profile)? { + vote_conclusion = Status::Funding { + amount: Uint128::zero(), + start: env.block.time, + end: env.block.time + setting.deadline, + } + } else if let Some(setting) = Profile::public_voting(&deps.storage, &profile)? { + vote_conclusion = Status::Voting { + start: env.block.time, + end: env.block.time + setting.deadline, + } + } else { + vote_conclusion = Status::Passed { + start: env.block.time, + end: env.block.time + + Profile::data(&deps.storage, &profile)?.cancel_deadline, + } + } + } + + new_status = vote_conclusion; + } + Status::Funding { amount, start, end } => { + // This helps combat the possibility of the profile changing + // before another proposal is finished + if let Some(setting) = Profile::funding(&deps.storage, &profile)? { + // Check if deadline or funding limit reached + if amount >= setting.required { + new_status = Status::Passed { + start: env.block.time, + end: env.block.time + + Profile::data(&deps.storage, &profile)?.cancel_deadline, + } + } else if end > env.block.time { + return Err(StdError::unauthorized()); + } else { + new_status = Status::Expired; + } + } else { + new_status = Status::Passed { + start: env.block.time, + end: env.block.time + Profile::data(&deps.storage, &profile)?.cancel_deadline, + } + } + + if let Status::Passed { .. } = new_status { + if let Some(setting) = Profile::public_voting(&deps.storage, &profile)? { + new_status = Status::Voting { + start: env.block.time, + end: env.block.time + setting.deadline, + } + } + } + } + Status::Voting { start, end } => { + if end > env.block.time { + return Err(StdError::unauthorized()); + } + + let config = Config::load(&deps.storage)?; + let votes = Proposal::public_votes(&deps.storage, &proposal)?; + + let query: snip20_staking::QueryAnswer = snip20_staking::QueryMsg::TotalStaked {} + .query( + &deps.querier, + config.vote_token.clone().unwrap().code_hash, + config.vote_token.unwrap().address, + )?; + + // Get total staking power + let total_power = match query { + // TODO: fix when uint update is merged + snip20_staking::QueryAnswer::TotalStaked { shares, tokens } => tokens.into(), + _ => return Err(StdError::generic_err("Wrong query returned")), + }; + + let mut vote_conclusion: Status; + + if let Some(settings) = Profile::public_voting(&deps.storage, &profile)? { + vote_conclusion = validate_votes(votes, total_power, settings); + } else { + vote_conclusion = Status::Success + } + + if let Status::Vetoed { .. } = vote_conclusion { + // Send the funding amount to the treasury + if let Some(profile) = Profile::funding(&deps.storage, &profile)? { + // Look for the history and find funding + for s in history.iter() { + // Check if it has funding history + if let Status::Funding { amount, .. } = s { + let loss = profile.veto_deposit_loss.clone(); + vote_conclusion = Status::Vetoed { + slash_percent: loss, + }; + + let send_amount = amount.multiply_ratio(100000u128, loss); + if send_amount != Uint128::zero() { + let config = Config::load(&deps.storage)?; + // Update slash amount + messages.push(send_msg( + config.treasury, + cosmwasm_std::Uint128(send_amount.u128()), + None, + None, + None, + 1, + config.funding_token.clone().unwrap().code_hash, + config.funding_token.unwrap().address, + )?); + } + break; + } + } + } + } else if let Status::Success = vote_conclusion { + vote_conclusion = Status::Passed { + start: env.block.time, + end: env.block.time + Profile::data(&deps.storage, &profile)?.cancel_deadline, + } + } + + new_status = vote_conclusion; + } + _ => return Err(StdError::generic_err("Cant update")), + } + + // Add old status to history + history.push(status); + Proposal::save_status_history(&mut deps.storage, &proposal, history)?; + // Save new status + Proposal::save_status(&mut deps.storage, &proposal, new_status.clone())?; + + Ok(HandleResponse { + messages, + log: vec![], + data: Some(to_binary(&HandleAnswer::Update { + status: ResponseStatus::Success, + })?), + }) +} + +pub fn try_receive( + deps: &mut Extern, + env: Env, + sender: HumanAddr, + from: HumanAddr, + amount: Uint128, + msg: Option, + memo: Option, +) -> StdResult { + // Check if sent token is the funding token + let funding_token: Contract; + if let Some(token) = Config::load(&deps.storage)?.funding_token { + funding_token = token.clone(); + if env.message.sender != token.address { + return Err(StdError::generic_err("Must be the set funding token")); + } + } else { + return Err(StdError::generic_err("Funding token not set")); + } + + // Check if msg contains the proposal information + let proposal: Uint128; + if let Some(msg) = msg { + proposal = from_binary(&msg)?; + } else { + return Err(StdError::generic_err("Msg must be set")); + } + + // Check if proposal is in funding stage + let mut return_amount = Uint128::zero(); + + let status = Proposal::status(&deps.storage, &proposal)?; + if let Status::Funding { + amount: funded, + start, + end, + } = status + { + // Check if proposal funding stage is set or funding limit already set + if env.block.time >= end { + return Err(StdError::generic_err("Funding time limit reached")); + } + + let mut new_fund = amount + funded; + + let assembly = &Proposal::assembly(&deps.storage, &proposal)?; + let profile = &Assembly::data(&deps.storage, assembly)?.profile; + if let Some(funding_profile) = Profile::funding(&deps.storage, &profile)? { + if funding_profile.required == funded { + return Err(StdError::generic_err("Already funded")); + } + + if funding_profile.required < new_fund { + return_amount = new_fund.checked_sub(funding_profile.required)?; + new_fund = funding_profile.required; + } + } else { + return Err(StdError::generic_err("Funding profile setting was removed")); + } + + // Store the funder information and update the current funding data + Proposal::save_status(&mut deps.storage, &proposal, Status::Funding { + amount: new_fund, + start, + end, + })?; + + // Either add or update funder + let mut funder_amount = amount.checked_sub(return_amount)?; + let mut funders = Proposal::funders(&deps.storage, &proposal)?; + if funders.contains(&from) { + funder_amount += Proposal::funding(&deps.storage, &proposal, &from)?.amount; + } else { + funders.push(from.clone()); + Proposal::save_funders(&mut deps.storage, &proposal, funders)?; + } + Proposal::save_funding(&mut deps.storage, &proposal, &from, Funding { + amount: funder_amount, + claimed: false, + })?; + } else { + return Err(StdError::generic_err("Not in funding status")); + } + + let mut messages = vec![]; + if return_amount != Uint128::zero() { + messages.push(send_msg( + from, + return_amount.into(), + None, + None, + None, + 256, + funding_token.code_hash, + funding_token.address, + )?); + } + + Ok(HandleResponse { + messages, + log: vec![], + data: Some(to_binary(&HandleAnswer::Receive { + status: ResponseStatus::Success, + })?), + }) +} + +pub fn try_claim_funding( + deps: &mut Extern, + env: Env, + id: Uint128, +) -> StdResult { + let reduction = match Proposal::status(&deps.storage, &id)? { + Status::AssemblyVote { .. } | Status::Funding { .. } | Status::Voting { .. } => { + return Err(StdError::generic_err("Cannot claim funding")); + } + Status::Vetoed { slash_percent } => slash_percent, + _ => Uint128::zero(), + }; + + let funding = Proposal::funding(&deps.storage, &id, &env.message.sender)?; + + if funding.claimed { + return Err(StdError::generic_err("Funding already claimed")); + } + + let return_amount = funding.amount.checked_sub( + funding + .amount + .multiply_ratio(reduction, Uint128::new(10000)), + )?; + + if return_amount == Uint128::zero() { + return Err(StdError::generic_err("Nothing to claim")); + } + + let funding_token = match Config::load(&deps.storage)?.funding_token { + None => return Err(StdError::generic_err("No funding token set")), + Some(token) => token, + }; + + Ok(HandleResponse { + messages: vec![send_msg( + env.message.sender, + return_amount.into(), + None, + None, + None, + 256, + funding_token.code_hash, + funding_token.address, + )?], + log: vec![], + data: Some(to_binary(&HandleAnswer::ClaimFunding { + status: ResponseStatus::Success, + })?), + }) +} + +pub fn try_receive_balance( + deps: &mut Extern, + env: Env, + sender: HumanAddr, + msg: Option, + balance: Uint128, + memo: Option, +) -> StdResult { + if let Some(token) = Config::load(&deps.storage)?.vote_token { + if env.message.sender != token.address { + return Err(StdError::generic_err("Must be the set voting token")); + } + } else { + return Err(StdError::generic_err("Voting token not set")); + } + + let vote: Vote; + let proposal: Uint128; + if let Some(msg) = msg { + let decoded_msg: ReceiveBalanceMsg = from_binary(&msg)?; + vote = decoded_msg.vote; + proposal = decoded_msg.proposal; + + // Verify that total does not exceed balance + let total_votes = vote.yes.checked_add( + vote.no + .checked_add(vote.abstain.checked_add(vote.no_with_veto)?)?, + )?; + + if total_votes > balance { + return Err(StdError::generic_err( + "Total voting is greater than available balance", + )); + } + } else { + return Err(StdError::generic_err("Msg not set")); + } + + // Check if proposal in assembly voting + if let Status::Voting { end, .. } = Proposal::status(&deps.storage, &proposal)? { + if end <= env.block.time { + return Err(StdError::generic_err("Voting time has been reached")); + } + } else { + return Err(StdError::generic_err("Not in public vote phase")); + } + + let mut tally = Proposal::public_votes(&deps.storage, &proposal)?; + + // Check if user voted + if let Some(old_vote) = Proposal::public_vote(&deps.storage, &proposal, &sender)? { + tally = tally.checked_sub(&old_vote)?; + } + + Proposal::save_public_vote(&mut deps.storage, &proposal, &sender, &vote)?; + Proposal::save_public_votes(&mut deps.storage, &proposal, &tally.checked_add(&vote)?)?; + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::ReceiveBalance { + status: ResponseStatus::Success, + })?), + }) +} diff --git a/contracts/governance/src/lib.rs b/contracts/governance/src/lib.rs index d82e1f663..f05e38508 100644 --- a/contracts/governance/src/lib.rs +++ b/contracts/governance/src/lib.rs @@ -1,11 +1,9 @@ pub mod contract; pub mod handle; -pub mod proposal_state; pub mod query; -pub mod state; #[cfg(test)] -mod test; +pub mod tests; #[cfg(target_arch = "wasm32")] mod wasm { diff --git a/contracts/governance/src/proposal_state.rs b/contracts/governance/src/proposal_state.rs deleted file mode 100644 index 8f5624c66..000000000 --- a/contracts/governance/src/proposal_state.rs +++ /dev/null @@ -1,131 +0,0 @@ -use cosmwasm_math_compat::Uint128; -use cosmwasm_std::Storage; -use cosmwasm_storage::{ - bucket, - bucket_read, - singleton, - singleton_read, - Bucket, - ReadonlyBucket, - ReadonlySingleton, - Singleton, -}; -use secret_toolkit::snip20::batch::SendAction; -use shade_protocol::{ - contract_interfaces::governance::{ - proposal::{Proposal, ProposalStatus}, - vote::VoteTally, - }, - utils::generic_response::ResponseStatus, -}; - -// Proposals -pub static PROPOSAL_KEY: &[u8] = b"proposals"; -pub static PROPOSAL_VOTE_DEADLINE_KEY: &[u8] = b"proposal_vote_deadline_key"; -pub static PROPOSAL_FUNDING_DEADLINE_KEY: &[u8] = b"proposal_funding_deadline_key"; -pub static PROPOSAL_STATUS_KEY: &[u8] = b"proposal_status_key"; -pub static PROPOSAL_RUN_KEY: &[u8] = b"proposal_run_key"; -pub static PROPOSAL_FUNDING_KEY: &[u8] = b"proposal_funding_key"; -pub static PROPOSAL_FUNDING_BATCH_KEY: &[u8] = b"proposal_funding_batch_key"; -pub static PROPOSAL_VOTES_KEY: &str = "proposal_votes"; -pub static TOTAL_PROPOSAL_VOTES_KEY: &[u8] = b"total_proposal_votes"; -pub static TOTAL_PROPOSAL_KEY: &[u8] = b"total_proposals"; - -// Total proposal counter -pub fn total_proposals_w(storage: &mut S) -> Singleton { - singleton(storage, TOTAL_PROPOSAL_KEY) -} - -pub fn total_proposals_r(storage: &S) -> ReadonlySingleton { - singleton_read(storage, TOTAL_PROPOSAL_KEY) -} - -// Individual proposals -pub fn proposal_r(storage: &S) -> ReadonlyBucket { - bucket_read(PROPOSAL_KEY, storage) -} - -pub fn proposal_w(storage: &mut S) -> Bucket { - bucket(PROPOSAL_KEY, storage) -} - -// Proposal funding deadline -pub fn proposal_funding_deadline_r(storage: &S) -> ReadonlyBucket { - bucket_read(PROPOSAL_FUNDING_DEADLINE_KEY, storage) -} - -pub fn proposal_funding_deadline_w(storage: &mut S) -> Bucket { - bucket(PROPOSAL_FUNDING_DEADLINE_KEY, storage) -} - -// Proposal voting deadline -pub fn proposal_voting_deadline_r(storage: &S) -> ReadonlyBucket { - bucket_read(PROPOSAL_VOTE_DEADLINE_KEY, storage) -} - -pub fn proposal_voting_deadline_w(storage: &mut S) -> Bucket { - bucket(PROPOSAL_VOTE_DEADLINE_KEY, storage) -} - -// Proposal status -pub fn proposal_status_r(storage: &S) -> ReadonlyBucket { - bucket_read(PROPOSAL_STATUS_KEY, storage) -} - -pub fn proposal_status_w(storage: &mut S) -> Bucket { - bucket(PROPOSAL_STATUS_KEY, storage) -} - -// Proposal total funding -pub fn proposal_funding_r(storage: &S) -> ReadonlyBucket { - bucket_read(PROPOSAL_FUNDING_KEY, storage) -} - -pub fn proposal_funding_w(storage: &mut S) -> Bucket { - bucket(PROPOSAL_FUNDING_KEY, storage) -} - -// Proposal funding batch -pub fn proposal_funding_batch_r(storage: &S) -> ReadonlyBucket> { - bucket_read(PROPOSAL_FUNDING_BATCH_KEY, storage) -} - -pub fn proposal_funding_batch_w(storage: &mut S) -> Bucket> { - bucket(PROPOSAL_FUNDING_BATCH_KEY, storage) -} - -// Proposal run status - will be available after proposal is run -pub fn proposal_run_status_r(storage: &S) -> ReadonlyBucket { - bucket_read(PROPOSAL_RUN_KEY, storage) -} - -pub fn proposal_run_status_w(storage: &mut S) -> Bucket { - bucket(PROPOSAL_RUN_KEY, storage) -} - -// Individual proposal user votes -pub fn proposal_votes_r( - storage: &S, - proposal: Uint128, -) -> ReadonlyBucket { - bucket_read( - (proposal.to_string() + PROPOSAL_VOTES_KEY).as_bytes(), - storage, - ) -} - -pub fn proposal_votes_w(storage: &mut S, proposal: Uint128) -> Bucket { - bucket( - (proposal.to_string() + PROPOSAL_VOTES_KEY).as_bytes(), - storage, - ) -} - -// Total proposal votes -pub fn total_proposal_votes_r(storage: &S) -> ReadonlyBucket { - bucket_read(TOTAL_PROPOSAL_VOTES_KEY, storage) -} - -pub fn total_proposal_votes_w(storage: &mut S) -> Bucket { - bucket(TOTAL_PROPOSAL_VOTES_KEY, storage) -} diff --git a/contracts/governance/src/query.rs b/contracts/governance/src/query.rs index 5b4255374..0c90485e8 100644 --- a/contracts/governance/src/query.rs +++ b/contracts/governance/src/query.rs @@ -1,49 +1,29 @@ use cosmwasm_math_compat::Uint128; use cosmwasm_std::{Api, Extern, Querier, StdError, StdResult, Storage}; -use shade_protocol::contract_interfaces::governance::{ - proposal::{ProposalStatus, QueriedProposal}, - QueryAnswer, -}; - -use crate::{ - proposal_state::{ - proposal_funding_deadline_r, - proposal_funding_r, - proposal_r, - proposal_run_status_r, - proposal_status_r, - proposal_voting_deadline_r, - total_proposal_votes_r, - total_proposals_r, - }, - state::{ - admin_commands_list_r, - admin_commands_r, - supported_contract_r, - supported_contracts_list_r, +use shade_protocol::{ + contract_interfaces::governance::{ + assembly::{Assembly, AssemblyMsg}, + contract::AllowedContract, + profile::Profile, + proposal::Proposal, + stored_id::ID, + Config, + QueryAnswer, }, + utils::storage::default::SingletonStorage, }; -fn build_proposal( +pub fn config(deps: &Extern) -> StdResult { + Ok(QueryAnswer::Config { + config: Config::load(&deps.storage)?, + }) +} + +pub fn total_proposals( deps: &Extern, - proposal_id: Uint128, -) -> StdResult { - let proposal = proposal_r(&deps.storage).load(proposal_id.to_string().as_bytes())?; - - Ok(QueriedProposal { - id: proposal.id, - target: proposal.target, - msg: proposal.msg, - description: proposal.description, - funding_deadline: proposal_funding_deadline_r(&deps.storage) - .load(proposal_id.to_string().as_bytes())?, - voting_deadline: proposal_voting_deadline_r(&deps.storage) - .may_load(proposal_id.to_string().as_bytes())?, - total_funding: proposal_funding_r(&deps.storage) - .load(proposal_id.to_string().as_bytes())?, - status: proposal_status_r(&deps.storage).load(proposal_id.to_string().as_bytes())?, - run_status: proposal_run_status_r(&deps.storage) - .may_load(proposal_id.to_string().as_bytes())?, +) -> StdResult { + Ok(QueryAnswer::Total { + total: ID::proposal(&deps.storage)?.checked_add(Uint128::new(1))?, }) } @@ -51,92 +31,150 @@ pub fn proposals( deps: &Extern, start: Uint128, end: Uint128, - status: Option, ) -> StdResult { - let mut proposals: Vec = vec![]; - - let max = total_proposals_r(&deps.storage).load()?; + let mut items = vec![]; + let mut end = end; + let total = ID::proposal(&deps.storage)?; - if start > max { - return Err(StdError::NotFound { - kind: "Proposal doesnt exist".to_string(), - backtrace: None, - }); + if start > total { + return Err(StdError::generic_err("Proposal not found")); } - let clamped_start = start.max(Uint128::new(1u128)); - - for i in clamped_start.u128()..((end + clamped_start).min(max).u128() + 1) { - let proposal = build_proposal(deps, Uint128::new(i))?; + if end > total { + end = total; + } - // Filter proposal by status if it was specified in fn params. - if let Some(s) = &status { - if s != &proposal.status { - continue; - } - } - proposals.push(proposal) + for i in start.u128()..=end.u128() { + items.push(Proposal::load(&deps.storage, &Uint128::new(i))?); } - Ok(QueryAnswer::Proposals { proposals }) + Ok(QueryAnswer::Proposals { props: items }) } -pub fn proposal( +pub fn total_profiles( deps: &Extern, - proposal_id: Uint128, ) -> StdResult { - Ok(QueryAnswer::Proposal { - proposal: build_proposal(deps, proposal_id)?, + Ok(QueryAnswer::Total { + total: ID::profile(&deps.storage)?.checked_add(Uint128::new(1))?, }) } -pub fn total_proposals( +pub fn profiles( deps: &Extern, + start: Uint128, + end: Uint128, ) -> StdResult { - Ok(QueryAnswer::TotalProposals { - total: total_proposals_r(&deps.storage).load()?, - }) + let mut items = vec![]; + let mut end = end; + let total = ID::profile(&deps.storage)?; + + if start > total { + return Err(StdError::generic_err("Profile not found")); + } + + if end > total { + end = total; + } + + for i in start.u128()..=end.u128() { + items.push(Profile::load(&deps.storage, &Uint128::new(i))?); + } + + Ok(QueryAnswer::Profiles { profiles: items }) } -pub fn proposal_votes( +pub fn total_assemblies( deps: &Extern, - proposal_id: Uint128, ) -> StdResult { - Ok(QueryAnswer::ProposalVotes { - status: total_proposal_votes_r(&deps.storage).load(proposal_id.to_string().as_bytes())?, + Ok(QueryAnswer::Total { + total: ID::assembly(&deps.storage)?.checked_add(Uint128::new(1))?, }) } -pub fn supported_contracts( +pub fn assemblies( deps: &Extern, + start: Uint128, + end: Uint128, ) -> StdResult { - Ok(QueryAnswer::SupportedContracts { - contracts: supported_contracts_list_r(&deps.storage).load()?, - }) + let mut items = vec![]; + let mut end = end; + let total = ID::assembly(&deps.storage)?; + + if start > total { + return Err(StdError::generic_err("Assembly not found")); + } + + if end > total { + end = total; + } + + for i in start.u128()..=end.u128() { + items.push(Assembly::load(&deps.storage, &Uint128::new(i))?); + } + + Ok(QueryAnswer::Assemblies { assemblies: items }) } -pub fn supported_contract( +pub fn total_assembly_msgs( deps: &Extern, - name: String, ) -> StdResult { - Ok(QueryAnswer::SupportedContract { - contract: supported_contract_r(&deps.storage).load(name.as_bytes())?, + Ok(QueryAnswer::Total { + total: ID::assembly_msg(&deps.storage)?.checked_add(Uint128::new(1))?, }) } -pub fn admin_commands( +pub fn assembly_msgs( deps: &Extern, + start: Uint128, + end: Uint128, ) -> StdResult { - Ok(QueryAnswer::AdminCommands { - commands: admin_commands_list_r(&deps.storage).load()?, - }) + let mut items = vec![]; + let mut end = end; + let total = ID::assembly_msg(&deps.storage)?; + + if start > total { + return Err(StdError::generic_err("AssemblyMsg not found")); + } + + if end > total { + end = total; + } + + for i in start.u128()..=end.u128() { + items.push(AssemblyMsg::load(&deps.storage, &Uint128::new(i))?); + } + + Ok(QueryAnswer::AssemblyMsgs { msgs: items }) } -pub fn admin_command( +pub fn total_contracts( deps: &Extern, - name: String, ) -> StdResult { - Ok(QueryAnswer::AdminCommand { - command: admin_commands_r(&deps.storage).load(name.as_bytes())?, + Ok(QueryAnswer::Total { + total: ID::contract(&deps.storage)?.checked_add(Uint128::new(1))?, }) } + +pub fn contracts( + deps: &Extern, + start: Uint128, + end: Uint128, +) -> StdResult { + let mut items = vec![]; + let mut end = end; + let total = ID::contract(&deps.storage)?; + + if start > total { + return Err(StdError::generic_err("Contract not found")); + } + + if end > total { + end = total; + } + + for i in start.u128()..=end.u128() { + items.push(AllowedContract::load(&deps.storage, &Uint128::new(i))?); + } + + Ok(QueryAnswer::Contracts { contracts: items }) +} diff --git a/contracts/governance/src/state.rs b/contracts/governance/src/state.rs deleted file mode 100644 index c5763df9e..000000000 --- a/contracts/governance/src/state.rs +++ /dev/null @@ -1,67 +0,0 @@ -use cosmwasm_std::Storage; -use cosmwasm_storage::{ - bucket, - bucket_read, - singleton, - singleton_read, - Bucket, - ReadonlyBucket, - ReadonlySingleton, - Singleton, -}; -use shade_protocol::{ - contract_interfaces::governance::{AdminCommand, Config}, - utils::asset::Contract, -}; - -pub static CONFIG_KEY: &[u8] = b"config"; -// Saved contracts -pub static CONTRACT_KEY: &[u8] = b"supported_contracts"; -pub static CONTRACT_LIST_KEY: &[u8] = b"supported_contracts_list"; -// Admin commands -pub static ADMIN_COMMANDS_KEY: &[u8] = b"admin_commands"; -pub static ADMIN_COMMANDS_LIST_KEY: &[u8] = b"admin_commands_list"; - -pub fn config_w(storage: &mut S) -> Singleton { - singleton(storage, CONFIG_KEY) -} - -pub fn config_r(storage: &S) -> ReadonlySingleton { - singleton_read(storage, CONFIG_KEY) -} - -// Supported contracts - -pub fn supported_contract_r(storage: &S) -> ReadonlyBucket { - bucket_read(CONTRACT_KEY, storage) -} - -pub fn supported_contract_w(storage: &mut S) -> Bucket { - bucket(CONTRACT_KEY, storage) -} - -pub fn supported_contracts_list_w(storage: &mut S) -> Singleton> { - singleton(storage, CONTRACT_LIST_KEY) -} - -pub fn supported_contracts_list_r(storage: &S) -> ReadonlySingleton> { - singleton_read(storage, CONTRACT_LIST_KEY) -} - -// Admin commands - -pub fn admin_commands_r(storage: &S) -> ReadonlyBucket { - bucket_read(ADMIN_COMMANDS_KEY, storage) -} - -pub fn admin_commands_w(storage: &mut S) -> Bucket { - bucket(ADMIN_COMMANDS_KEY, storage) -} - -pub fn admin_commands_list_w(storage: &mut S) -> Singleton> { - singleton(storage, ADMIN_COMMANDS_LIST_KEY) -} - -pub fn admin_commands_list_r(storage: &S) -> ReadonlySingleton> { - singleton_read(storage, ADMIN_COMMANDS_LIST_KEY) -} diff --git a/contracts/governance/src/test.rs b/contracts/governance/src/test.rs deleted file mode 100644 index cca507db3..000000000 --- a/contracts/governance/src/test.rs +++ /dev/null @@ -1,156 +0,0 @@ -#[cfg(test)] -mod tests { - use crate::contract; - use cosmwasm_math_compat::Uint128; - use cosmwasm_std::{ - coins, - from_binary, - testing::{mock_dependencies, mock_env}, - Api, - Extern, - HumanAddr, - Querier, - Storage, - }; - use shade_protocol::{ - contract_interfaces::{ - governance, - governance::proposal::{ProposalStatus, QueriedProposal}, - }, - utils::{asset::Contract, generic_response::ResponseStatus}, - }; - - #[test] - fn get_proposals_by_status() { - let mut deps = mock_dependencies(20, &coins(0, "")); - - // Initialize governance contract. - let env = mock_env("creator", &coins(0, "")); - let governance_init_msg = governance::InitMsg { - admin: None, - // The next governance votes will not require voting - staker: None, - funding_token: Contract { - address: HumanAddr::from(""), - code_hash: String::from(""), - }, - funding_amount: Uint128::new(1000000u128), - funding_deadline: 180, - voting_deadline: 180, - // 5 shade is the minimum - quorum: Uint128::new(5000000u128), - }; - let res = contract::init(&mut deps, env, governance_init_msg).unwrap(); - assert_eq!(1, res.messages.len()); - - // Initialized governance contract has no proposals. - let res = contract::query(&deps, governance::QueryMsg::GetProposals { - start: Uint128::new(0u128), - end: Uint128::new(100u128), - status: Some(ProposalStatus::Funding), - }) - .unwrap(); - let value: governance::QueryAnswer = from_binary(&res).unwrap(); - match value { - governance::QueryAnswer::Proposals { proposals } => { - assert_eq!(0, proposals.len()); - } - _ => { - panic!("Received wrong answer") - } - } - - // Create a proposal on governance contract. - let env = mock_env("creator", &coins(0, "")); - let res = contract::handle(&mut deps, env, governance::HandleMsg::CreateProposal { - target_contract: String::from(governance::GOVERNANCE_SELF), - proposal: serde_json::to_string(&governance::HandleMsg::AddAdminCommand { - name: "random data here".to_string(), - proposal: "{\"update_config\":{\"unbond_time\": {}, \"admin\": null}}".to_string(), - }) - .unwrap(), - description: String::from("Proposal on governance contract"), - }) - .unwrap(); - let value: governance::HandleAnswer = from_binary(&res.data.unwrap()).unwrap(); - match value { - governance::HandleAnswer::CreateProposal { - status, - proposal_id, - } => { - assert_eq!(ResponseStatus::Success, status); - assert!(!proposal_id.is_zero()); - } - _ => { - panic!("Received wrong answer") - } - } - - // Now we should have single proposal in `funding`. - - // Should return this proposal when no specific status is specified. - assert_get_proposals( - &deps, - governance::QueryMsg::GetProposals { - start: Uint128::zero(), - end: Uint128::new(100u128), - status: None, - }, - |proposals| { - assert_eq!(1, proposals.len()); - assert_eq!(proposals[0].status, ProposalStatus::Funding); - }, - ); - - // Should return this proposal when `funding` status is specified. - assert_get_proposals( - &deps, - governance::QueryMsg::GetProposals { - start: Uint128::zero(), - end: Uint128::new(100u128), - status: Some(ProposalStatus::Funding), - }, - |proposals| { - assert_eq!(1, proposals.len()); - assert_eq!(proposals[0].status, ProposalStatus::Funding); - }, - ); - - // Shouldn't return this proposal when querying by status different from `funding`. - assert_get_proposals( - &deps, - governance::QueryMsg::GetProposals { - start: Uint128::zero(), - end: Uint128::new(100u128), - status: Some(ProposalStatus::Voting), - }, - |proposals| { - assert_eq!(0, proposals.len()); - }, - ); - } - - /// - /// Assert via assertFn on the result of governance::QueryMsg::GetProposals contract call. - /// - /// # Arguments - /// - /// * 'deps' - External contract dependencies - /// * 'msg' - The message data - /// * 'assert_fn' - A bunch of assert statements to be performed on contract call response - /// - pub fn assert_get_proposals( - deps: &Extern, - msg: governance::QueryMsg, - assert_fn: fn(result: Vec), - ) { - let res = contract::query(&deps, msg).unwrap(); - let value: governance::QueryAnswer = from_binary(&res).unwrap(); - match value { - governance::QueryAnswer::Proposals { proposals } => assert_fn(proposals), - _ => { - panic!("Received wrong answer") - } - } - } -} diff --git a/contracts/governance/src/tests/handle/assembly.rs b/contracts/governance/src/tests/handle/assembly.rs new file mode 100644 index 000000000..2ddf07889 --- /dev/null +++ b/contracts/governance/src/tests/handle/assembly.rs @@ -0,0 +1,110 @@ +use crate::tests::{admin_only_governance, get_assemblies}; +use cosmwasm_math_compat::Uint128; +use cosmwasm_std::HumanAddr; +use fadroma_ensemble::MockEnv; +use shade_protocol::contract_interfaces::governance; + +#[test] +fn add_assembly() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + chain + .execute( + &governance::HandleMsg::AddAssembly { + name: "Other assembly".to_string(), + metadata: "some data".to_string(), + members: vec![], + profile: Uint128::new(1), + padding: None, + }, + MockEnv::new( + // Sender is self + gov.address.clone(), + gov.clone(), + ), + ) + .unwrap(); + + let assemblies = get_assemblies(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap(); + + assert_eq!(assemblies.len(), 3); +} + +#[test] +fn unauthorised_add_assembly() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + chain + .execute( + &governance::HandleMsg::AddAssembly { + name: "Other assembly".to_string(), + metadata: "some data".to_string(), + members: vec![], + profile: Uint128::new(1), + padding: None, + }, + MockEnv::new( + // Sender is self + HumanAddr::from("random"), + gov.clone(), + ), + ) + .is_err(); +} + +#[test] +fn set_assembly() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + let old_assembly = + get_assemblies(&mut chain, &gov, Uint128::new(1), Uint128::new(2)).unwrap()[0].clone(); + + chain + .execute( + &governance::HandleMsg::SetAssembly { + id: Uint128::new(1), + name: Some("Random name".to_string()), + metadata: Some("data".to_string()), + members: None, + profile: None, + padding: None, + }, + MockEnv::new( + // Sender is self + gov.address.clone(), + gov.clone(), + ), + ) + .unwrap(); + + let new_assembly = + get_assemblies(&mut chain, &gov, Uint128::new(1), Uint128::new(2)).unwrap()[0].clone(); + + assert_ne!(new_assembly.name, old_assembly.name); + assert_ne!(new_assembly.metadata, old_assembly.metadata); + assert_eq!(new_assembly.members, old_assembly.members); + assert_eq!(new_assembly.profile, old_assembly.profile); +} + +#[test] +fn unauthorised_set_assembly() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + chain + .execute( + &governance::HandleMsg::SetAssembly { + id: Uint128::new(1), + name: Some("Random name".to_string()), + metadata: Some("data".to_string()), + members: None, + profile: None, + padding: None, + }, + MockEnv::new( + // Sender is self + HumanAddr::from("random"), + gov.clone(), + ), + ) + .is_err(); +} diff --git a/contracts/governance/src/tests/handle/assembly_msg.rs b/contracts/governance/src/tests/handle/assembly_msg.rs new file mode 100644 index 000000000..931ef7b29 --- /dev/null +++ b/contracts/governance/src/tests/handle/assembly_msg.rs @@ -0,0 +1,105 @@ +use crate::tests::{admin_only_governance, get_assembly_msgs}; +use cosmwasm_math_compat::Uint128; +use fadroma_ensemble::MockEnv; +use shade_protocol::contract_interfaces::{governance, governance::assembly::AssemblyMsg}; + +#[test] +fn add_assembly_msg() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + chain + .execute( + &governance::HandleMsg::AddAssemblyMsg { + name: "Some Assembly name".to_string(), + msg: "{}".to_string(), + assemblies: vec![Uint128::zero()], + padding: None, + }, + MockEnv::new( + // Sender is self + gov.address.clone(), + gov.clone(), + ), + ) + .unwrap(); + + let assemblies = get_assembly_msgs(&mut chain, &gov, Uint128::zero(), Uint128::new(1)).unwrap(); + + assert_eq!(assemblies.len(), 2); +} + +#[test] +fn unauthorised_add_assembly_msg() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + chain + .execute( + &governance::HandleMsg::AddAssemblyMsg { + name: "Some Assembly name".to_string(), + msg: "{}".to_string(), + assemblies: vec![Uint128::zero()], + padding: None, + }, + MockEnv::new( + // Sender is self + "random", + gov.clone(), + ), + ) + .is_err(); +} + +#[test] +fn set_assembly_msg() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + let original_msg = + get_assembly_msgs(&mut chain, &gov, Uint128::zero(), Uint128::new(1)).unwrap()[0].clone(); + + chain + .execute( + &governance::HandleMsg::SetAssemblyMsg { + id: Uint128::zero(), + name: Some("New name".to_string()), + msg: None, + assemblies: None, + padding: None, + }, + MockEnv::new( + // Sender is self + gov.address.clone(), + gov.clone(), + ), + ) + .unwrap(); + + let assemblies = get_assembly_msgs(&mut chain, &gov, Uint128::zero(), Uint128::new(1)).unwrap(); + + assert_eq!(assemblies.len(), 1); + + assert_ne!(original_msg.name, assemblies[0].name); + assert_eq!(original_msg.assemblies, assemblies[0].assemblies); + assert_eq!(original_msg.msg, assemblies[0].msg); +} + +#[test] +fn unauthorised_set_assembly_msg() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + chain + .execute( + &governance::HandleMsg::SetAssemblyMsg { + id: Uint128::zero(), + name: Some("New name".to_string()), + msg: None, + assemblies: None, + padding: None, + }, + MockEnv::new( + // Sender is self + "random", + gov.clone(), + ), + ) + .is_err(); +} diff --git a/contracts/governance/src/tests/handle/contract.rs b/contracts/governance/src/tests/handle/contract.rs new file mode 100644 index 000000000..2b8152d2e --- /dev/null +++ b/contracts/governance/src/tests/handle/contract.rs @@ -0,0 +1,315 @@ +use crate::tests::{admin_only_governance, get_contract}; +use cosmwasm_math_compat::Uint128; +use cosmwasm_std::HumanAddr; +use fadroma_ensemble::MockEnv; +use shade_protocol::{contract_interfaces::governance, utils::asset::Contract}; + +#[test] +fn add_contract() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + chain + .execute( + &governance::HandleMsg::AddContract { + name: "Contract".to_string(), + metadata: "some description".to_string(), + contract: Contract { + address: HumanAddr::from("contract"), + code_hash: "hash".to_string(), + }, + assemblies: None, + padding: None, + }, + MockEnv::new( + // Sender is self + gov.address.clone(), + gov.clone(), + ), + ) + .unwrap(); + + let contracts = get_contract(&mut chain, &gov, Uint128::zero(), Uint128::new(1)).unwrap(); + + assert_eq!(contracts.len(), 2); +} +#[test] +fn unauthorised_add_contract() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + chain + .execute( + &governance::HandleMsg::AddContract { + name: "Contract".to_string(), + metadata: "some description".to_string(), + contract: Contract { + address: HumanAddr::from("contract"), + code_hash: "hash".to_string(), + }, + assemblies: None, + padding: None, + }, + MockEnv::new( + // Sender is self + HumanAddr::from("random"), + gov.clone(), + ), + ) + .is_err(); +} +#[test] +fn set_contract() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + chain + .execute( + &governance::HandleMsg::AddContract { + name: "Contract".to_string(), + metadata: "some description".to_string(), + contract: Contract { + address: HumanAddr::from("contract"), + code_hash: "hash".to_string(), + }, + assemblies: None, + padding: None, + }, + MockEnv::new( + // Sender is self + gov.address.clone(), + gov.clone(), + ), + ) + .unwrap(); + + let old_contract = + get_contract(&mut chain, &gov, Uint128::new(1), Uint128::new(1)).unwrap()[0].clone(); + + chain + .execute( + &governance::HandleMsg::SetContract { + id: Uint128::new(1), + name: Some("New name".to_string()), + metadata: Some("New desc".to_string()), + contract: Some(Contract { + address: HumanAddr::from("new contract"), + code_hash: "other hash".to_string(), + }), + disable_assemblies: false, + assemblies: None, + padding: None, + }, + MockEnv::new( + // Sender is self + gov.address.clone(), + gov.clone(), + ), + ) + .unwrap(); + + let new_contract = + get_contract(&mut chain, &gov, Uint128::new(1), Uint128::new(1)).unwrap()[0].clone(); + + assert_ne!(old_contract.name, new_contract.name); + assert_ne!(old_contract.metadata, new_contract.metadata); + assert_ne!(old_contract.contract.address, new_contract.contract.address); + assert_ne!( + old_contract.contract.code_hash, + new_contract.contract.code_hash + ); +} + +#[test] +fn disable_contract_assemblies() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + chain + .execute( + &governance::HandleMsg::AddContract { + name: "Contract".to_string(), + metadata: "some description".to_string(), + contract: Contract { + address: HumanAddr::from("contract"), + code_hash: "hash".to_string(), + }, + assemblies: Some(vec![Uint128::zero()]), + padding: None, + }, + MockEnv::new( + // Sender is self + gov.address.clone(), + gov.clone(), + ), + ) + .unwrap(); + + let old_contract = + get_contract(&mut chain, &gov, Uint128::new(1), Uint128::new(1)).unwrap()[0].clone(); + + chain + .execute( + &governance::HandleMsg::SetContract { + id: Uint128::new(1), + name: Some("New name".to_string()), + metadata: Some("New desc".to_string()), + contract: Some(Contract { + address: HumanAddr::from("new contract"), + code_hash: "other hash".to_string(), + }), + disable_assemblies: true, + assemblies: None, + padding: None, + }, + MockEnv::new( + // Sender is self + gov.address.clone(), + gov.clone(), + ), + ) + .unwrap(); + + let new_contract = + get_contract(&mut chain, &gov, Uint128::new(1), Uint128::new(1)).unwrap()[0].clone(); + + assert_ne!(old_contract.name, new_contract.name); + assert_ne!(old_contract.metadata, new_contract.metadata); + assert_ne!(old_contract.contract.address, new_contract.contract.address); + assert_ne!( + old_contract.contract.code_hash, + new_contract.contract.code_hash + ); + assert_ne!(old_contract.assemblies, new_contract.assemblies); +} + +#[test] +fn enable_contract_assemblies() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + chain + .execute( + &governance::HandleMsg::AddContract { + name: "Contract".to_string(), + metadata: "some description".to_string(), + contract: Contract { + address: HumanAddr::from("contract"), + code_hash: "hash".to_string(), + }, + assemblies: None, + padding: None, + }, + MockEnv::new( + // Sender is self + gov.address.clone(), + gov.clone(), + ), + ) + .unwrap(); + + let old_contract = + get_contract(&mut chain, &gov, Uint128::new(1), Uint128::new(1)).unwrap()[0].clone(); + + chain + .execute( + &governance::HandleMsg::SetContract { + id: Uint128::new(1), + name: Some("New name".to_string()), + metadata: Some("New desc".to_string()), + contract: Some(Contract { + address: HumanAddr::from("new contract"), + code_hash: "other hash".to_string(), + }), + disable_assemblies: false, + assemblies: Some(vec![Uint128::zero()]), + padding: None, + }, + MockEnv::new( + // Sender is self + gov.address.clone(), + gov.clone(), + ), + ) + .unwrap(); + + let new_contract = + get_contract(&mut chain, &gov, Uint128::new(1), Uint128::new(1)).unwrap()[0].clone(); + + assert_ne!(old_contract.name, new_contract.name); + assert_ne!(old_contract.metadata, new_contract.metadata); + assert_ne!(old_contract.contract.address, new_contract.contract.address); + assert_ne!( + old_contract.contract.code_hash, + new_contract.contract.code_hash + ); + assert_ne!(old_contract.assemblies, new_contract.assemblies); +} + +#[test] +fn unauthorised_set_contract() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + chain + .execute( + &governance::HandleMsg::SetContract { + id: Uint128::new(1), + name: Some("New name".to_string()), + metadata: Some("New desc".to_string()), + contract: Some(Contract { + address: HumanAddr::from("new contract"), + code_hash: "other hash".to_string(), + }), + disable_assemblies: false, + assemblies: None, + padding: None, + }, + MockEnv::new( + // Sender is self + HumanAddr::from("random"), + gov.clone(), + ), + ) + .is_err(); +} +#[test] +fn add_contract_assemblies() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + chain + .execute( + &governance::HandleMsg::AddContract { + name: "Contract".to_string(), + metadata: "some description".to_string(), + contract: Contract { + address: HumanAddr::from("contract"), + code_hash: "hash".to_string(), + }, + assemblies: Some(vec![Uint128::zero()]), + padding: None, + }, + MockEnv::new( + // Sender is self + gov.address.clone(), + gov.clone(), + ), + ) + .unwrap(); + + let old_contract = + get_contract(&mut chain, &gov, Uint128::new(1), Uint128::new(1)).unwrap()[0].clone(); + + chain + .execute( + &governance::HandleMsg::AddContractAssemblies { + id: Uint128::new(1), + assemblies: vec![Uint128::new(1)], + }, + MockEnv::new( + // Sender is self + gov.address.clone(), + gov.clone(), + ), + ) + .unwrap(); + + let new_contract = + get_contract(&mut chain, &gov, Uint128::new(1), Uint128::new(1)).unwrap()[0].clone(); + + assert_ne!(old_contract.assemblies, new_contract.assemblies); +} diff --git a/contracts/governance/src/tests/handle/mod.rs b/contracts/governance/src/tests/handle/mod.rs new file mode 100644 index 000000000..3a3b8f809 --- /dev/null +++ b/contracts/governance/src/tests/handle/mod.rs @@ -0,0 +1,164 @@ +pub mod assembly; +pub mod assembly_msg; +pub mod contract; +pub mod profile; +pub mod proposal; + +use crate::tests::{admin_only_governance, get_config}; +use contract_harness::harness::snip20::Snip20; +use cosmwasm_std::HumanAddr; +use fadroma_ensemble::MockEnv; +use fadroma_platform_scrt::ContractLink; +use shade_protocol::{contract_interfaces::governance, utils::asset::Contract}; + +#[test] +fn init_contract() { + admin_only_governance().unwrap(); +} + +#[test] +fn set_config_msg() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + let old_config = get_config(&mut chain, &gov).unwrap(); + + let snip20 = chain.register(Box::new(Snip20)); + let snip20 = chain + .instantiate( + snip20.id, + &snip20_reference_impl::msg::InitMsg { + name: "funding_token".to_string(), + admin: None, + symbol: "FND".to_string(), + decimals: 6, + initial_balances: None, + prng_seed: Default::default(), + config: None, + }, + MockEnv::new("admin", ContractLink { + address: "funding_token".into(), + code_hash: snip20.code_hash, + }), + ) + .unwrap(); + + chain + .execute( + &governance::HandleMsg::SetConfig { + treasury: Some(HumanAddr::from("random")), + funding_token: Some(Contract { + address: snip20.address.clone(), + code_hash: snip20.code_hash.clone(), + }), + vote_token: Some(Contract { + address: snip20.address, + code_hash: snip20.code_hash, + }), + padding: None, + }, + MockEnv::new( + // Sender is self + gov.address.clone(), + gov.clone(), + ), + ) + .unwrap(); + + let new_config = get_config(&mut chain, &gov).unwrap(); + + assert_ne!(old_config.treasury, new_config.treasury); + assert_ne!(old_config.funding_token, new_config.funding_token); + assert_ne!(old_config.vote_token, new_config.vote_token); +} + +#[test] +fn unauthorised_set_config_msg() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + chain + .execute( + &governance::HandleMsg::SetConfig { + treasury: None, + funding_token: None, + vote_token: None, + padding: None, + }, + MockEnv::new( + // Sender is self + "random", + gov.clone(), + ), + ) + .is_err(); +} + +#[test] +fn reject_disable_config_tokens() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + let snip20 = chain.register(Box::new(Snip20)); + let snip20 = chain + .instantiate( + snip20.id, + &snip20_reference_impl::msg::InitMsg { + name: "funding_token".to_string(), + admin: None, + symbol: "FND".to_string(), + decimals: 6, + initial_balances: None, + prng_seed: Default::default(), + config: None, + }, + MockEnv::new("admin", ContractLink { + address: "funding_token".into(), + code_hash: snip20.code_hash, + }), + ) + .unwrap(); + + chain + .execute( + &governance::HandleMsg::SetConfig { + treasury: Some(HumanAddr::from("random")), + funding_token: Some(Contract { + address: snip20.address.clone(), + code_hash: snip20.code_hash.clone(), + }), + vote_token: Some(Contract { + address: snip20.address, + code_hash: snip20.code_hash, + }), + padding: None, + }, + MockEnv::new( + // Sender is self + gov.address.clone(), + gov.clone(), + ), + ) + .unwrap(); + + let old_config = get_config(&mut chain, &gov).unwrap(); + + chain + .execute( + &governance::HandleMsg::SetConfig { + treasury: None, + funding_token: None, + vote_token: None, + padding: None, + }, + MockEnv::new( + // Sender is self + gov.address.clone(), + gov.clone(), + ), + ) + .unwrap(); + + let new_config = get_config(&mut chain, &gov).unwrap(); + + assert_eq!(old_config.treasury, new_config.treasury); + assert_eq!(old_config.funding_token, new_config.funding_token); + assert_eq!(old_config.vote_token, new_config.vote_token); +} diff --git a/contracts/governance/src/tests/handle/profile.rs b/contracts/governance/src/tests/handle/profile.rs new file mode 100644 index 000000000..63dc35092 --- /dev/null +++ b/contracts/governance/src/tests/handle/profile.rs @@ -0,0 +1,476 @@ +use crate::tests::{admin_only_governance, get_profiles}; +use cosmwasm_math_compat::Uint128; +use cosmwasm_std::HumanAddr; +use fadroma_ensemble::MockEnv; +use shade_protocol::contract_interfaces::{ + governance, + governance::profile::{Count, Profile, UpdateFundProfile, UpdateProfile, UpdateVoteProfile}, +}; + +#[test] +fn add_profile() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + chain + .execute( + &governance::HandleMsg::AddProfile { + profile: Profile { + name: "Other Profile".to_string(), + enabled: false, + assembly: None, + funding: None, + token: None, + cancel_deadline: 0, + }, + padding: None, + }, + MockEnv::new( + // Sender is self + gov.address.clone(), + gov.clone(), + ), + ) + .unwrap(); + + let profiles = get_profiles(&mut chain, &gov, Uint128::zero(), Uint128::new(10)).unwrap(); + + assert_eq!(profiles.len(), 3); +} +#[test] +fn unauthorised_add_profile() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + chain + .execute( + &governance::HandleMsg::AddProfile { + profile: Profile { + name: "Other Profile".to_string(), + enabled: false, + assembly: None, + funding: None, + token: None, + cancel_deadline: 0, + }, + padding: None, + }, + MockEnv::new( + // Sender is self + HumanAddr::from("random"), + gov.clone(), + ), + ) + .is_err(); +} + +#[test] +fn set_profile() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + let old_profile = + get_profiles(&mut chain, &gov, Uint128::new(1), Uint128::new(1)).unwrap()[0].clone(); + + chain + .execute( + &governance::HandleMsg::SetProfile { + id: Uint128::new(1), + profile: UpdateProfile { + name: Some("New Name".to_string()), + enabled: None, + disable_assembly: false, + assembly: None, + disable_funding: false, + funding: None, + disable_token: false, + token: None, + cancel_deadline: None, + }, + padding: None, + }, + MockEnv::new( + // Sender is self + gov.address.clone(), + gov.clone(), + ), + ) + .unwrap(); + + let new_profile = + get_profiles(&mut chain, &gov, Uint128::new(1), Uint128::new(1)).unwrap()[0].clone(); + + assert_ne!(new_profile.name, old_profile.name); + assert_eq!(new_profile.assembly, old_profile.assembly); + assert_eq!(new_profile.funding, old_profile.funding); + assert_eq!(new_profile.token, old_profile.token); + assert_eq!(new_profile.enabled, old_profile.enabled); + assert_eq!(new_profile.cancel_deadline, old_profile.cancel_deadline); +} + +#[test] +fn unauthorised_set_profile() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + chain + .execute( + &governance::HandleMsg::SetProfile { + id: Uint128::new(1), + profile: UpdateProfile { + name: Some("New Name".to_string()), + enabled: None, + disable_assembly: false, + assembly: None, + disable_funding: false, + funding: None, + disable_token: false, + token: None, + cancel_deadline: None, + }, + padding: None, + }, + MockEnv::new( + // Sender is self + HumanAddr::from("random"), + gov.clone(), + ), + ) + .is_err(); +} + +#[test] +fn set_profile_disable_assembly() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + chain + .execute( + &governance::HandleMsg::SetProfile { + id: Uint128::new(1), + profile: UpdateProfile { + name: None, + enabled: None, + disable_assembly: false, + assembly: Some(UpdateVoteProfile { + deadline: Some(0), + threshold: Some(Count::LiteralCount { + count: Uint128::zero(), + }), + yes_threshold: Some(Count::LiteralCount { + count: Uint128::zero(), + }), + veto_threshold: Some(Count::LiteralCount { + count: Uint128::zero(), + }), + }), + disable_funding: false, + funding: None, + disable_token: false, + token: None, + cancel_deadline: None, + }, + padding: None, + }, + MockEnv::new( + // Sender is self + gov.address.clone(), + gov.clone(), + ), + ) + .unwrap(); + + let old_profile = + get_profiles(&mut chain, &gov, Uint128::new(1), Uint128::new(1)).unwrap()[0].clone(); + + chain + .execute( + &governance::HandleMsg::SetProfile { + id: Uint128::new(1), + profile: UpdateProfile { + name: None, + enabled: None, + disable_assembly: true, + assembly: None, + disable_funding: false, + funding: None, + disable_token: false, + token: None, + cancel_deadline: None, + }, + padding: None, + }, + MockEnv::new( + // Sender is self + gov.address.clone(), + gov.clone(), + ), + ) + .unwrap(); + + let new_profile = + get_profiles(&mut chain, &gov, Uint128::new(1), Uint128::new(1)).unwrap()[0].clone(); + + assert_eq!(new_profile.name, old_profile.name); + assert_ne!(new_profile.assembly, old_profile.assembly); + assert_eq!(new_profile.funding, old_profile.funding); + assert_eq!(new_profile.token, old_profile.token); + assert_eq!(new_profile.enabled, old_profile.enabled); + assert_eq!(new_profile.cancel_deadline, old_profile.cancel_deadline); +} + +#[test] +fn set_profile_set_incomplete_assembly() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + chain + .execute( + &governance::HandleMsg::SetProfile { + id: Uint128::new(1), + profile: UpdateProfile { + name: None, + enabled: None, + disable_assembly: false, + assembly: Some(UpdateVoteProfile { + deadline: Some(0), + threshold: None, + yes_threshold: None, + veto_threshold: Some(Count::LiteralCount { + count: Uint128::zero(), + }), + }), + disable_funding: false, + funding: None, + disable_token: false, + token: None, + cancel_deadline: None, + }, + padding: None, + }, + MockEnv::new( + // Sender is self + gov.address.clone(), + gov.clone(), + ), + ) + .is_err(); +} + +#[test] +fn set_profile_disable_token() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + chain + .execute( + &governance::HandleMsg::SetProfile { + id: Uint128::new(1), + profile: UpdateProfile { + name: None, + enabled: None, + disable_assembly: false, + assembly: None, + disable_funding: false, + funding: None, + disable_token: false, + token: Some(UpdateVoteProfile { + deadline: Some(0), + threshold: Some(Count::LiteralCount { + count: Uint128::zero(), + }), + yes_threshold: Some(Count::LiteralCount { + count: Uint128::zero(), + }), + veto_threshold: Some(Count::LiteralCount { + count: Uint128::zero(), + }), + }), + cancel_deadline: None, + }, + padding: None, + }, + MockEnv::new( + // Sender is self + gov.address.clone(), + gov.clone(), + ), + ) + .unwrap(); + + let old_profile = + get_profiles(&mut chain, &gov, Uint128::new(1), Uint128::new(1)).unwrap()[0].clone(); + + chain + .execute( + &governance::HandleMsg::SetProfile { + id: Uint128::new(1), + profile: UpdateProfile { + name: None, + enabled: None, + disable_assembly: false, + assembly: None, + disable_funding: false, + funding: None, + disable_token: true, + token: None, + cancel_deadline: None, + }, + padding: None, + }, + MockEnv::new( + // Sender is self + gov.address.clone(), + gov.clone(), + ), + ) + .unwrap(); + + let new_profile = + get_profiles(&mut chain, &gov, Uint128::new(1), Uint128::new(1)).unwrap()[0].clone(); + + assert_eq!(new_profile.name, old_profile.name); + assert_eq!(new_profile.assembly, old_profile.assembly); + assert_eq!(new_profile.funding, old_profile.funding); + assert_ne!(new_profile.token, old_profile.token); + assert_eq!(new_profile.enabled, old_profile.enabled); + assert_eq!(new_profile.cancel_deadline, old_profile.cancel_deadline); +} + +#[test] +fn set_profile_set_incomplete_token() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + chain + .execute( + &governance::HandleMsg::SetProfile { + id: Uint128::new(1), + profile: UpdateProfile { + name: None, + enabled: None, + disable_assembly: false, + assembly: None, + disable_funding: false, + funding: None, + disable_token: false, + token: Some(UpdateVoteProfile { + deadline: Some(0), + threshold: None, + yes_threshold: None, + veto_threshold: Some(Count::LiteralCount { + count: Uint128::zero(), + }), + }), + cancel_deadline: None, + }, + padding: None, + }, + MockEnv::new( + // Sender is self + gov.address.clone(), + gov.clone(), + ), + ) + .is_err(); +} + +#[test] +fn set_profile_disable_funding() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + chain + .execute( + &governance::HandleMsg::SetProfile { + id: Uint128::new(1), + profile: UpdateProfile { + name: None, + enabled: None, + disable_assembly: false, + assembly: None, + disable_funding: false, + funding: Some(UpdateFundProfile { + deadline: Some(0), + required: Some(Uint128::zero()), + privacy: Some(true), + veto_deposit_loss: Some(Uint128::zero()), + }), + disable_token: false, + token: None, + cancel_deadline: None, + }, + padding: None, + }, + MockEnv::new( + // Sender is self + gov.address.clone(), + gov.clone(), + ), + ) + .unwrap(); + + let old_profile = + get_profiles(&mut chain, &gov, Uint128::new(1), Uint128::new(1)).unwrap()[0].clone(); + + chain + .execute( + &governance::HandleMsg::SetProfile { + id: Uint128::new(1), + profile: UpdateProfile { + name: None, + enabled: None, + disable_assembly: false, + assembly: None, + disable_funding: true, + funding: None, + disable_token: false, + token: None, + cancel_deadline: None, + }, + padding: None, + }, + MockEnv::new( + // Sender is self + gov.address.clone(), + gov.clone(), + ), + ) + .unwrap(); + + let new_profile = + get_profiles(&mut chain, &gov, Uint128::new(1), Uint128::new(1)).unwrap()[0].clone(); + + assert_eq!(new_profile.name, old_profile.name); + assert_eq!(new_profile.assembly, old_profile.assembly); + assert_ne!(new_profile.funding, old_profile.funding); + assert_eq!(new_profile.token, old_profile.token); + assert_eq!(new_profile.enabled, old_profile.enabled); + assert_eq!(new_profile.cancel_deadline, old_profile.cancel_deadline); +} + +#[test] +fn set_profile_set_incomplete_fuding() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + chain + .execute( + &governance::HandleMsg::SetProfile { + id: Uint128::new(1), + profile: UpdateProfile { + name: None, + enabled: None, + disable_assembly: false, + assembly: None, + disable_funding: false, + funding: Some(UpdateFundProfile { + deadline: Some(0), + required: None, + privacy: Some(true), + veto_deposit_loss: None, + }), + disable_token: false, + token: None, + cancel_deadline: None, + }, + padding: None, + }, + MockEnv::new( + // Sender is self + gov.address.clone(), + gov.clone(), + ), + ) + .is_err(); +} diff --git a/contracts/governance/src/tests/handle/proposal/assembly_voting.rs b/contracts/governance/src/tests/handle/proposal/assembly_voting.rs new file mode 100644 index 000000000..e43711148 --- /dev/null +++ b/contracts/governance/src/tests/handle/proposal/assembly_voting.rs @@ -0,0 +1,1022 @@ +use crate::tests::{ + admin_only_governance, + get_assemblies, + get_proposals, + gov_generic_proposal, + gov_msg_proposal, + init_governance, +}; +use contract_harness::harness::{governance::Governance, snip20_staking::Snip20Staking}; +use cosmwasm_math_compat::Uint128; +use cosmwasm_std::{to_binary, Binary, HumanAddr, StdResult}; +use fadroma_ensemble::{ContractEnsemble, MockEnv}; +use fadroma_platform_scrt::ContractLink; +use shade_protocol::{ + contract_interfaces::{ + governance, + governance::{ + profile::{Count, FundProfile, Profile, UpdateProfile, UpdateVoteProfile, VoteProfile}, + proposal::{ProposalMsg, Status}, + vote::Vote, + InitMsg, + }, + }, + utils::asset::Contract, +}; + +fn init_assembly_governance_with_proposal() -> StdResult<(ContractEnsemble, ContractLink)> +{ + let (mut chain, gov) = init_governance(InitMsg { + treasury: HumanAddr::from("treasury"), + admin_members: vec![ + HumanAddr::from("alpha"), + HumanAddr::from("beta"), + HumanAddr::from("charlie"), + ], + admin_profile: Profile { + name: "admin".to_string(), + enabled: true, + assembly: Some(VoteProfile { + deadline: 10000, + threshold: Count::LiteralCount { + count: Uint128::new(2), + }, + yes_threshold: Count::LiteralCount { + count: Uint128::new(2), + }, + veto_threshold: Count::LiteralCount { + count: Uint128::new(3), + }, + }), + funding: None, + token: None, + cancel_deadline: 0, + }, + public_profile: Profile { + name: "public".to_string(), + enabled: false, + assembly: None, + funding: None, + token: None, + cancel_deadline: 0, + }, + funding_token: None, + vote_token: None, + })?; + + chain.execute( + &governance::HandleMsg::AssemblyProposal { + assembly: Uint128::new(1), + title: "Title".to_string(), + metadata: "Text only proposal".to_string(), + msgs: None, + padding: None, + }, + MockEnv::new("alpha", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + )?; + + Ok((chain, gov)) +} + +#[test] +fn assembly_voting() { + let (mut chain, gov) = init_assembly_governance_with_proposal().unwrap(); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + match prop.status { + Status::AssemblyVote { .. } => assert!(true), + _ => assert!(false), + }; +} + +#[test] +fn update_before_deadline() { + let (mut chain, gov) = init_assembly_governance_with_proposal().unwrap(); + + assert!( + chain + .execute( + &governance::HandleMsg::Update { + proposal: Uint128::new(0), + padding: None + }, + MockEnv::new("alpha", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }) + ) + .is_err() + ); +} + +#[test] +fn update_after_deadline() { + let (mut chain, gov) = init_assembly_governance_with_proposal().unwrap(); + + chain.block().time += 30000; + + assert!( + chain + .execute( + &governance::HandleMsg::Update { + proposal: Uint128::new(0), + padding: None + }, + MockEnv::new("alpha", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }) + ) + .is_ok() + ); +} + +#[test] +fn invalid_vote() { + let (mut chain, gov) = init_assembly_governance_with_proposal().unwrap(); + + assert!( + chain + .execute( + &governance::HandleMsg::AssemblyVote { + proposal: Uint128::new(0), + vote: Vote { + yes: Uint128::new(1), + no: Uint128::new(1), + no_with_veto: Default::default(), + abstain: Default::default() + }, + padding: None + }, + MockEnv::new("alpha", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }) + ) + .is_err() + ); +} + +#[test] +fn unauthorised_vote() { + let (mut chain, gov) = init_assembly_governance_with_proposal().unwrap(); + + assert!( + chain + .execute( + &governance::HandleMsg::AssemblyVote { + proposal: Uint128::new(0), + vote: Vote { + yes: Uint128::zero(), + no: Uint128::new(1), + no_with_veto: Uint128::zero(), + abstain: Uint128::zero() + }, + padding: None + }, + MockEnv::new("foxtrot", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }) + ) + .is_err() + ); +} + +#[test] +fn vote_after_deadline() { + let (mut chain, gov) = init_assembly_governance_with_proposal().unwrap(); + + chain.block().time += 30000; + + assert!( + chain + .execute( + &governance::HandleMsg::AssemblyVote { + proposal: Uint128::new(0), + vote: Vote { + yes: Uint128::zero(), + no: Uint128::new(1), + no_with_veto: Uint128::zero(), + abstain: Uint128::zero() + }, + padding: None + }, + MockEnv::new("alpha", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }) + ) + .is_err() + ); +} + +#[test] +fn vote_yes() { + let (mut chain, gov) = init_assembly_governance_with_proposal().unwrap(); + + chain + .execute( + &governance::HandleMsg::AssemblyVote { + proposal: Uint128::new(0), + vote: Vote { + yes: Uint128::new(1), + no: Uint128::zero(), + no_with_veto: Uint128::zero(), + abstain: Uint128::zero(), + }, + padding: None, + }, + MockEnv::new("alpha", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + match prop.status { + Status::AssemblyVote { .. } => assert!(true), + _ => assert!(false), + }; + + assert_eq!( + prop.assembly_vote_tally, + Some(Vote { + yes: Uint128::new(1), + no: Uint128::zero(), + no_with_veto: Uint128::zero(), + abstain: Uint128::zero() + }) + ) +} + +#[test] +fn vote_abstain() { + let (mut chain, gov) = init_assembly_governance_with_proposal().unwrap(); + + chain + .execute( + &governance::HandleMsg::AssemblyVote { + proposal: Uint128::new(0), + vote: Vote { + yes: Uint128::zero(), + no: Uint128::zero(), + no_with_veto: Uint128::zero(), + abstain: Uint128::new(1), + }, + padding: None, + }, + MockEnv::new("alpha", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + match prop.status { + Status::AssemblyVote { .. } => assert!(true), + _ => assert!(false), + }; + + assert_eq!( + prop.assembly_vote_tally, + Some(Vote { + yes: Uint128::zero(), + no: Uint128::zero(), + no_with_veto: Uint128::zero(), + abstain: Uint128::new(1) + }) + ) +} + +#[test] +fn vote_no() { + let (mut chain, gov) = init_assembly_governance_with_proposal().unwrap(); + + chain + .execute( + &governance::HandleMsg::AssemblyVote { + proposal: Uint128::new(0), + vote: Vote { + yes: Uint128::zero(), + no: Uint128::new(1), + no_with_veto: Uint128::zero(), + abstain: Uint128::zero(), + }, + padding: None, + }, + MockEnv::new("alpha", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + match prop.status { + Status::AssemblyVote { .. } => assert!(true), + _ => assert!(false), + }; + + assert_eq!( + prop.assembly_vote_tally, + Some(Vote { + yes: Uint128::zero(), + no: Uint128::new(1), + no_with_veto: Uint128::zero(), + abstain: Uint128::zero() + }) + ) +} + +#[test] +fn vote_veto() { + let (mut chain, gov) = init_assembly_governance_with_proposal().unwrap(); + + chain + .execute( + &governance::HandleMsg::AssemblyVote { + proposal: Uint128::new(0), + vote: Vote { + yes: Uint128::zero(), + no: Uint128::zero(), + no_with_veto: Uint128::new(1), + abstain: Uint128::zero(), + }, + padding: None, + }, + MockEnv::new("alpha", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + match prop.status { + Status::AssemblyVote { .. } => assert!(true), + _ => assert!(false), + }; + + assert_eq!( + prop.assembly_vote_tally, + Some(Vote { + yes: Uint128::zero(), + no: Uint128::zero(), + no_with_veto: Uint128::new(1), + abstain: Uint128::zero() + }) + ) +} + +#[test] +fn vote_passed() { + let (mut chain, gov) = init_assembly_governance_with_proposal().unwrap(); + + chain + .execute( + &governance::HandleMsg::AssemblyVote { + proposal: Uint128::new(0), + vote: Vote { + yes: Uint128::new(1), + no: Uint128::zero(), + no_with_veto: Uint128::zero(), + abstain: Uint128::zero(), + }, + padding: None, + }, + MockEnv::new("alpha", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + chain + .execute( + &governance::HandleMsg::AssemblyVote { + proposal: Uint128::new(0), + vote: Vote { + yes: Uint128::new(1), + no: Uint128::zero(), + no_with_veto: Uint128::zero(), + abstain: Uint128::zero(), + }, + padding: None, + }, + MockEnv::new("beta", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + chain.block().time += 30000; + + chain + .execute( + &governance::HandleMsg::Update { + proposal: Uint128::zero(), + padding: None, + }, + MockEnv::new("beta", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + match prop.status { + Status::Passed { .. } => assert!(true), + _ => assert!(false), + }; +} + +#[test] +fn vote_abstained() { + let (mut chain, gov) = init_assembly_governance_with_proposal().unwrap(); + + chain + .execute( + &governance::HandleMsg::AssemblyVote { + proposal: Uint128::new(0), + vote: Vote { + yes: Uint128::zero(), + no: Uint128::zero(), + no_with_veto: Uint128::zero(), + abstain: Uint128::new(1), + }, + padding: None, + }, + MockEnv::new("alpha", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + chain + .execute( + &governance::HandleMsg::AssemblyVote { + proposal: Uint128::new(0), + vote: Vote { + yes: Uint128::zero(), + no: Uint128::zero(), + no_with_veto: Uint128::zero(), + abstain: Uint128::new(1), + }, + padding: None, + }, + MockEnv::new("beta", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + chain.block().time += 30000; + + chain + .execute( + &governance::HandleMsg::Update { + proposal: Uint128::zero(), + padding: None, + }, + MockEnv::new("beta", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + match prop.status { + Status::Rejected { .. } => assert!(true), + _ => assert!(false), + }; +} + +#[test] +fn vote_rejected() { + let (mut chain, gov) = init_assembly_governance_with_proposal().unwrap(); + + chain + .execute( + &governance::HandleMsg::AssemblyVote { + proposal: Uint128::new(0), + vote: Vote { + yes: Uint128::zero(), + no: Uint128::new(1), + no_with_veto: Uint128::zero(), + abstain: Uint128::zero(), + }, + padding: None, + }, + MockEnv::new("alpha", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + chain + .execute( + &governance::HandleMsg::AssemblyVote { + proposal: Uint128::new(0), + vote: Vote { + yes: Uint128::zero(), + no: Uint128::new(1), + no_with_veto: Uint128::zero(), + abstain: Uint128::zero(), + }, + padding: None, + }, + MockEnv::new("beta", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + chain.block().time += 30000; + + chain + .execute( + &governance::HandleMsg::Update { + proposal: Uint128::zero(), + padding: None, + }, + MockEnv::new("beta", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + match prop.status { + Status::Rejected { .. } => assert!(true), + _ => assert!(false), + }; +} + +#[test] +fn vote_vetoed() { + let (mut chain, gov) = init_assembly_governance_with_proposal().unwrap(); + + chain + .execute( + &governance::HandleMsg::AssemblyVote { + proposal: Uint128::new(0), + vote: Vote { + yes: Uint128::zero(), + no: Uint128::zero(), + no_with_veto: Uint128::new(1), + abstain: Uint128::zero(), + }, + padding: None, + }, + MockEnv::new("alpha", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + chain + .execute( + &governance::HandleMsg::AssemblyVote { + proposal: Uint128::new(0), + vote: Vote { + yes: Uint128::zero(), + no: Uint128::zero(), + no_with_veto: Uint128::new(1), + abstain: Uint128::zero(), + }, + padding: None, + }, + MockEnv::new("beta", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + chain.block().time += 30000; + + chain + .execute( + &governance::HandleMsg::Update { + proposal: Uint128::zero(), + padding: None, + }, + MockEnv::new("beta", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + match prop.status { + // NOTE: assembly votes cannot be vetoed + Status::Rejected { .. } => assert!(true), + _ => assert!(false), + }; +} + +#[test] +fn vote_no_quorum() { + let (mut chain, gov) = init_assembly_governance_with_proposal().unwrap(); + + chain + .execute( + &governance::HandleMsg::AssemblyVote { + proposal: Uint128::new(0), + vote: Vote { + yes: Uint128::zero(), + no: Uint128::zero(), + no_with_veto: Uint128::new(1), + abstain: Uint128::zero(), + }, + padding: None, + }, + MockEnv::new("alpha", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + chain.block().time += 30000; + + chain + .execute( + &governance::HandleMsg::Update { + proposal: Uint128::new(0), + padding: None, + }, + MockEnv::new("alpha", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + assert_eq!(prop.status, Status::Expired); +} + +#[test] +fn vote_total() { + let (mut chain, gov) = init_assembly_governance_with_proposal().unwrap(); + + chain + .execute( + &governance::HandleMsg::AssemblyVote { + proposal: Uint128::new(0), + vote: Vote { + yes: Uint128::zero(), + no: Uint128::zero(), + no_with_veto: Uint128::new(1), + abstain: Uint128::zero(), + }, + padding: None, + }, + MockEnv::new("alpha", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + chain + .execute( + &governance::HandleMsg::AssemblyVote { + proposal: Uint128::new(0), + vote: Vote { + yes: Uint128::new(1), + no: Uint128::zero(), + no_with_veto: Uint128::zero(), + abstain: Uint128::zero(), + }, + padding: None, + }, + MockEnv::new("beta", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + chain + .execute( + &governance::HandleMsg::AssemblyVote { + proposal: Uint128::new(0), + vote: Vote { + yes: Uint128::new(1), + no: Uint128::zero(), + no_with_veto: Uint128::zero(), + abstain: Uint128::zero(), + }, + padding: None, + }, + MockEnv::new("charlie", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + match prop.status { + Status::AssemblyVote { .. } => assert!(true), + _ => assert!(false), + }; + + assert_eq!( + prop.assembly_vote_tally, + Some(Vote { + yes: Uint128::new(2), + no: Uint128::zero(), + no_with_veto: Uint128::new(1), + abstain: Uint128::zero() + }) + ) +} + +#[test] +fn update_vote() { + let (mut chain, gov) = init_assembly_governance_with_proposal().unwrap(); + + chain + .execute( + &governance::HandleMsg::AssemblyVote { + proposal: Uint128::new(0), + vote: Vote { + yes: Uint128::zero(), + no: Uint128::zero(), + no_with_veto: Uint128::new(1), + abstain: Uint128::zero(), + }, + padding: None, + }, + MockEnv::new("alpha", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + assert_eq!( + prop.assembly_vote_tally, + Some(Vote { + yes: Uint128::zero(), + no: Uint128::zero(), + no_with_veto: Uint128::new(1), + abstain: Uint128::zero() + }) + ); + + chain + .execute( + &governance::HandleMsg::AssemblyVote { + proposal: Uint128::new(0), + vote: Vote { + yes: Uint128::new(1), + no: Uint128::zero(), + no_with_veto: Uint128::zero(), + abstain: Uint128::zero(), + }, + padding: None, + }, + MockEnv::new("alpha", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + assert_eq!( + prop.assembly_vote_tally, + Some(Vote { + yes: Uint128::new(1), + no: Uint128::zero(), + no_with_veto: Uint128::zero(), + abstain: Uint128::zero() + }) + ); +} + +#[test] +fn vote_count() { + let (mut chain, gov) = init_assembly_governance_with_proposal().unwrap(); + + chain + .execute( + &governance::HandleMsg::AssemblyVote { + proposal: Uint128::new(0), + vote: Vote { + yes: Uint128::new(1), + no: Uint128::zero(), + no_with_veto: Uint128::zero(), + abstain: Uint128::zero(), + }, + padding: None, + }, + MockEnv::new("alpha", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + chain + .execute( + &governance::HandleMsg::AssemblyVote { + proposal: Uint128::new(0), + vote: Vote { + yes: Uint128::new(1), + no: Uint128::zero(), + no_with_veto: Uint128::zero(), + abstain: Uint128::zero(), + }, + padding: None, + }, + MockEnv::new("beta", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + chain.block().time += 30000; + + chain + .execute( + &governance::HandleMsg::Update { + proposal: Uint128::zero(), + padding: None, + }, + MockEnv::new("beta", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + match prop.status { + Status::Passed { .. } => assert!(true), + _ => assert!(false), + }; +} + +#[test] +fn vote_count_percentage() { + let (mut chain, gov) = init_governance(InitMsg { + treasury: HumanAddr::from("treasury"), + admin_members: vec![ + HumanAddr::from("alpha"), + HumanAddr::from("beta"), + HumanAddr::from("charlie"), + ], + admin_profile: Profile { + name: "admin".to_string(), + enabled: true, + assembly: Some(VoteProfile { + deadline: 10000, + threshold: Count::Percentage { percent: 6500 }, + yes_threshold: Count::Percentage { percent: 6500 }, + veto_threshold: Count::Percentage { percent: 6500 }, + }), + funding: None, + token: None, + cancel_deadline: 0, + }, + public_profile: Profile { + name: "public".to_string(), + enabled: false, + assembly: None, + funding: None, + token: None, + cancel_deadline: 0, + }, + funding_token: None, + vote_token: None, + }) + .unwrap(); + + chain + .execute( + &governance::HandleMsg::AssemblyProposal { + assembly: Uint128::new(1), + title: "Title".to_string(), + metadata: "Text only proposal".to_string(), + msgs: None, + padding: None, + }, + MockEnv::new("alpha", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + chain + .execute( + &governance::HandleMsg::AssemblyVote { + proposal: Uint128::new(0), + vote: Vote { + yes: Uint128::new(1), + no: Uint128::zero(), + no_with_veto: Uint128::zero(), + abstain: Uint128::zero(), + }, + padding: None, + }, + MockEnv::new("alpha", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + chain + .execute( + &governance::HandleMsg::AssemblyVote { + proposal: Uint128::new(0), + vote: Vote { + yes: Uint128::new(1), + no: Uint128::zero(), + no_with_veto: Uint128::zero(), + abstain: Uint128::zero(), + }, + padding: None, + }, + MockEnv::new("beta", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + chain.block().time += 30000; + + chain + .execute( + &governance::HandleMsg::Update { + proposal: Uint128::zero(), + padding: None, + }, + MockEnv::new("beta", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + match prop.status { + Status::Passed { .. } => assert!(true), + _ => assert!(false), + }; +} diff --git a/contracts/governance/src/tests/handle/proposal/funding.rs b/contracts/governance/src/tests/handle/proposal/funding.rs new file mode 100644 index 000000000..7f08afc47 --- /dev/null +++ b/contracts/governance/src/tests/handle/proposal/funding.rs @@ -0,0 +1,739 @@ +use crate::tests::{ + admin_only_governance, + get_assemblies, + get_proposals, + gov_generic_proposal, + gov_msg_proposal, + init_governance, +}; +use contract_harness::harness::{governance::Governance, snip20::Snip20}; +use cosmwasm_math_compat::Uint128; +use cosmwasm_std::{to_binary, Binary, HumanAddr, StdResult}; +use fadroma_ensemble::{ContractEnsemble, MockEnv}; +use fadroma_platform_scrt::ContractLink; +use shade_protocol::{ + contract_interfaces::{ + governance, + governance::{ + profile::{Count, FundProfile, Profile, UpdateProfile, UpdateVoteProfile, VoteProfile}, + proposal::{ProposalMsg, Status}, + vote::Vote, + InitMsg, + }, + }, + utils::asset::Contract, +}; + +fn init_funding_governance_with_proposal() -> StdResult<( + ContractEnsemble, + ContractLink, + ContractLink, +)> { + let mut chain = ContractEnsemble::new(50); + + // Register snip20 + let snip20 = chain.register(Box::new(Snip20)); + let snip20 = chain.instantiate( + snip20.id, + &snip20_reference_impl::msg::InitMsg { + name: "funding_token".to_string(), + admin: None, + symbol: "FND".to_string(), + decimals: 6, + initial_balances: Some(vec![ + snip20_reference_impl::msg::InitialBalance { + address: HumanAddr::from("alpha"), + amount: cosmwasm_std::Uint128(10000), + }, + snip20_reference_impl::msg::InitialBalance { + address: HumanAddr::from("beta"), + amount: cosmwasm_std::Uint128(10000), + }, + snip20_reference_impl::msg::InitialBalance { + address: HumanAddr::from("charlie"), + amount: cosmwasm_std::Uint128(10000), + }, + ]), + prng_seed: Default::default(), + config: None, + }, + MockEnv::new("admin", ContractLink { + address: "funding_token".into(), + code_hash: snip20.code_hash, + }), + )?; + + // Register governance + let gov = chain.register(Box::new(Governance)); + let gov = chain.instantiate( + gov.id, + &InitMsg { + treasury: HumanAddr::from("treasury"), + admin_members: vec![ + HumanAddr::from("alpha"), + HumanAddr::from("beta"), + HumanAddr::from("charlie"), + ], + admin_profile: Profile { + name: "admin".to_string(), + enabled: true, + assembly: None, + funding: Some(FundProfile { + deadline: 1000, + required: Uint128::new(2000), + privacy: false, + veto_deposit_loss: Default::default(), + }), + token: None, + cancel_deadline: 0, + }, + public_profile: Profile { + name: "public".to_string(), + enabled: false, + assembly: None, + funding: None, + token: None, + cancel_deadline: 0, + }, + funding_token: Some(Contract { + address: snip20.address.clone(), + code_hash: snip20.code_hash.clone(), + }), + vote_token: None, + }, + MockEnv::new("admin", ContractLink { + address: "gov".into(), + code_hash: gov.code_hash, + }), + )?; + + chain.execute( + &governance::HandleMsg::AssemblyProposal { + assembly: Uint128::new(1), + title: "Title".to_string(), + metadata: "Text only proposal".to_string(), + msgs: None, + padding: None, + }, + MockEnv::new("alpha", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + )?; + + chain.execute( + &snip20_reference_impl::msg::HandleMsg::SetViewingKey { + key: "password".to_string(), + padding: None, + }, + MockEnv::new("alpha", ContractLink { + address: snip20.address.clone(), + code_hash: snip20.code_hash.clone(), + }), + )?; + + chain.execute( + &snip20_reference_impl::msg::HandleMsg::SetViewingKey { + key: "password".to_string(), + padding: None, + }, + MockEnv::new("beta", ContractLink { + address: snip20.address.clone(), + code_hash: snip20.code_hash.clone(), + }), + )?; + + chain.execute( + &snip20_reference_impl::msg::HandleMsg::SetViewingKey { + key: "password".to_string(), + padding: None, + }, + MockEnv::new("charlie", ContractLink { + address: snip20.address.clone(), + code_hash: snip20.code_hash.clone(), + }), + )?; + + Ok((chain, gov, snip20)) +} + +#[test] +fn assembly_to_funding_transition() { + let (mut chain, gov, snip20) = init_funding_governance_with_proposal().unwrap(); + chain + .execute( + &governance::HandleMsg::SetProfile { + id: Uint128::new(1), + profile: UpdateProfile { + name: None, + enabled: None, + disable_assembly: false, + assembly: Some(UpdateVoteProfile { + deadline: Some(1000), + threshold: Some(Count::LiteralCount { + count: Uint128::new(1), + }), + yes_threshold: Some(Count::LiteralCount { + count: Uint128::new(1), + }), + veto_threshold: Some(Count::LiteralCount { + count: Uint128::new(1), + }), + }), + disable_funding: false, + funding: None, + disable_token: false, + token: None, + cancel_deadline: None, + }, + padding: None, + }, + MockEnv::new( + // Sender is self + gov.address.clone(), + gov.clone(), + ), + ) + .unwrap(); + + chain + .execute( + &governance::HandleMsg::AssemblyProposal { + assembly: Uint128::new(1), + title: "Title".to_string(), + metadata: "Text only proposal".to_string(), + msgs: None, + padding: None, + }, + MockEnv::new("alpha", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + chain + .execute( + &governance::HandleMsg::AssemblyVote { + proposal: Uint128::new(1), + vote: Vote { + yes: Uint128::new(1), + no: Uint128::zero(), + no_with_veto: Uint128::zero(), + abstain: Uint128::zero(), + }, + padding: None, + }, + MockEnv::new("alpha", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + chain + .execute( + &governance::HandleMsg::AssemblyVote { + proposal: Uint128::new(1), + vote: Vote { + yes: Uint128::new(1), + no: Uint128::zero(), + no_with_veto: Uint128::zero(), + abstain: Uint128::zero(), + }, + padding: None, + }, + MockEnv::new("beta", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + chain.block().time += 30000; + + chain + .execute( + &governance::HandleMsg::Update { + proposal: Uint128::new(1), + padding: None, + }, + MockEnv::new("beta", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + let prop = + get_proposals(&mut chain, &gov, Uint128::new(1), Uint128::new(2)).unwrap()[0].clone(); + + match prop.status { + Status::Funding { .. } => assert!(true), + _ => assert!(false), + }; +} +#[test] +fn fake_funding_token() { + let (mut chain, gov, snip20) = init_funding_governance_with_proposal().unwrap(); + + let other = chain.register(Box::new(Snip20)); + let other = chain + .instantiate( + other.id, + &snip20_reference_impl::msg::InitMsg { + name: "funding_token".to_string(), + admin: None, + symbol: "FND".to_string(), + decimals: 6, + initial_balances: Some(vec![ + snip20_reference_impl::msg::InitialBalance { + address: HumanAddr::from("alpha"), + amount: cosmwasm_std::Uint128(10000), + }, + snip20_reference_impl::msg::InitialBalance { + address: HumanAddr::from("beta"), + amount: cosmwasm_std::Uint128(10000), + }, + snip20_reference_impl::msg::InitialBalance { + address: HumanAddr::from("charlie"), + amount: cosmwasm_std::Uint128(10000), + }, + ]), + prng_seed: Default::default(), + config: None, + }, + MockEnv::new("admin", ContractLink { + address: "other".into(), + code_hash: snip20.code_hash.clone(), + }), + ) + .unwrap(); + + chain + .execute( + &governance::HandleMsg::SetConfig { + treasury: None, + funding_token: Some(Contract { + address: other.address.clone(), + code_hash: other.code_hash, + }), + vote_token: None, + padding: None, + }, + MockEnv::new( + // Sender is self + gov.address.clone(), + gov.clone(), + ), + ) + .unwrap(); + + assert!( + chain + .execute( + &snip20_reference_impl::msg::HandleMsg::Send { + recipient: gov.address, + recipient_code_hash: None, + amount: cosmwasm_std::Uint128(100), + msg: None, + memo: None, + padding: None + }, + MockEnv::new( + // Sender is self + HumanAddr::from("alpha"), + snip20.clone() + ) + ) + .is_err() + ); +} +#[test] +fn funding_proposal_without_msg() { + let (mut chain, gov, snip20) = init_funding_governance_with_proposal().unwrap(); + + assert!( + chain + .execute( + &snip20_reference_impl::msg::HandleMsg::Send { + recipient: gov.address, + recipient_code_hash: None, + amount: cosmwasm_std::Uint128(100), + msg: None, + memo: None, + padding: None + }, + MockEnv::new( + // Sender is self + HumanAddr::from("alpha"), + snip20.clone() + ) + ) + .is_err() + ); +} +#[test] +fn funding_proposal() { + let (mut chain, gov, snip20) = init_funding_governance_with_proposal().unwrap(); + + chain + .execute( + &snip20_reference_impl::msg::HandleMsg::Send { + recipient: gov.address.clone(), + recipient_code_hash: None, + amount: cosmwasm_std::Uint128(100), + msg: Some(to_binary(&Uint128::zero()).unwrap()), + memo: None, + padding: None, + }, + MockEnv::new( + // Sender is self + HumanAddr::from("alpha"), + snip20.clone(), + ), + ) + .unwrap(); + + chain + .execute( + &snip20_reference_impl::msg::HandleMsg::Send { + recipient: gov.address.clone(), + recipient_code_hash: None, + amount: cosmwasm_std::Uint128(100), + msg: Some(to_binary(&Uint128::zero()).unwrap()), + memo: None, + padding: None, + }, + MockEnv::new( + // Sender is self + HumanAddr::from("beta"), + snip20.clone(), + ), + ) + .unwrap(); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + match prop.status { + Status::Funding { amount, .. } => assert_eq!(amount, Uint128::new(200)), + _ => assert!(false), + }; +} +#[test] +fn funding_proposal_after_deadline() { + let (mut chain, gov, snip20) = init_funding_governance_with_proposal().unwrap(); + + chain.block().time += 10000; + + assert!( + chain + .execute( + &snip20_reference_impl::msg::HandleMsg::Send { + recipient: gov.address.clone(), + recipient_code_hash: None, + amount: cosmwasm_std::Uint128(100), + msg: Some(to_binary(&Uint128::zero()).unwrap()), + memo: None, + padding: None + }, + MockEnv::new( + // Sender is self + HumanAddr::from("alpha"), + snip20.clone() + ) + ) + .is_err() + ) +} +#[test] +fn update_while_funding() { + let (mut chain, gov, snip20) = init_funding_governance_with_proposal().unwrap(); + + assert!( + chain + .execute( + &governance::HandleMsg::Update { + proposal: Uint128::zero(), + padding: None + }, + MockEnv::new("beta", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }) + ) + .is_err() + ); +} +#[test] +fn update_when_fully_funded() { + let (mut chain, gov, snip20) = init_funding_governance_with_proposal().unwrap(); + + chain + .execute( + &snip20_reference_impl::msg::HandleMsg::Send { + recipient: gov.address.clone(), + recipient_code_hash: None, + amount: cosmwasm_std::Uint128(1000), + msg: Some(to_binary(&Uint128::zero()).unwrap()), + memo: None, + padding: None, + }, + MockEnv::new( + // Sender is self + HumanAddr::from("alpha"), + snip20.clone(), + ), + ) + .unwrap(); + + chain + .execute( + &snip20_reference_impl::msg::HandleMsg::Send { + recipient: gov.address.clone(), + recipient_code_hash: None, + amount: cosmwasm_std::Uint128(1000), + msg: Some(to_binary(&Uint128::zero()).unwrap()), + memo: None, + padding: None, + }, + MockEnv::new( + // Sender is self + HumanAddr::from("beta"), + snip20.clone(), + ), + ) + .unwrap(); + + chain.execute( + &governance::HandleMsg::Update { + proposal: Uint128::zero(), + padding: None, + }, + MockEnv::new("beta", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + match prop.status { + Status::Passed { .. } => assert!(true), + _ => assert!(false), + }; +} +#[test] +fn update_after_failed_funding() { + let (mut chain, gov, snip20) = init_funding_governance_with_proposal().unwrap(); + + chain + .execute( + &snip20_reference_impl::msg::HandleMsg::Send { + recipient: gov.address.clone(), + recipient_code_hash: None, + amount: cosmwasm_std::Uint128(1000), + msg: Some(to_binary(&Uint128::zero()).unwrap()), + memo: None, + padding: None, + }, + MockEnv::new( + // Sender is self + HumanAddr::from("alpha"), + snip20.clone(), + ), + ) + .unwrap(); + + chain.block().time += 10000; + + chain.execute( + &governance::HandleMsg::Update { + proposal: Uint128::zero(), + padding: None, + }, + MockEnv::new("beta", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + match prop.status { + Status::Expired {} => assert!(true), + _ => assert!(false), + }; +} +#[test] +fn claim_when_not_finished() { + let (mut chain, gov, snip20) = init_funding_governance_with_proposal().unwrap(); + + chain + .execute( + &snip20_reference_impl::msg::HandleMsg::Send { + recipient: gov.address.clone(), + recipient_code_hash: None, + amount: cosmwasm_std::Uint128(1000), + msg: Some(to_binary(&Uint128::zero()).unwrap()), + memo: None, + padding: None, + }, + MockEnv::new( + // Sender is self + HumanAddr::from("alpha"), + snip20.clone(), + ), + ) + .unwrap(); + + assert!( + chain + .execute( + &governance::HandleMsg::ClaimFunding { + id: Uint128::new(0) + }, + MockEnv::new( + // Sender is self + HumanAddr::from("alpha"), + snip20.clone() + ) + ) + .is_err() + ); +} +#[test] +fn claim_after_failing() { + let (mut chain, gov, snip20) = init_funding_governance_with_proposal().unwrap(); + + chain + .execute( + &snip20_reference_impl::msg::HandleMsg::Send { + recipient: gov.address.clone(), + recipient_code_hash: None, + amount: cosmwasm_std::Uint128(1000), + msg: Some(to_binary(&Uint128::zero()).unwrap()), + memo: None, + padding: None, + }, + MockEnv::new( + // Sender is self + HumanAddr::from("alpha"), + snip20.clone(), + ), + ) + .unwrap(); + + chain.block().time += 10000; + + chain.execute( + &governance::HandleMsg::Update { + proposal: Uint128::zero(), + padding: None, + }, + MockEnv::new("beta", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ); + + chain + .execute( + &governance::HandleMsg::ClaimFunding { + id: Uint128::new(0), + }, + MockEnv::new( + // Sender is self + HumanAddr::from("alpha"), + gov.clone(), + ), + ) + .unwrap(); + + let query: snip20_reference_impl::msg::QueryAnswer = chain + .query( + snip20.address.clone(), + &snip20_reference_impl::msg::QueryMsg::Balance { + address: HumanAddr::from("alpha"), + key: "password".to_string(), + }, + ) + .unwrap(); + + match query { + snip20_reference_impl::msg::QueryAnswer::Balance { amount } => { + assert_eq!(amount, cosmwasm_std::Uint128(10000)) + } + _ => assert!(false), + }; +} +#[test] +fn claim_after_passing() { + let (mut chain, gov, snip20) = init_funding_governance_with_proposal().unwrap(); + + chain + .execute( + &snip20_reference_impl::msg::HandleMsg::Send { + recipient: gov.address.clone(), + recipient_code_hash: None, + amount: cosmwasm_std::Uint128(2000), + msg: Some(to_binary(&Uint128::zero()).unwrap()), + memo: None, + padding: None, + }, + MockEnv::new( + // Sender is self + HumanAddr::from("alpha"), + snip20.clone(), + ), + ) + .unwrap(); + + chain.execute( + &governance::HandleMsg::Update { + proposal: Uint128::zero(), + padding: None, + }, + MockEnv::new("beta", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ); + + chain + .execute( + &governance::HandleMsg::ClaimFunding { + id: Uint128::new(0), + }, + MockEnv::new( + // Sender is self + HumanAddr::from("alpha"), + gov.clone(), + ), + ) + .unwrap(); + + let query: snip20_reference_impl::msg::QueryAnswer = chain + .query( + snip20.address.clone(), + &snip20_reference_impl::msg::QueryMsg::Balance { + address: HumanAddr::from("alpha"), + key: "password".to_string(), + }, + ) + .unwrap(); + + match query { + snip20_reference_impl::msg::QueryAnswer::Balance { amount } => { + assert_eq!(amount, cosmwasm_std::Uint128(10000)) + } + _ => assert!(false), + }; +} + +// TODO: Claim after passing +// TODO: claim after failing +// TODO: claim after veto diff --git a/contracts/governance/src/tests/handle/proposal/mod.rs b/contracts/governance/src/tests/handle/proposal/mod.rs new file mode 100644 index 000000000..7709594a4 --- /dev/null +++ b/contracts/governance/src/tests/handle/proposal/mod.rs @@ -0,0 +1,316 @@ +pub mod assembly_voting; +pub mod funding; +pub mod voting; + +use crate::tests::{ + admin_only_governance, + get_assemblies, + get_proposals, + gov_generic_proposal, + gov_msg_proposal, + init_governance, +}; +use cosmwasm_math_compat::Uint128; +use cosmwasm_std::{to_binary, Binary, HumanAddr, StdResult}; +use fadroma_ensemble::{ContractEnsemble, MockEnv}; +use fadroma_platform_scrt::ContractLink; +use shade_protocol::{ + contract_interfaces::{ + governance, + governance::{ + profile::{Count, FundProfile, Profile, UpdateProfile, UpdateVoteProfile, VoteProfile}, + proposal::{ProposalMsg, Status}, + vote::Vote, + InitMsg, + }, + }, + utils::asset::Contract, +}; + +#[test] +fn trigger_admin_command() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + chain + .execute( + &governance::HandleMsg::AssemblyProposal { + assembly: Uint128::new(1), + title: "Title".to_string(), + metadata: "Proposal metadata".to_string(), + msgs: None, + padding: None, + }, + MockEnv::new("admin", ContractLink { + address: gov.address, + code_hash: gov.code_hash, + }), + ) + .unwrap(); +} + +#[test] +fn unauthorized_trigger_admin_command() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + assert!( + chain + .execute( + &governance::HandleMsg::AssemblyProposal { + assembly: Uint128::new(1), + title: "Title".to_string(), + metadata: "Proposal metadata".to_string(), + msgs: None, + padding: None + }, + MockEnv::new("random", gov.clone()) + ) + .is_err() + ); +} + +#[test] +fn text_only_proposal() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + chain + .execute( + &governance::HandleMsg::AssemblyProposal { + assembly: Uint128::new(1), + title: "Title".to_string(), + metadata: "Text only proposal".to_string(), + msgs: None, + padding: None, + }, + MockEnv::new("admin", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + assert_eq!(prop.proposer, HumanAddr::from("admin")); + assert_eq!(prop.title, "Title".to_string()); + assert_eq!(prop.metadata, "Text only proposal".to_string()); + assert_eq!(prop.msgs, None); + assert_eq!(prop.assembly, Uint128::new(1)); + assert_eq!(prop.assembly_vote_tally, None); + assert_eq!(prop.public_vote_tally, None); + match prop.status { + Status::Passed { .. } => assert!(true), + _ => assert!(false), + }; + assert_eq!(prop.status_history.len(), 0); + assert_eq!(prop.funders, None); + + chain + .execute( + &governance::HandleMsg::Trigger { + proposal: Uint128::new(0), + padding: None, + }, + MockEnv::new("admin", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + assert_eq!(prop.status, Status::Success); + assert_eq!(prop.status_history.len(), 1); +} + +#[test] +fn msg_proposal() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + gov_generic_proposal( + &mut chain, + &gov, + "admin", + governance::HandleMsg::SetAssembly { + id: Uint128::new(1), + name: Some("Random name".to_string()), + metadata: None, + members: None, + profile: None, + padding: None, + }, + ) + .unwrap(); + + let old_assembly = + get_assemblies(&mut chain, &gov, Uint128::new(1), Uint128::new(2)).unwrap()[0].clone(); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + match prop.status { + Status::Passed { .. } => assert!(true), + _ => assert!(false), + }; + + chain + .execute( + &governance::HandleMsg::Trigger { + proposal: Uint128::new(0), + padding: None, + }, + MockEnv::new("admin", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + assert_eq!(prop.status, Status::Success); + + let new_assembly = + get_assemblies(&mut chain, &gov, Uint128::new(1), Uint128::new(2)).unwrap()[0].clone(); + + assert_ne!(new_assembly.name, old_assembly.name); +} + +#[test] +fn multi_msg_proposal() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + gov_msg_proposal(&mut chain, &gov, "admin", vec![ + ProposalMsg { + target: Uint128::zero(), + assembly_msg: Uint128::zero(), + msg: to_binary(&vec![ + serde_json::to_string(&governance::HandleMsg::SetAssembly { + id: Uint128::new(1), + name: Some("Random name".to_string()), + metadata: None, + members: None, + profile: None, + padding: None, + }) + .unwrap(), + ]) + .unwrap(), + send: vec![], + }, + ProposalMsg { + target: Uint128::zero(), + assembly_msg: Uint128::zero(), + msg: to_binary(&vec![ + serde_json::to_string(&governance::HandleMsg::SetAssembly { + id: Uint128::new(1), + name: None, + metadata: Some("Random name".to_string()), + members: None, + profile: None, + padding: None, + }) + .unwrap(), + ]) + .unwrap(), + send: vec![], + }, + ]); + + let old_assembly = + get_assemblies(&mut chain, &gov, Uint128::new(1), Uint128::new(2)).unwrap()[0].clone(); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + match prop.status { + Status::Passed { .. } => assert!(true), + _ => assert!(false), + }; + + chain + .execute( + &governance::HandleMsg::Trigger { + proposal: Uint128::new(0), + padding: None, + }, + MockEnv::new("admin", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + assert_eq!(prop.status, Status::Success); + + let new_assembly = + get_assemblies(&mut chain, &gov, Uint128::new(1), Uint128::new(2)).unwrap()[0].clone(); + + assert_ne!(new_assembly.name, old_assembly.name); + assert_ne!(new_assembly.metadata, old_assembly.metadata); +} + +#[test] +fn msg_proposal_invalid_msg() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + gov_generic_proposal( + &mut chain, + &gov, + "admin", + governance::HandleMsg::SetAssembly { + id: Uint128::new(3), + name: Some("Random name".to_string()), + metadata: None, + members: None, + profile: None, + padding: None, + }, + ) + .unwrap(); + + assert!( + chain + .execute( + &governance::HandleMsg::Trigger { + proposal: Uint128::new(0), + padding: None + }, + MockEnv::new("admin", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }) + ) + .is_err() + ); + + chain.block().time += 100000; + + chain + .execute( + &governance::HandleMsg::Cancel { + proposal: Uint128::new(0), + padding: None, + }, + MockEnv::new("admin", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + assert_eq!(prop.status, Status::Canceled); +} + +// TODO: Assembly update if assembly setting removed from profile +// TODO: funding update if funding setting removed from profile +// TODO: voting update if voting setting removed from profile diff --git a/contracts/governance/src/tests/handle/proposal/voting.rs b/contracts/governance/src/tests/handle/proposal/voting.rs new file mode 100644 index 000000000..75b15a36f --- /dev/null +++ b/contracts/governance/src/tests/handle/proposal/voting.rs @@ -0,0 +1,1466 @@ +use crate::tests::{get_proposals, init_governance}; +use contract_harness::harness::{ + governance::Governance, + snip20::Snip20, + snip20_staking::Snip20Staking, +}; +use cosmwasm_math_compat::Uint128; +use cosmwasm_std::{to_binary, HumanAddr, StdResult}; +use fadroma_ensemble::{ContractEnsemble, MockEnv}; +use fadroma_platform_scrt::ContractLink; +use shade_protocol::{ + contract_interfaces::{ + governance, + governance::{ + profile::{Count, Profile, VoteProfile}, + proposal::Status, + vote::Vote, + InitMsg, + }, + staking::snip20_staking, + }, + utils::asset::Contract, +}; + +fn init_voting_governance_with_proposal() -> StdResult<( + ContractEnsemble, + ContractLink, + ContractLink, +)> { + let mut chain = ContractEnsemble::new(50); + + // Register snip20 + let snip20 = chain.register(Box::new(Snip20)); + let snip20 = chain.instantiate( + snip20.id, + &snip20_reference_impl::msg::InitMsg { + name: "token".to_string(), + admin: None, + symbol: "TKN".to_string(), + decimals: 6, + initial_balances: Some(vec![ + snip20_reference_impl::msg::InitialBalance { + address: HumanAddr::from("alpha"), + amount: cosmwasm_std::Uint128(20_000_000), + }, + snip20_reference_impl::msg::InitialBalance { + address: HumanAddr::from("beta"), + amount: cosmwasm_std::Uint128(20_000_000), + }, + snip20_reference_impl::msg::InitialBalance { + address: HumanAddr::from("charlie"), + amount: cosmwasm_std::Uint128(20_000_000), + }, + ]), + prng_seed: Default::default(), + config: None, + }, + MockEnv::new("admin", ContractLink { + address: "token".into(), + code_hash: snip20.code_hash, + }), + )?; + + let stkd_tkn = chain.register(Box::new(Snip20Staking)); + let stkd_tkn = chain.instantiate( + stkd_tkn.id, + &spip_stkd_0::msg::InitMsg { + name: "Staked TKN".to_string(), + admin: None, + symbol: "TKN".to_string(), + decimals: Some(6), + share_decimals: 18, + prng_seed: Default::default(), + config: None, + unbond_time: 0, + staked_token: Contract { + address: snip20.address.clone(), + code_hash: snip20.code_hash.clone(), + }, + treasury: None, + treasury_code_hash: None, + limit_transfer: false, + distributors: None, + }, + MockEnv::new("admin", ContractLink { + address: "staked_token".into(), + code_hash: stkd_tkn.code_hash, + }), + )?; + + // Stake tokens + chain.execute( + &snip20_reference_impl::msg::HandleMsg::Send { + recipient: stkd_tkn.address.clone(), + recipient_code_hash: None, + amount: cosmwasm_std::Uint128(20_000_000), + memo: None, + msg: Some(to_binary(&snip20_staking::ReceiveType::Bond { use_from: None }).unwrap()), + padding: None, + }, + MockEnv::new("alpha", ContractLink { + address: snip20.address.clone(), + code_hash: snip20.code_hash.clone(), + }), + )?; + chain.execute( + &snip20_reference_impl::msg::HandleMsg::Send { + recipient: stkd_tkn.address.clone(), + recipient_code_hash: None, + amount: cosmwasm_std::Uint128(20_000_000), + memo: None, + msg: Some(to_binary(&snip20_staking::ReceiveType::Bond { use_from: None }).unwrap()), + padding: None, + }, + MockEnv::new("beta", ContractLink { + address: snip20.address.clone(), + code_hash: snip20.code_hash.clone(), + }), + )?; + chain.execute( + &snip20_reference_impl::msg::HandleMsg::Send { + recipient: stkd_tkn.address.clone(), + recipient_code_hash: None, + amount: cosmwasm_std::Uint128(20_000_000), + memo: None, + msg: Some(to_binary(&snip20_staking::ReceiveType::Bond { use_from: None }).unwrap()), + padding: None, + }, + MockEnv::new("charlie", ContractLink { + address: snip20.address.clone(), + code_hash: snip20.code_hash.clone(), + }), + )?; + + // Register governance + let gov = chain.register(Box::new(Governance)); + let gov = chain.instantiate( + gov.id, + &InitMsg { + treasury: HumanAddr::from("treasury"), + admin_members: vec![ + HumanAddr::from("alpha"), + HumanAddr::from("beta"), + HumanAddr::from("charlie"), + ], + admin_profile: Profile { + name: "admin".to_string(), + enabled: true, + assembly: None, + funding: None, + token: Some(VoteProfile { + deadline: 10000, + threshold: Count::LiteralCount { + count: Uint128::new(10_000_000), + }, + yes_threshold: Count::LiteralCount { + count: Uint128::new(15_000_000), + }, + veto_threshold: Count::LiteralCount { + count: Uint128::new(15_000_000), + }, + }), + cancel_deadline: 0, + }, + public_profile: Profile { + name: "public".to_string(), + enabled: false, + assembly: None, + funding: None, + token: None, + cancel_deadline: 0, + }, + funding_token: None, + vote_token: Some(Contract { + address: stkd_tkn.address.clone(), + code_hash: stkd_tkn.code_hash.clone(), + }), + }, + MockEnv::new("admin", ContractLink { + address: "gov".into(), + code_hash: gov.code_hash, + }), + )?; + + chain.execute( + &governance::HandleMsg::AssemblyProposal { + assembly: Uint128::new(1), + title: "Title".to_string(), + metadata: "Text only proposal".to_string(), + msgs: None, + padding: None, + }, + MockEnv::new("alpha", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + )?; + + Ok((chain, gov, stkd_tkn)) +} + +#[test] +fn voting() { + let (mut chain, gov, _) = init_voting_governance_with_proposal().unwrap(); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + match prop.status { + Status::Voting { .. } => assert!(true), + _ => assert!(false), + }; +} + +#[test] +fn update_before_deadline() { + let (mut chain, gov, _) = init_voting_governance_with_proposal().unwrap(); + + assert!( + chain + .execute( + &governance::HandleMsg::Update { + proposal: Uint128::new(0), + padding: None + }, + MockEnv::new("alpha", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }) + ) + .is_err() + ); +} + +#[test] +fn update_after_deadline() { + let (mut chain, gov, _) = init_voting_governance_with_proposal().unwrap(); + + chain.block().time += 30000; + + assert!( + chain + .execute( + &governance::HandleMsg::Update { + proposal: Uint128::new(0), + padding: None + }, + MockEnv::new("alpha", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }) + ) + .is_ok() + ); +} + +#[test] +fn invalid_vote() { + let (mut chain, gov, stkd_tkn) = init_voting_governance_with_proposal().unwrap(); + + assert!( + chain + .execute( + &snip20_staking::HandleMsg::ExposeBalance { + recipient: gov.address, + code_hash: None, + msg: Some( + to_binary(&governance::vote::ReceiveBalanceMsg { + vote: Vote { + yes: Uint128::new(25_000_000), + no: Default::default(), + no_with_veto: Default::default(), + abstain: Default::default() + }, + proposal: Uint128::zero() + }) + .unwrap() + ), + memo: None, + padding: None + }, + MockEnv::new("alpha", ContractLink { + address: stkd_tkn.address.clone(), + code_hash: stkd_tkn.code_hash.clone(), + }) + ) + .is_err() + ); +} + +#[test] +fn vote_after_deadline() { + let (mut chain, gov, stkd_tkn) = init_voting_governance_with_proposal().unwrap(); + + chain.block().time += 30000; + + assert!( + chain + .execute( + &snip20_staking::HandleMsg::ExposeBalance { + recipient: gov.address, + code_hash: None, + msg: Some( + to_binary(&governance::vote::ReceiveBalanceMsg { + vote: Vote { + yes: Uint128::new(25_000_000), + no: Default::default(), + no_with_veto: Default::default(), + abstain: Default::default() + }, + proposal: Uint128::zero() + }) + .unwrap() + ), + memo: None, + padding: None + }, + MockEnv::new("alpha", ContractLink { + address: stkd_tkn.address.clone(), + code_hash: stkd_tkn.code_hash.clone(), + }) + ) + .is_err() + ); +} + +#[test] +fn vote_yes() { + let (mut chain, gov, stkd_tkn) = init_voting_governance_with_proposal().unwrap(); + + chain + .execute( + &snip20_staking::HandleMsg::ExposeBalance { + recipient: gov.address.clone(), + code_hash: None, + msg: Some( + to_binary(&governance::vote::ReceiveBalanceMsg { + vote: Vote { + yes: Uint128::new(1_000_000), + no: Default::default(), + no_with_veto: Default::default(), + abstain: Default::default(), + }, + proposal: Uint128::zero(), + }) + .unwrap(), + ), + memo: None, + padding: None, + }, + MockEnv::new("alpha", ContractLink { + address: stkd_tkn.address.clone(), + code_hash: stkd_tkn.code_hash.clone(), + }), + ) + .unwrap(); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + match prop.status { + Status::Voting { .. } => assert!(true), + _ => assert!(false), + }; + + assert_eq!( + prop.public_vote_tally, + Some(Vote { + yes: Uint128::new(1_000_000), + no: Uint128::zero(), + no_with_veto: Uint128::zero(), + abstain: Uint128::zero() + }) + ) +} + +#[test] +fn vote_abstain() { + let (mut chain, gov, stkd_tkn) = init_voting_governance_with_proposal().unwrap(); + + chain + .execute( + &snip20_staking::HandleMsg::ExposeBalance { + recipient: gov.address.clone(), + code_hash: None, + msg: Some( + to_binary(&governance::vote::ReceiveBalanceMsg { + vote: Vote { + yes: Uint128::zero(), + no: Uint128::zero(), + no_with_veto: Uint128::zero(), + abstain: Uint128::new(1_000_000), + }, + proposal: Uint128::zero(), + }) + .unwrap(), + ), + memo: None, + padding: None, + }, + MockEnv::new("alpha", ContractLink { + address: stkd_tkn.address.clone(), + code_hash: stkd_tkn.code_hash.clone(), + }), + ) + .unwrap(); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + match prop.status { + Status::Voting { .. } => assert!(true), + _ => assert!(false), + }; + + assert_eq!( + prop.public_vote_tally, + Some(Vote { + yes: Uint128::zero(), + no: Uint128::zero(), + no_with_veto: Uint128::zero(), + abstain: Uint128::new(1_000_000) + }) + ) +} + +#[test] +fn vote_no() { + let (mut chain, gov, stkd_tkn) = init_voting_governance_with_proposal().unwrap(); + + chain + .execute( + &snip20_staking::HandleMsg::ExposeBalance { + recipient: gov.address.clone(), + code_hash: None, + msg: Some( + to_binary(&governance::vote::ReceiveBalanceMsg { + vote: Vote { + yes: Uint128::zero(), + no: Uint128::new(1_000_000), + no_with_veto: Uint128::zero(), + abstain: Uint128::zero(), + }, + proposal: Uint128::zero(), + }) + .unwrap(), + ), + memo: None, + padding: None, + }, + MockEnv::new("alpha", ContractLink { + address: stkd_tkn.address.clone(), + code_hash: stkd_tkn.code_hash.clone(), + }), + ) + .unwrap(); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + match prop.status { + Status::Voting { .. } => assert!(true), + _ => assert!(false), + }; + + assert_eq!( + prop.public_vote_tally, + Some(Vote { + yes: Uint128::zero(), + no: Uint128::new(1_000_000), + no_with_veto: Uint128::zero(), + abstain: Uint128::zero() + }) + ) +} + +#[test] +fn vote_veto() { + let (mut chain, gov, stkd_tkn) = init_voting_governance_with_proposal().unwrap(); + + chain + .execute( + &snip20_staking::HandleMsg::ExposeBalance { + recipient: gov.address.clone(), + code_hash: None, + msg: Some( + to_binary(&governance::vote::ReceiveBalanceMsg { + vote: Vote { + yes: Uint128::zero(), + no: Uint128::zero(), + no_with_veto: Uint128::new(1_000_000), + abstain: Uint128::zero(), + }, + proposal: Uint128::zero(), + }) + .unwrap(), + ), + memo: None, + padding: None, + }, + MockEnv::new("alpha", ContractLink { + address: stkd_tkn.address.clone(), + code_hash: stkd_tkn.code_hash.clone(), + }), + ) + .unwrap(); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + match prop.status { + Status::Voting { .. } => assert!(true), + _ => assert!(false), + }; + + assert_eq!( + prop.public_vote_tally, + Some(Vote { + yes: Uint128::zero(), + no: Uint128::zero(), + no_with_veto: Uint128::new(1_000_000), + abstain: Uint128::zero() + }) + ) +} + +#[test] +fn vote_passed() { + let (mut chain, gov, stkd_tkn) = init_voting_governance_with_proposal().unwrap(); + + chain + .execute( + &snip20_staking::HandleMsg::ExposeBalance { + recipient: gov.address.clone(), + code_hash: None, + msg: Some( + to_binary(&governance::vote::ReceiveBalanceMsg { + vote: Vote { + yes: Uint128::new(10_000_000), + no: Uint128::zero(), + no_with_veto: Uint128::zero(), + abstain: Uint128::zero(), + }, + proposal: Uint128::zero(), + }) + .unwrap(), + ), + memo: None, + padding: None, + }, + MockEnv::new("alpha", ContractLink { + address: stkd_tkn.address.clone(), + code_hash: stkd_tkn.code_hash.clone(), + }), + ) + .unwrap(); + chain + .execute( + &snip20_staking::HandleMsg::ExposeBalance { + recipient: gov.address.clone(), + code_hash: None, + msg: Some( + to_binary(&governance::vote::ReceiveBalanceMsg { + vote: Vote { + yes: Uint128::new(10_000_000), + no: Uint128::zero(), + no_with_veto: Uint128::zero(), + abstain: Uint128::zero(), + }, + proposal: Uint128::zero(), + }) + .unwrap(), + ), + memo: None, + padding: None, + }, + MockEnv::new("beta", ContractLink { + address: stkd_tkn.address.clone(), + code_hash: stkd_tkn.code_hash.clone(), + }), + ) + .unwrap(); + + chain.block().time += 30000; + + chain + .execute( + &governance::HandleMsg::Update { + proposal: Uint128::zero(), + padding: None, + }, + MockEnv::new("beta", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + match prop.status { + Status::Passed { .. } => assert!(true), + _ => assert!(false), + }; +} + +#[test] +fn vote_abstained() { + let (mut chain, gov, stkd_tkn) = init_voting_governance_with_proposal().unwrap(); + + chain + .execute( + &snip20_staking::HandleMsg::ExposeBalance { + recipient: gov.address.clone(), + code_hash: None, + msg: Some( + to_binary(&governance::vote::ReceiveBalanceMsg { + vote: Vote { + yes: Uint128::zero(), + no: Uint128::zero(), + no_with_veto: Uint128::zero(), + abstain: Uint128::new(10_000_000), + }, + proposal: Uint128::zero(), + }) + .unwrap(), + ), + memo: None, + padding: None, + }, + MockEnv::new("alpha", ContractLink { + address: stkd_tkn.address.clone(), + code_hash: stkd_tkn.code_hash.clone(), + }), + ) + .unwrap(); + chain + .execute( + &snip20_staking::HandleMsg::ExposeBalance { + recipient: gov.address.clone(), + code_hash: None, + msg: Some( + to_binary(&governance::vote::ReceiveBalanceMsg { + vote: Vote { + yes: Uint128::zero(), + no: Uint128::zero(), + no_with_veto: Uint128::zero(), + abstain: Uint128::new(10_000_000), + }, + proposal: Uint128::zero(), + }) + .unwrap(), + ), + memo: None, + padding: None, + }, + MockEnv::new("beta", ContractLink { + address: stkd_tkn.address.clone(), + code_hash: stkd_tkn.code_hash.clone(), + }), + ) + .unwrap(); + + chain.block().time += 30000; + + chain + .execute( + &governance::HandleMsg::Update { + proposal: Uint128::zero(), + padding: None, + }, + MockEnv::new("beta", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + match prop.status { + Status::Rejected { .. } => assert!(true), + _ => assert!(false), + }; +} + +#[test] +fn vote_rejected() { + let (mut chain, gov, stkd_tkn) = init_voting_governance_with_proposal().unwrap(); + + chain + .execute( + &snip20_staking::HandleMsg::ExposeBalance { + recipient: gov.address.clone(), + code_hash: None, + msg: Some( + to_binary(&governance::vote::ReceiveBalanceMsg { + vote: Vote { + yes: Uint128::zero(), + no: Uint128::new(10_000_000), + no_with_veto: Uint128::zero(), + abstain: Uint128::zero(), + }, + proposal: Uint128::zero(), + }) + .unwrap(), + ), + memo: None, + padding: None, + }, + MockEnv::new("alpha", ContractLink { + address: stkd_tkn.address.clone(), + code_hash: stkd_tkn.code_hash.clone(), + }), + ) + .unwrap(); + chain + .execute( + &snip20_staking::HandleMsg::ExposeBalance { + recipient: gov.address.clone(), + code_hash: None, + msg: Some( + to_binary(&governance::vote::ReceiveBalanceMsg { + vote: Vote { + yes: Uint128::zero(), + no: Uint128::new(10_000_000), + no_with_veto: Uint128::zero(), + abstain: Uint128::zero(), + }, + proposal: Uint128::zero(), + }) + .unwrap(), + ), + memo: None, + padding: None, + }, + MockEnv::new("beta", ContractLink { + address: stkd_tkn.address.clone(), + code_hash: stkd_tkn.code_hash.clone(), + }), + ) + .unwrap(); + + chain.block().time += 30000; + + chain + .execute( + &governance::HandleMsg::Update { + proposal: Uint128::zero(), + padding: None, + }, + MockEnv::new("beta", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + match prop.status { + Status::Rejected { .. } => assert!(true), + _ => assert!(false), + }; +} + +#[test] +fn vote_vetoed() { + let (mut chain, gov, stkd_tkn) = init_voting_governance_with_proposal().unwrap(); + + chain + .execute( + &snip20_staking::HandleMsg::ExposeBalance { + recipient: gov.address.clone(), + code_hash: None, + msg: Some( + to_binary(&governance::vote::ReceiveBalanceMsg { + vote: Vote { + yes: Uint128::zero(), + no: Uint128::zero(), + no_with_veto: Uint128::new(10_000_000), + abstain: Uint128::zero(), + }, + proposal: Uint128::zero(), + }) + .unwrap(), + ), + memo: None, + padding: None, + }, + MockEnv::new("alpha", ContractLink { + address: stkd_tkn.address.clone(), + code_hash: stkd_tkn.code_hash.clone(), + }), + ) + .unwrap(); + chain + .execute( + &snip20_staking::HandleMsg::ExposeBalance { + recipient: gov.address.clone(), + code_hash: None, + msg: Some( + to_binary(&governance::vote::ReceiveBalanceMsg { + vote: Vote { + yes: Uint128::zero(), + no: Uint128::zero(), + no_with_veto: Uint128::new(10_000_000), + abstain: Uint128::zero(), + }, + proposal: Uint128::zero(), + }) + .unwrap(), + ), + memo: None, + padding: None, + }, + MockEnv::new("beta", ContractLink { + address: stkd_tkn.address.clone(), + code_hash: stkd_tkn.code_hash.clone(), + }), + ) + .unwrap(); + + chain.block().time += 30000; + + chain + .execute( + &governance::HandleMsg::Update { + proposal: Uint128::zero(), + padding: None, + }, + MockEnv::new("beta", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + match prop.status { + Status::Vetoed { .. } => assert!(true), + _ => assert!(false), + }; +} + +#[test] +fn vote_no_quorum() { + let (mut chain, gov, stkd_tkn) = init_voting_governance_with_proposal().unwrap(); + + chain + .execute( + &snip20_staking::HandleMsg::ExposeBalance { + recipient: gov.address.clone(), + code_hash: None, + msg: Some( + to_binary(&governance::vote::ReceiveBalanceMsg { + vote: Vote { + yes: Uint128::new(10), + no: Uint128::zero(), + no_with_veto: Uint128::zero(), + abstain: Uint128::zero(), + }, + proposal: Uint128::zero(), + }) + .unwrap(), + ), + memo: None, + padding: None, + }, + MockEnv::new("alpha", ContractLink { + address: stkd_tkn.address.clone(), + code_hash: stkd_tkn.code_hash.clone(), + }), + ) + .unwrap(); + chain + .execute( + &snip20_staking::HandleMsg::ExposeBalance { + recipient: gov.address.clone(), + code_hash: None, + msg: Some( + to_binary(&governance::vote::ReceiveBalanceMsg { + vote: Vote { + yes: Uint128::new(10), + no: Uint128::zero(), + no_with_veto: Uint128::zero(), + abstain: Uint128::zero(), + }, + proposal: Uint128::zero(), + }) + .unwrap(), + ), + memo: None, + padding: None, + }, + MockEnv::new("beta", ContractLink { + address: stkd_tkn.address.clone(), + code_hash: stkd_tkn.code_hash.clone(), + }), + ) + .unwrap(); + + chain.block().time += 30000; + + chain + .execute( + &governance::HandleMsg::Update { + proposal: Uint128::zero(), + padding: None, + }, + MockEnv::new("beta", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + match prop.status { + Status::Expired { .. } => assert!(true), + _ => assert!(false), + }; +} + +#[test] +fn vote_total() { + let (mut chain, gov, stkd_tkn) = init_voting_governance_with_proposal().unwrap(); + + chain + .execute( + &snip20_staking::HandleMsg::ExposeBalance { + recipient: gov.address.clone(), + code_hash: None, + msg: Some( + to_binary(&governance::vote::ReceiveBalanceMsg { + vote: Vote { + yes: Uint128::new(10), + no: Uint128::zero(), + no_with_veto: Uint128::zero(), + abstain: Uint128::zero(), + }, + proposal: Uint128::zero(), + }) + .unwrap(), + ), + memo: None, + padding: None, + }, + MockEnv::new("alpha", ContractLink { + address: stkd_tkn.address.clone(), + code_hash: stkd_tkn.code_hash.clone(), + }), + ) + .unwrap(); + + chain + .execute( + &snip20_staking::HandleMsg::ExposeBalance { + recipient: gov.address.clone(), + code_hash: None, + msg: Some( + to_binary(&governance::vote::ReceiveBalanceMsg { + vote: Vote { + yes: Uint128::new(10), + no: Uint128::zero(), + no_with_veto: Uint128::new(10_000), + abstain: Uint128::zero(), + }, + proposal: Uint128::zero(), + }) + .unwrap(), + ), + memo: None, + padding: None, + }, + MockEnv::new("beta", ContractLink { + address: stkd_tkn.address.clone(), + code_hash: stkd_tkn.code_hash.clone(), + }), + ) + .unwrap(); + + chain + .execute( + &snip20_staking::HandleMsg::ExposeBalance { + recipient: gov.address.clone(), + code_hash: None, + msg: Some( + to_binary(&governance::vote::ReceiveBalanceMsg { + vote: Vote { + yes: Uint128::zero(), + no: Uint128::new(23_000), + no_with_veto: Uint128::zero(), + abstain: Uint128::new(10_000), + }, + proposal: Uint128::zero(), + }) + .unwrap(), + ), + memo: None, + padding: None, + }, + MockEnv::new("charlie", ContractLink { + address: stkd_tkn.address.clone(), + code_hash: stkd_tkn.code_hash.clone(), + }), + ) + .unwrap(); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + match prop.status { + Status::Voting { .. } => assert!(true), + _ => assert!(false), + }; + + assert_eq!( + prop.public_vote_tally, + Some(Vote { + yes: Uint128::new(20), + no: Uint128::new(23_000), + no_with_veto: Uint128::new(10_000), + abstain: Uint128::new(10_000) + }) + ) +} + +#[test] +fn update_vote() { + let (mut chain, gov, stkd_tkn) = init_voting_governance_with_proposal().unwrap(); + + chain + .execute( + &snip20_staking::HandleMsg::ExposeBalance { + recipient: gov.address.clone(), + code_hash: None, + msg: Some( + to_binary(&governance::vote::ReceiveBalanceMsg { + vote: Vote { + yes: Uint128::zero(), + no: Uint128::zero(), + no_with_veto: Uint128::new(22_000), + abstain: Uint128::zero(), + }, + proposal: Uint128::zero(), + }) + .unwrap(), + ), + memo: None, + padding: None, + }, + MockEnv::new("alpha", ContractLink { + address: stkd_tkn.address.clone(), + code_hash: stkd_tkn.code_hash.clone(), + }), + ) + .unwrap(); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + assert_eq!( + prop.public_vote_tally, + Some(Vote { + yes: Uint128::zero(), + no: Uint128::zero(), + no_with_veto: Uint128::new(22_000), + abstain: Uint128::zero() + }) + ); + + chain + .execute( + &snip20_staking::HandleMsg::ExposeBalance { + recipient: gov.address.clone(), + code_hash: None, + msg: Some( + to_binary(&governance::vote::ReceiveBalanceMsg { + vote: Vote { + yes: Uint128::new(10_000), + no: Uint128::zero(), + no_with_veto: Uint128::zero(), + abstain: Uint128::zero(), + }, + proposal: Uint128::zero(), + }) + .unwrap(), + ), + memo: None, + padding: None, + }, + MockEnv::new("alpha", ContractLink { + address: stkd_tkn.address.clone(), + code_hash: stkd_tkn.code_hash.clone(), + }), + ) + .unwrap(); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + assert_eq!( + prop.public_vote_tally, + Some(Vote { + yes: Uint128::new(10_000), + no: Uint128::zero(), + no_with_veto: Uint128::zero(), + abstain: Uint128::zero() + }) + ); +} + +#[test] +fn vote_count() { + let (mut chain, gov, stkd_tkn) = init_voting_governance_with_proposal().unwrap(); + + chain + .execute( + &snip20_staking::HandleMsg::ExposeBalance { + recipient: gov.address.clone(), + code_hash: None, + msg: Some( + to_binary(&governance::vote::ReceiveBalanceMsg { + vote: Vote { + yes: Uint128::new(10_000_000), + no: Uint128::zero(), + no_with_veto: Uint128::zero(), + abstain: Uint128::zero(), + }, + proposal: Uint128::zero(), + }) + .unwrap(), + ), + memo: None, + padding: None, + }, + MockEnv::new("alpha", ContractLink { + address: stkd_tkn.address.clone(), + code_hash: stkd_tkn.code_hash.clone(), + }), + ) + .unwrap(); + chain + .execute( + &snip20_staking::HandleMsg::ExposeBalance { + recipient: gov.address.clone(), + code_hash: None, + msg: Some( + to_binary(&governance::vote::ReceiveBalanceMsg { + vote: Vote { + yes: Uint128::new(10_000_000), + no: Uint128::zero(), + no_with_veto: Uint128::zero(), + abstain: Uint128::zero(), + }, + proposal: Uint128::zero(), + }) + .unwrap(), + ), + memo: None, + padding: None, + }, + MockEnv::new("beta", ContractLink { + address: stkd_tkn.address.clone(), + code_hash: stkd_tkn.code_hash.clone(), + }), + ) + .unwrap(); + + chain.block().time += 30000; + + chain + .execute( + &governance::HandleMsg::Update { + proposal: Uint128::zero(), + padding: None, + }, + MockEnv::new("beta", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + match prop.status { + Status::Passed { .. } => assert!(true), + _ => assert!(false), + }; +} + +#[test] +fn vote_count_percentage() { + let (mut chain, gov, stkd_tkn) = init_voting_governance_with_proposal().unwrap(); + + let mut chain = ContractEnsemble::new(50); + + // Register snip20 + let snip20 = chain.register(Box::new(Snip20)); + let snip20 = chain + .instantiate( + snip20.id, + &snip20_reference_impl::msg::InitMsg { + name: "token".to_string(), + admin: None, + symbol: "TKN".to_string(), + decimals: 6, + initial_balances: Some(vec![ + snip20_reference_impl::msg::InitialBalance { + address: HumanAddr::from("alpha"), + amount: cosmwasm_std::Uint128(20_000_000), + }, + snip20_reference_impl::msg::InitialBalance { + address: HumanAddr::from("beta"), + amount: cosmwasm_std::Uint128(20_000_000), + }, + snip20_reference_impl::msg::InitialBalance { + address: HumanAddr::from("charlie"), + amount: cosmwasm_std::Uint128(20_000_000), + }, + ]), + prng_seed: Default::default(), + config: None, + }, + MockEnv::new("admin", ContractLink { + address: "token".into(), + code_hash: snip20.code_hash, + }), + ) + .unwrap(); + + let stkd_tkn = chain.register(Box::new(Snip20Staking)); + let stkd_tkn = chain + .instantiate( + stkd_tkn.id, + &spip_stkd_0::msg::InitMsg { + name: "Staked TKN".to_string(), + admin: None, + symbol: "TKN".to_string(), + decimals: Some(6), + share_decimals: 18, + prng_seed: Default::default(), + config: None, + unbond_time: 0, + staked_token: Contract { + address: snip20.address.clone(), + code_hash: snip20.code_hash.clone(), + }, + treasury: None, + treasury_code_hash: None, + limit_transfer: false, + distributors: None, + }, + MockEnv::new("admin", ContractLink { + address: "staked_token".into(), + code_hash: stkd_tkn.code_hash, + }), + ) + .unwrap(); + + // Stake tokens + chain + .execute( + &snip20_reference_impl::msg::HandleMsg::Send { + recipient: stkd_tkn.address.clone(), + recipient_code_hash: None, + amount: cosmwasm_std::Uint128(20_000_000), + memo: None, + msg: Some( + to_binary(&snip20_staking::ReceiveType::Bond { use_from: None }).unwrap(), + ), + padding: None, + }, + MockEnv::new("alpha", ContractLink { + address: snip20.address.clone(), + code_hash: snip20.code_hash.clone(), + }), + ) + .unwrap(); + chain + .execute( + &snip20_reference_impl::msg::HandleMsg::Send { + recipient: stkd_tkn.address.clone(), + recipient_code_hash: None, + amount: cosmwasm_std::Uint128(20_000_000), + memo: None, + msg: Some( + to_binary(&snip20_staking::ReceiveType::Bond { use_from: None }).unwrap(), + ), + padding: None, + }, + MockEnv::new("beta", ContractLink { + address: snip20.address.clone(), + code_hash: snip20.code_hash.clone(), + }), + ) + .unwrap(); + chain + .execute( + &snip20_reference_impl::msg::HandleMsg::Send { + recipient: stkd_tkn.address.clone(), + recipient_code_hash: None, + amount: cosmwasm_std::Uint128(20_000_000), + memo: None, + msg: Some( + to_binary(&snip20_staking::ReceiveType::Bond { use_from: None }).unwrap(), + ), + padding: None, + }, + MockEnv::new("charlie", ContractLink { + address: snip20.address.clone(), + code_hash: snip20.code_hash.clone(), + }), + ) + .unwrap(); + + // Register governance + let gov = chain.register(Box::new(Governance)); + let gov = chain + .instantiate( + gov.id, + &InitMsg { + treasury: HumanAddr::from("treasury"), + admin_members: vec![ + HumanAddr::from("alpha"), + HumanAddr::from("beta"), + HumanAddr::from("charlie"), + ], + admin_profile: Profile { + name: "admin".to_string(), + enabled: true, + assembly: None, + funding: None, + token: Some(VoteProfile { + deadline: 10000, + threshold: Count::Percentage { percent: 3300 }, + yes_threshold: Count::Percentage { percent: 6600 }, + veto_threshold: Count::Percentage { percent: 3300 }, + }), + cancel_deadline: 0, + }, + public_profile: Profile { + name: "public".to_string(), + enabled: false, + assembly: None, + funding: None, + token: None, + cancel_deadline: 0, + }, + funding_token: None, + vote_token: Some(Contract { + address: stkd_tkn.address.clone(), + code_hash: stkd_tkn.code_hash.clone(), + }), + }, + MockEnv::new("admin", ContractLink { + address: "gov".into(), + code_hash: gov.code_hash, + }), + ) + .unwrap(); + + chain + .execute( + &governance::HandleMsg::AssemblyProposal { + assembly: Uint128::new(1), + title: "Title".to_string(), + metadata: "Text only proposal".to_string(), + msgs: None, + padding: None, + }, + MockEnv::new("alpha", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + let (mut chain, gov, stkd_tkn) = init_voting_governance_with_proposal().unwrap(); + + chain + .execute( + &snip20_staking::HandleMsg::ExposeBalance { + recipient: gov.address.clone(), + code_hash: None, + msg: Some( + to_binary(&governance::vote::ReceiveBalanceMsg { + vote: Vote { + yes: Uint128::new(10_000_000), + no: Uint128::zero(), + no_with_veto: Uint128::zero(), + abstain: Uint128::zero(), + }, + proposal: Uint128::zero(), + }) + .unwrap(), + ), + memo: None, + padding: None, + }, + MockEnv::new("alpha", ContractLink { + address: stkd_tkn.address.clone(), + code_hash: stkd_tkn.code_hash.clone(), + }), + ) + .unwrap(); + chain + .execute( + &snip20_staking::HandleMsg::ExposeBalance { + recipient: gov.address.clone(), + code_hash: None, + msg: Some( + to_binary(&governance::vote::ReceiveBalanceMsg { + vote: Vote { + yes: Uint128::new(10_000_000), + no: Uint128::zero(), + no_with_veto: Uint128::zero(), + abstain: Uint128::zero(), + }, + proposal: Uint128::zero(), + }) + .unwrap(), + ), + memo: None, + padding: None, + }, + MockEnv::new("beta", ContractLink { + address: stkd_tkn.address.clone(), + code_hash: stkd_tkn.code_hash.clone(), + }), + ) + .unwrap(); + + chain.block().time += 30000; + + chain + .execute( + &governance::HandleMsg::Update { + proposal: Uint128::zero(), + padding: None, + }, + MockEnv::new("beta", ContractLink { + address: gov.address.clone(), + code_hash: gov.code_hash.clone(), + }), + ) + .unwrap(); + + let prop = + get_proposals(&mut chain, &gov, Uint128::zero(), Uint128::new(2)).unwrap()[0].clone(); + + match prop.status { + Status::Passed { .. } => assert!(true), + _ => assert!(false), + }; +} diff --git a/contracts/governance/src/tests/mod.rs b/contracts/governance/src/tests/mod.rs new file mode 100644 index 000000000..47dc0c21b --- /dev/null +++ b/contracts/governance/src/tests/mod.rs @@ -0,0 +1,211 @@ +pub mod handle; +pub mod query; + +use crate::contract::{handle, init, query}; +use contract_harness::harness::governance::Governance; +use cosmwasm_math_compat::Uint128; +use cosmwasm_std::{ + from_binary, + to_binary, + Binary, + Env, + HandleResponse, + HumanAddr, + InitResponse, + StdError, + StdResult, +}; +use fadroma_ensemble::{ContractEnsemble, ContractHarness, MockDeps, MockEnv}; +use fadroma_platform_scrt::ContractLink; +use serde::Serialize; +use shade_protocol::contract_interfaces::{ + governance, + governance::{ + assembly::{Assembly, AssemblyMsg}, + contract::AllowedContract, + profile::Profile, + proposal::{Proposal, ProposalMsg}, + Config, + }, +}; + +pub fn init_governance( + msg: governance::InitMsg, +) -> StdResult<(ContractEnsemble, ContractLink)> { + let mut chain = ContractEnsemble::new(50); + + // Register governance + let gov = chain.register(Box::new(Governance)); + let gov = chain.instantiate( + gov.id, + &msg, + MockEnv::new("admin", ContractLink { + address: "gov".into(), + code_hash: gov.code_hash, + }), + )?; + + Ok((chain, gov)) +} + +pub fn admin_only_governance() -> StdResult<(ContractEnsemble, ContractLink)> { + init_governance(governance::InitMsg { + treasury: HumanAddr("treasury".to_string()), + admin_members: vec![HumanAddr("admin".to_string())], + admin_profile: Profile { + name: "admin".to_string(), + enabled: true, + assembly: None, + funding: None, + token: None, + cancel_deadline: 0, + }, + public_profile: Profile { + name: "public".to_string(), + enabled: false, + assembly: None, + funding: None, + token: None, + cancel_deadline: 0, + }, + funding_token: None, + vote_token: None, + }) +} + +pub fn gov_generic_proposal( + chain: &mut ContractEnsemble, + gov: &ContractLink, + sender: &str, + msg: governance::HandleMsg, +) -> StdResult<()> { + gov_msg_proposal(chain, gov, sender, vec![ProposalMsg { + target: Uint128::zero(), + assembly_msg: Uint128::zero(), + msg: to_binary(&vec![serde_json::to_string(&msg).unwrap()])?, + send: vec![], + }]) +} + +pub fn gov_msg_proposal( + chain: &mut ContractEnsemble, + gov: &ContractLink, + sender: &str, + msgs: Vec, +) -> StdResult<()> { + chain.execute( + &governance::HandleMsg::AssemblyProposal { + assembly: Uint128::new(1), + title: "Title".to_string(), + metadata: "Proposal metadata".to_string(), + msgs: Some(msgs), + padding: None, + }, + MockEnv::new(sender, gov.clone()), + ) +} + +pub fn get_assembly_msgs( + chain: &mut ContractEnsemble, + gov: &ContractLink, + start: Uint128, + end: Uint128, +) -> StdResult> { + let query: governance::QueryAnswer = + chain.query(gov.address.clone(), &governance::QueryMsg::AssemblyMsgs { + start, + end, + })?; + + let msgs = match query { + governance::QueryAnswer::AssemblyMsgs { msgs } => msgs, + _ => return Err(StdError::generic_err("Returned wrong enum")), + }; + + Ok(msgs) +} + +pub fn get_contract( + chain: &mut ContractEnsemble, + gov: &ContractLink, + start: Uint128, + end: Uint128, +) -> StdResult> { + let query: governance::QueryAnswer = + chain.query(gov.address.clone(), &governance::QueryMsg::Contracts { + start, + end, + })?; + + match query { + governance::QueryAnswer::Contracts { contracts } => Ok(contracts), + _ => return Err(StdError::generic_err("Returned wrong enum")), + } +} + +pub fn get_profiles( + chain: &mut ContractEnsemble, + gov: &ContractLink, + start: Uint128, + end: Uint128, +) -> StdResult> { + let query: governance::QueryAnswer = + chain.query(gov.address.clone(), &governance::QueryMsg::Profiles { + start, + end, + })?; + + match query { + governance::QueryAnswer::Profiles { profiles } => Ok(profiles), + _ => return Err(StdError::generic_err("Returned wrong enum")), + } +} + +pub fn get_assemblies( + chain: &mut ContractEnsemble, + gov: &ContractLink, + start: Uint128, + end: Uint128, +) -> StdResult> { + let query: governance::QueryAnswer = + chain.query(gov.address.clone(), &governance::QueryMsg::Assemblies { + start, + end, + })?; + + match query { + governance::QueryAnswer::Assemblies { assemblies } => Ok(assemblies), + _ => return Err(StdError::generic_err("Returned wrong enum")), + } +} + +pub fn get_proposals( + chain: &mut ContractEnsemble, + gov: &ContractLink, + start: Uint128, + end: Uint128, +) -> StdResult> { + let query: governance::QueryAnswer = + chain.query(gov.address.clone(), &governance::QueryMsg::Proposals { + start, + end, + })?; + + match query { + governance::QueryAnswer::Proposals { props } => Ok(props), + _ => return Err(StdError::generic_err("Returned wrong enum")), + } +} + +pub fn get_config( + chain: &mut ContractEnsemble, + gov: &ContractLink, +) -> StdResult { + let query: governance::QueryAnswer = + chain.query(gov.address.clone(), &governance::QueryMsg::Config {})?; + + match query { + governance::QueryAnswer::Config { config } => Ok(config), + _ => return Err(StdError::generic_err("Returned wrong enum")), + } +} diff --git a/contracts/governance/src/tests/query/mod.rs b/contracts/governance/src/tests/query/mod.rs new file mode 100644 index 000000000..efe769414 --- /dev/null +++ b/contracts/governance/src/tests/query/mod.rs @@ -0,0 +1 @@ +pub mod public_queries; diff --git a/contracts/governance/src/tests/query/public_queries.rs b/contracts/governance/src/tests/query/public_queries.rs new file mode 100644 index 000000000..d15039e81 --- /dev/null +++ b/contracts/governance/src/tests/query/public_queries.rs @@ -0,0 +1,210 @@ +// TODO: Queries +// TODO: Check proposal without voting or funding and see how it returns + +// TODO: Verify proposal history +// TODO: quwery proposals + +// TODO: Query user funding +// TODO: Query where theres no user funding + +// TODO: Query user assembly vote +// TODO: Query where theres no user vote + +// TODO: Query user vote +// TODO: Query where theres no user vote + +// TODO: funding privacy + +use crate::tests::{ + admin_only_governance, + get_assemblies, + get_assembly_msgs, + get_config, + get_contract, + get_profiles, +}; +use cosmwasm_math_compat::Uint128; +use cosmwasm_std::StdError; +use shade_protocol::contract_interfaces::governance; + +#[test] +fn query_total_assembly_msg() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + let query: governance::QueryAnswer = chain + .query( + gov.address.clone(), + &governance::QueryMsg::TotalAssemblyMsgs {}, + ) + .unwrap(); + + let total = match query { + governance::QueryAnswer::Total { total } => total, + _ => Uint128::zero(), + }; + + assert_eq!(total, Uint128::new(1)); +} + +#[test] +fn query_assembly_msg() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + let assemblies = get_assembly_msgs(&mut chain, &gov, Uint128::zero(), Uint128::zero()).unwrap(); + + assert_eq!(assemblies.len(), 1); +} + +#[test] +fn query_assembly_msg_large_end() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + let assemblies = + get_assembly_msgs(&mut chain, &gov, Uint128::zero(), Uint128::new(10)).unwrap(); + + assert_eq!(assemblies.len(), 1); +} + +#[test] +fn query_assembly_msg_wrong_index() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + let assemblies = + get_assembly_msgs(&mut chain, &gov, Uint128::new(5), Uint128::new(10)).is_err(); +} + +#[test] +fn query_total_contracts() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + let query: governance::QueryAnswer = chain + .query( + gov.address.clone(), + &governance::QueryMsg::TotalContracts {}, + ) + .unwrap(); + + let total = match query { + governance::QueryAnswer::Total { total } => total, + _ => Uint128::zero(), + }; + + assert_eq!(total, Uint128::new(1)); +} + +#[test] +fn query_contracts() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + let contracts = get_contract(&mut chain, &gov, Uint128::zero(), Uint128::zero()).unwrap(); + + assert_eq!(contracts.len(), 1); +} + +#[test] +fn query_contracts_large_end() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + let contracts = get_contract(&mut chain, &gov, Uint128::zero(), Uint128::new(10)).unwrap(); + + assert_eq!(contracts.len(), 1); +} + +#[test] +fn query_contracts_wrong_index() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + get_contract(&mut chain, &gov, Uint128::new(5), Uint128::new(10)).is_err(); +} + +#[test] +fn query_total_profiles() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + let query: governance::QueryAnswer = chain + .query(gov.address.clone(), &governance::QueryMsg::TotalProfiles {}) + .unwrap(); + + let total = match query { + governance::QueryAnswer::Total { total } => total, + _ => Uint128::zero(), + }; + + assert_eq!(total, Uint128::new(2)); +} + +#[test] +fn query_profiles() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + let profiles = get_profiles(&mut chain, &gov, Uint128::zero(), Uint128::zero()).unwrap(); + + assert_eq!(profiles.len(), 1); +} + +#[test] +fn query_profiles_large_end() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + let profiles = get_profiles(&mut chain, &gov, Uint128::zero(), Uint128::new(10)).unwrap(); + + assert_eq!(profiles.len(), 2); +} + +#[test] +fn query_profiles_wrong_index() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + get_profiles(&mut chain, &gov, Uint128::new(5), Uint128::new(10)).is_err(); +} + +#[test] +fn query_total_assemblies() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + let query: governance::QueryAnswer = chain + .query( + gov.address.clone(), + &governance::QueryMsg::TotalAssemblies {}, + ) + .unwrap(); + + let total = match query { + governance::QueryAnswer::Total { total } => total, + _ => Uint128::zero(), + }; + + assert_eq!(total, Uint128::new(2)); +} + +#[test] +fn query_assemblies() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + let assemblies = get_assemblies(&mut chain, &gov, Uint128::zero(), Uint128::zero()).unwrap(); + + assert_eq!(assemblies.len(), 1); +} + +#[test] +fn query_assemblies_large_end() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + let assemblies = get_assemblies(&mut chain, &gov, Uint128::zero(), Uint128::new(10)).unwrap(); + + assert_eq!(assemblies.len(), 2); +} + +#[test] +fn query_assemblies_wrong_index() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + get_assemblies(&mut chain, &gov, Uint128::new(5), Uint128::new(10)).is_err(); +} + +#[test] +fn query_config() { + let (mut chain, gov) = admin_only_governance().unwrap(); + + get_config(&mut chain, &gov).unwrap(); +} diff --git a/makefile b/makefile index 4d49d0acd..9f795d997 100755 --- a/makefile +++ b/makefile @@ -35,7 +35,7 @@ compress_all: setup compress-snip20: setup $(call opt_and_compress,snip20,snip20_reference_impl) -compress-shd_staking: setup +compress-snip20_staking: setup $(call opt_and_compress,snip20_staking,spip_stkd_0) compress-%: setup @@ -55,9 +55,9 @@ test: test-%: (cd ${contracts_dir}/$*; cargo test) -shd_staking: setup - (cd ${contracts_dir}/shd_staking; ${build-release}) - @$(MAKE) $(addprefix compress-,shd_staking) +snip20_staking: setup + (cd ${contracts_dir}/snip20_staking; ${build-release}) + @$(MAKE) $(addprefix compress-,snip20_staking) setup: $(compiled_dir) $(checksum_dir) diff --git a/packages/contract_harness/Cargo.toml b/packages/contract_harness/Cargo.toml index 50da75762..fa442ca46 100644 --- a/packages/contract_harness/Cargo.toml +++ b/packages/contract_harness/Cargo.toml @@ -15,6 +15,8 @@ snip20 = ["dep:snip20-reference-impl"] mint = ["dep:mint"] oracle = ["dep:oracle"] mock_band= ["dep:mock_band"] +governance = ["dep:governance"] +snip20_staking = ["dep:spip_stkd_0"] [dependencies] cosmwasm-std = { version = "0.10", package = "secret-cosmwasm-std" } @@ -22,4 +24,6 @@ fadroma-ensemble = { branch = "v100", git = "https://github.com/hackbg/fadroma.g snip20-reference-impl = { version = "0.1.0", path = "../../contracts/snip20", optional = true } mint = { version = "0.1.0", path = "../../contracts/mint", optional = true } oracle = { version = "0.1.0", path = "../../contracts/oracle", optional = true } -mock_band = { version = "0.1.0", path = "../../contracts/mock_band", optional = true } \ No newline at end of file +mock_band = { version = "0.1.0", path = "../../contracts/mock_band", optional = true } +governance = { version = "0.1.0", path = "../../contracts/governance", optional = true } +spip_stkd_0 = { version = "0.1.0", path = "../../contracts/snip20_staking", optional = true } \ No newline at end of file diff --git a/packages/contract_harness/src/harness.rs b/packages/contract_harness/src/harness.rs index d9ab57892..10bbb5568 100644 --- a/packages/contract_harness/src/harness.rs +++ b/packages/contract_harness/src/harness.rs @@ -33,3 +33,21 @@ pub mod mock_band { pub struct MockBand; harness_macro::implement_harness!(MockBand, mock_band); } + +#[cfg(feature = "governance")] +pub mod governance { + use crate::harness_macro; + use governance; + + pub struct Governance; + harness_macro::implement_harness!(Governance, governance); +} + +#[cfg(feature = "snip20_staking")] +pub mod snip20_staking { + use crate::harness_macro; + use spip_stkd_0; + + pub struct Snip20Staking; + harness_macro::implement_harness!(Snip20Staking, spip_stkd_0); +} diff --git a/packages/network_integration/src/testnet_staking.rs b/packages/network_integration/src/testnet_staking.rs index 520b78992..d634a0105 100644 --- a/packages/network_integration/src/testnet_staking.rs +++ b/packages/network_integration/src/testnet_staking.rs @@ -9,11 +9,11 @@ use serde::{Deserialize, Serialize}; use serde_json::Result; use shade_protocol::utils::asset::Contract; use shade_protocol::{ - contract_interfaces::staking::shd_staking, - contract_interfaces::snip20, + snip20_staking, + snip20, }; use std::{env, fs}; -use shade_protocol::contract_interfaces::snip20::InitialBalance; +use shade_protocol::snip20::InitialBalance; fn main() -> Result<()> { // Initialize snip20 @@ -53,14 +53,14 @@ fn main() -> Result<()> { // Initialize staker print_header("Initializing Staking"); - let init_msg = shd_staking::InitMsg { + let init_msg = snip20_staking::InitMsg { name: "StakedShade".to_string(), admin: None, symbol: "STKSHD".to_string(), decimals: Some(8), share_decimals: 18, prng_seed: Default::default(), - config: Some(shd_staking::InitConfig { + config: Some(snip20_staking::InitConfig { public_total_supply: Some(true), }), unbond_time: 180, diff --git a/packages/shade_protocol/Cargo.toml b/packages/shade_protocol/Cargo.toml index 2e976f9d6..bae46e1b4 100644 --- a/packages/shade_protocol/Cargo.toml +++ b/packages/shade_protocol/Cargo.toml @@ -31,7 +31,7 @@ storage_plus = ["dep:secret-storage-plus"] # Protocol contracts airdrop = ["utils", "errors", "dep:remain", "dep:query-authentication"] initializer = ["snip20", "utils"] -governance = ["utils"] +governance = ["utils", "flexible_msg"] mint = ["utils", "snip20"] mint_router = ["utils", "snip20"] oracle = ["snip20"] @@ -42,6 +42,10 @@ rewards_emission = ["adapter"] adapter = [] snip20_staking = ["utils"] +# Protocol Implementation Contracts +# Used in internal smart contracts +governance-impl = ["governance", "storage"] + # for quicker tests, cargo test --lib # for more explicit tests, cargo test --features=backtraces backtraces = ["cosmwasm-std/backtraces"] diff --git a/packages/shade_protocol/src/contract_interfaces/governance/assembly.rs b/packages/shade_protocol/src/contract_interfaces/governance/assembly.rs new file mode 100644 index 000000000..3f1bb9be2 --- /dev/null +++ b/packages/shade_protocol/src/contract_interfaces/governance/assembly.rs @@ -0,0 +1,200 @@ +use crate::{contract_interfaces::governance::stored_id::ID, utils::flexible_msg::FlexibleMsg}; +use cosmwasm_math_compat::Uint128; +use cosmwasm_std::{HumanAddr, StdResult, Storage}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "governance-impl")] +use crate::utils::storage::default::BucketStorage; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct Assembly { + // Readable name + pub name: String, + // Description of the assembly, preferably in base64 + pub metadata: String, + // List of members in assembly + pub members: Vec, + // Selected profile + pub profile: Uint128, +} + +#[cfg(feature = "governance-impl")] +impl Assembly { + pub fn load(storage: &S, id: &Uint128) -> StdResult { + let desc = Self::description(storage, id)?; + let data = Self::data(storage, id)?; + + Ok(Self { + name: desc.name, + metadata: desc.metadata, + members: data.members, + profile: data.profile, + }) + } + + pub fn may_load(storage: &S, id: &Uint128) -> StdResult> { + if id > &ID::assembly(storage)? { + return Ok(None); + } + Ok(Some(Self::load(storage, id)?)) + } + + pub fn save(&self, storage: &mut S, id: &Uint128) -> StdResult<()> { + AssemblyData { + members: self.members.clone(), + profile: self.profile, + } + .save(storage, &id.to_be_bytes())?; + + AssemblyDescription { + name: self.name.clone(), + metadata: self.metadata.clone(), + } + .save(storage, &id.to_be_bytes())?; + + Ok(()) + } + + pub fn data(storage: &S, id: &Uint128) -> StdResult { + AssemblyData::load(storage, &id.to_be_bytes()) + } + + pub fn save_data( + storage: &mut S, + id: &Uint128, + data: AssemblyData, + ) -> StdResult<()> { + data.save(storage, &id.to_be_bytes()) + } + + pub fn description(storage: &S, id: &Uint128) -> StdResult { + AssemblyDescription::load(storage, &id.to_be_bytes()) + } + + pub fn save_description( + storage: &mut S, + id: &Uint128, + desc: AssemblyDescription, + ) -> StdResult<()> { + desc.save(storage, &id.to_be_bytes()) + } +} + +#[cfg(feature = "governance-impl")] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct AssemblyData { + pub members: Vec, + pub profile: Uint128, +} + +#[cfg(feature = "governance-impl")] +impl BucketStorage for AssemblyData { + const NAMESPACE: &'static [u8] = b"assembly_data-"; +} + +#[cfg(feature = "governance-impl")] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct AssemblyDescription { + pub name: String, + pub metadata: String, +} + +#[cfg(feature = "governance-impl")] +impl BucketStorage for AssemblyDescription { + const NAMESPACE: &'static [u8] = b"assembly_description-"; +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +// A generic msg is created at init, its a black msg where the variable is the start +pub struct AssemblyMsg { + pub name: String, + // Assemblies allowed to call this msg + pub assemblies: Vec, + // HandleMsg template + pub msg: FlexibleMsg, +} + +#[cfg(feature = "governance-impl")] +impl AssemblyMsg { + pub fn load(storage: &S, id: &Uint128) -> StdResult { + let desc = Self::description(storage, id)?; + let data = Self::data(storage, id)?; + + Ok(Self { + name: desc, + assemblies: data.assemblies, + msg: data.msg, + }) + } + + pub fn may_load(storage: &S, id: &Uint128) -> StdResult> { + if id > &ID::assembly_msg(storage)? { + return Ok(None); + } + Ok(Some(Self::load(storage, id)?)) + } + + pub fn save(&self, storage: &mut S, id: &Uint128) -> StdResult<()> { + AssemblyMsgData { + assemblies: self.assemblies.clone(), + msg: self.msg.clone(), + } + .save(storage, &id.to_be_bytes())?; + + AssemblyMsgDescription(self.name.clone()).save(storage, &id.to_be_bytes())?; + + Ok(()) + } + + pub fn data(storage: &S, id: &Uint128) -> StdResult { + AssemblyMsgData::load(storage, &id.to_be_bytes()) + } + + pub fn save_data( + storage: &mut S, + id: &Uint128, + data: AssemblyMsgData, + ) -> StdResult<()> { + data.save(storage, &id.to_be_bytes()) + } + + pub fn description(storage: &S, id: &Uint128) -> StdResult { + Ok(AssemblyMsgDescription::load(storage, &id.to_be_bytes())?.0) + } + + pub fn save_description( + storage: &mut S, + id: &Uint128, + desc: String, + ) -> StdResult<()> { + AssemblyMsgDescription(desc).save(storage, &id.to_be_bytes()) + } +} + +#[cfg(feature = "governance-impl")] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct AssemblyMsgData { + pub assemblies: Vec, + pub msg: FlexibleMsg, +} + +#[cfg(feature = "governance-impl")] +impl BucketStorage for AssemblyMsgData { + const NAMESPACE: &'static [u8] = b"assembly_msg_data-"; +} + +#[cfg(feature = "governance-impl")] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +struct AssemblyMsgDescription(pub String); + +#[cfg(feature = "governance-impl")] +impl BucketStorage for AssemblyMsgDescription { + const NAMESPACE: &'static [u8] = b"assembly_msg_description-"; +} diff --git a/packages/shade_protocol/src/contract_interfaces/governance/contract.rs b/packages/shade_protocol/src/contract_interfaces/governance/contract.rs new file mode 100644 index 000000000..7ad6aed1d --- /dev/null +++ b/packages/shade_protocol/src/contract_interfaces/governance/contract.rs @@ -0,0 +1,110 @@ +use crate::{ + contract_interfaces::governance::stored_id::ID, + utils::{asset::Contract, storage::default::BucketStorage}, +}; +use cosmwasm_math_compat::Uint128; +use cosmwasm_std::{StdResult, Storage}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct AllowedContract { + pub name: String, + pub metadata: String, + // If none then anyone can use it + #[serde(skip_serializing_if = "Option::is_none")] + pub assemblies: Option>, + pub contract: Contract, +} + +#[cfg(feature = "governance-impl")] +impl AllowedContract { + pub fn load(storage: &S, id: &Uint128) -> StdResult { + let desc = Self::description(storage, id)?; + let data = Self::data(storage, id)?; + + Ok(Self { + name: desc.name, + metadata: desc.metadata, + contract: data.contract, + assemblies: data.assemblies, + }) + } + + pub fn may_load(storage: &S, id: &Uint128) -> StdResult> { + if id > &ID::contract(storage)? { + return Ok(None); + } + Ok(Some(Self::load(storage, id)?)) + } + + pub fn save(&self, storage: &mut S, id: &Uint128) -> StdResult<()> { + AllowedContractData { + contract: self.contract.clone(), + assemblies: self.assemblies.clone(), + } + .save(storage, &id.to_be_bytes())?; + + AllowedContractDescription { + name: self.name.clone(), + metadata: self.metadata.clone(), + } + .save(storage, &id.to_be_bytes())?; + + Ok(()) + } + + pub fn data(storage: &S, id: &Uint128) -> StdResult { + AllowedContractData::load(storage, &id.to_be_bytes()) + } + + pub fn save_data( + storage: &mut S, + id: &Uint128, + data: AllowedContractData, + ) -> StdResult<()> { + data.save(storage, &id.to_be_bytes()) + } + + pub fn description( + storage: &S, + id: &Uint128, + ) -> StdResult { + AllowedContractDescription::load(storage, &id.to_be_bytes()) + } + + pub fn save_description( + storage: &mut S, + id: &Uint128, + desc: AllowedContractDescription, + ) -> StdResult<()> { + desc.save(storage, &id.to_be_bytes()) + } +} + +#[cfg(feature = "governance-impl")] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct AllowedContractData { + pub contract: Contract, + pub assemblies: Option>, +} + +#[cfg(feature = "governance-impl")] +impl BucketStorage for AllowedContractData { + const NAMESPACE: &'static [u8] = b"allowed_contract_data-"; +} + +#[cfg(feature = "governance-impl")] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct AllowedContractDescription { + pub name: String, + pub metadata: String, +} + +#[cfg(feature = "governance-impl")] +impl BucketStorage for AllowedContractDescription { + const NAMESPACE: &'static [u8] = b"allowed_contract_description-"; +} diff --git a/packages/shade_protocol/src/contract_interfaces/governance/mod.rs b/packages/shade_protocol/src/contract_interfaces/governance/mod.rs index 68628e303..427b01d66 100644 --- a/packages/shade_protocol/src/contract_interfaces/governance/mod.rs +++ b/packages/shade_protocol/src/contract_interfaces/governance/mod.rs @@ -1,135 +1,243 @@ +pub mod assembly; +pub mod contract; +pub mod profile; pub mod proposal; +#[cfg(feature = "governance-impl")] +pub mod stored_id; pub mod vote; -use crate::utils::{asset::Contract, generic_response::ResponseStatus}; +use crate::{ + contract_interfaces::governance::{ + assembly::{Assembly, AssemblyMsg}, + contract::AllowedContract, + profile::{Profile, UpdateProfile}, + proposal::{Proposal, ProposalMsg}, + vote::Vote, + }, + utils::{asset::Contract, generic_response::ResponseStatus}, +}; use cosmwasm_math_compat::Uint128; -use cosmwasm_std::{Binary, HumanAddr}; +use cosmwasm_std::{Binary, Coin, HumanAddr}; use schemars::JsonSchema; use secret_toolkit::utils::{HandleCallback, InitCallback, Query}; use serde::{Deserialize, Serialize}; -// This is used when calling itself -pub const GOVERNANCE_SELF: &str = "SELF"; +#[cfg(feature = "governance-impl")] +use crate::utils::storage::default::SingletonStorage; // Admin command variable spot -pub const ADMIN_COMMAND_VARIABLE: &str = "{}"; +pub const MSG_VARIABLE: &str = "{~}"; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] pub struct Config { - pub admin: HumanAddr, - // Staking contract - optional to support admin only - pub staker: Option, - // The token allowed for funding - pub funding_token: Contract, - // The amount required to fund a proposal - pub funding_amount: Uint128, - // Proposal funding period deadline - pub funding_deadline: u64, - // Proposal voting period deadline - pub voting_deadline: u64, - // The minimum total amount of votes needed to approve deadline - pub minimum_votes: Uint128, + pub treasury: HumanAddr, + // When public voting is enabled, a voting token is expected + pub vote_token: Option, + // When funding is enabled, a funding token is expected + pub funding_token: Option, } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub struct AdminCommand { - pub msg: String, - pub total_arguments: u16, +#[cfg(feature = "governance-impl")] +impl SingletonStorage for Config { + const NAMESPACE: &'static [u8] = b"config-"; } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct InitMsg { - pub admin: Option, - pub staker: Option, - pub funding_token: Contract, - pub funding_amount: Uint128, - pub funding_deadline: u64, - pub voting_deadline: u64, - pub quorum: Uint128, + pub treasury: HumanAddr, + + // Admin rules + pub admin_members: Vec, + pub admin_profile: Profile, + + // Public rules + pub public_profile: Profile, + pub funding_token: Option, + pub vote_token: Option, } impl InitCallback for InitMsg { const BLOCK_SIZE: usize = 256; } +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum RuntimeState { + // Run like normal + Normal, + // Disable staking + DisableVoteToken, + // Allow only specific assemblys and admin + SpecificAssemblys { commitees: Vec }, + // Set as admin only + AdminOnly, +} + +#[cfg(feature = "governance-impl")] +impl SingletonStorage for RuntimeState { + const NAMESPACE: &'static [u8] = b"runtime_state-"; +} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum HandleMsg { - /// Generic proposal - CreateProposal { - // Contract that will be run - target_contract: String, - // This will be saved as binary - proposal: String, - description: String, + // Internal config + SetConfig { + treasury: Option, + funding_token: Option, + vote_token: Option, + padding: Option, + }, + SetRuntimeState { + state: RuntimeState, + padding: Option, + }, + + // Proposals + // Same as AssemblyProposal where assembly is 0 and assembly msg is 0 + Proposal { + title: String, + metadata: String, + + // Optionals, if none the proposal is assumed to be a text proposal + // Allowed Contract + contract: Option, + // Msg for tx + msg: Option, + coins: Option>, + padding: Option, }, - /// Proposal funding + // Proposal interaction + /// Triggers the proposal when the MSG is approved + Trigger { + proposal: Uint128, + padding: Option, + }, + /// Cancels the proposal if the msg keeps failing + Cancel { + proposal: Uint128, + padding: Option, + }, + /// Forces a proposal update, + /// proposals automatically update on interaction + /// but this is a cheaper alternative + Update { + proposal: Uint128, + padding: Option, + }, + /// Funds a proposal, msg is a prop ID Receive { sender: HumanAddr, + from: HumanAddr, amount: Uint128, - // Proposal ID msg: Option, + memo: Option, + padding: Option, }, - - /// Admin Command - /// These commands can be run by admins any time - AddAdminCommand { - name: String, - proposal: String, + ClaimFunding { + id: Uint128, }, - RemoveAdminCommand { - name: String, + /// Votes on a assembly vote + AssemblyVote { + proposal: Uint128, + vote: Vote, + padding: Option, }, - UpdateAdminCommand { - name: String, - proposal: String, - }, - TriggerAdminCommand { - target: String, - command: String, - variables: Vec, - description: String, - }, - - /// Config changes - UpdateConfig { - admin: Option, - staker: Option, - proposal_deadline: Option, - funding_amount: Option, - funding_deadline: Option, - minimum_votes: Option, + /// Votes on voting token + ReceiveBalance { + sender: HumanAddr, + msg: Option, + balance: Uint128, + memo: Option, }, - DisableStaker {}, + // Assemblies + /// Creates a proposal under a assembly + AssemblyProposal { + assembly: Uint128, + title: String, + metadata: String, - // RequestMigration {} - /// Add a contract to send proposal msgs to - AddSupportedContract { - name: String, - contract: Contract, + // Optionals, if none the proposal is assumed to be a text proposal + msgs: Option>, + padding: Option, }, - RemoveSupportedContract { + + /// Creates a new assembly + AddAssembly { name: String, + metadata: String, + members: Vec, + profile: Uint128, + padding: Option, + }, + /// Edits an existing assembly + SetAssembly { + id: Uint128, + name: Option, + metadata: Option, + members: Option>, + profile: Option, + padding: Option, }, - UpdateSupportedContract { + + // AssemblyMsgs + /// Creates a new assembly message and its allowed users + AddAssemblyMsg { name: String, - contract: Contract, + msg: String, + assemblies: Vec, + padding: Option, + }, + /// Edits an existing assembly msg + SetAssemblyMsg { + id: Uint128, + name: Option, + msg: Option, + assemblies: Option>, + padding: Option, + }, + AddAssemblyMsgAssemblies { + id: Uint128, + assemblies: Vec, }, - /// Proposal voting - can only be done by staking contract - MakeVote { - voter: HumanAddr, - proposal_id: Uint128, - votes: vote::VoteTally, + // Profiles + /// Creates a new profile that can be added to assemblys + AddProfile { + profile: Profile, + padding: Option, + }, + /// Edits an already existing profile and the assemblys using the profile + SetProfile { + id: Uint128, + profile: UpdateProfile, + padding: Option, }, - /// Trigger proposal - TriggerProposal { - proposal_id: Uint128, + // Contracts + AddContract { + name: String, + metadata: String, + contract: Contract, + assemblies: Option>, + padding: Option, + }, + SetContract { + id: Uint128, + name: Option, + metadata: Option, + contract: Option, + disable_assemblies: bool, + assemblies: Option>, + padding: Option, + }, + AddContractAssemblies { + id: Uint128, + assemblies: Vec, }, } @@ -140,73 +248,52 @@ impl HandleCallback for HandleMsg { #[derive(Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum HandleAnswer { - CreateProposal { - status: ResponseStatus, - proposal_id: Uint128, - }, - FundProposal { - status: ResponseStatus, - total_funding: Uint128, - }, - AddAdminCommand { - status: ResponseStatus, - }, - RemoveAdminCommand { - status: ResponseStatus, - }, - UpdateAdminCommand { - status: ResponseStatus, - }, - TriggerAdminCommand { - status: ResponseStatus, - proposal_id: Uint128, - }, - UpdateConfig { - status: ResponseStatus, - }, - DisableStaker { - status: ResponseStatus, - }, - AddSupportedContract { - status: ResponseStatus, - }, - RemoveSupportedContract { - status: ResponseStatus, - }, - UpdateSupportedContract { - status: ResponseStatus, - }, - MakeVote { - status: ResponseStatus, - }, - TriggerProposal { - status: ResponseStatus, - }, + SetConfig { status: ResponseStatus }, + SetRuntimeState { status: ResponseStatus }, + Proposal { status: ResponseStatus }, + ReceiveBalance { status: ResponseStatus }, + Trigger { status: ResponseStatus }, + Cancel { status: ResponseStatus }, + Update { status: ResponseStatus }, + Receive { status: ResponseStatus }, + ClaimFunding { status: ResponseStatus }, + AssemblyVote { status: ResponseStatus }, + AssemblyProposal { status: ResponseStatus }, + AddAssembly { status: ResponseStatus }, + SetAssembly { status: ResponseStatus }, + AddAssemblyMsg { status: ResponseStatus }, + SetAssemblyMsg { status: ResponseStatus }, + AddProfile { status: ResponseStatus }, + SetProfile { status: ResponseStatus }, + AddContract { status: ResponseStatus }, + SetContract { status: ResponseStatus }, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum QueryMsg { - GetProposalVotes { - proposal_id: Uint128, - }, - GetProposals { - start: Uint128, - end: Uint128, - status: Option, - }, - GetProposal { - proposal_id: Uint128, - }, - GetTotalProposals {}, - GetSupportedContracts {}, - GetSupportedContract { - name: String, - }, - GetAdminCommands {}, - GetAdminCommand { - name: String, - }, + // TODO: Query individual user vote with VK and permit + Config {}, + + TotalProposals {}, + + Proposals { start: Uint128, end: Uint128 }, + + TotalAssemblies {}, + + Assemblies { start: Uint128, end: Uint128 }, + + TotalAssemblyMsgs {}, + + AssemblyMsgs { start: Uint128, end: Uint128 }, + + TotalProfiles {}, + + Profiles { start: Uint128, end: Uint128 }, + + TotalContracts {}, + + Contracts { start: Uint128, end: Uint128 }, } impl Query for QueryMsg { @@ -216,28 +303,17 @@ impl Query for QueryMsg { #[derive(Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum QueryAnswer { - ProposalVotes { - status: vote::VoteTally, - }, - Proposals { - proposals: Vec, - }, - Proposal { - proposal: proposal::QueriedProposal, - }, - TotalProposals { - total: Uint128, - }, - SupportedContracts { - contracts: Vec, - }, - SupportedContract { - contract: Contract, - }, - AdminCommands { - commands: Vec, - }, - AdminCommand { - command: AdminCommand, - }, + Config { config: Config }, + + Proposals { props: Vec }, + + Assemblies { assemblies: Vec }, + + AssemblyMsgs { msgs: Vec }, + + Profiles { profiles: Vec }, + + Contracts { contracts: Vec }, + + Total { total: Uint128 }, } diff --git a/packages/shade_protocol/src/contract_interfaces/governance/profile.rs b/packages/shade_protocol/src/contract_interfaces/governance/profile.rs new file mode 100644 index 000000000..b4b461a45 --- /dev/null +++ b/packages/shade_protocol/src/contract_interfaces/governance/profile.rs @@ -0,0 +1,326 @@ +use crate::contract_interfaces::governance::stored_id::ID; +use cosmwasm_math_compat::Uint128; +use cosmwasm_std::{StdError, StdResult, Storage}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "governance-impl")] +use crate::utils::storage::default::BucketStorage; +#[cfg(feature = "governance-impl")] +use crate::utils::storage::default::NaiveBucketStorage; + +/// Allow better control over the safety and privacy features that proposals will need if +/// Assemblys are implemented. If a profile is disabled then its assembly will also be disabled. +/// All percentages are taken as follows 100000 = 100% +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct Profile { + pub name: String, + // State of the current profile and its subsequent assemblies + pub enabled: bool, + // Require assembly voting + #[serde(skip_serializing_if = "Option::is_none")] + pub assembly: Option, + // Require funding + #[serde(skip_serializing_if = "Option::is_none")] + pub funding: Option, + // Require token voting + #[serde(skip_serializing_if = "Option::is_none")] + pub token: Option, + // Once the contract is approved, theres a deadline for the tx to be executed and completed + // else it will just be canceled and assume that the tx failed + pub cancel_deadline: u64, +} + +const COMMITTEE_PROFILE_KEY: &'static [u8] = b"assembly_vote_profile-"; +const TOKEN_PROFILE_KEY: &'static [u8] = b"token_vote_profile-"; + +#[cfg(feature = "governance-impl")] +impl Profile { + pub fn load(storage: &S, id: &Uint128) -> StdResult { + let data = Self::data(storage, id)?; + + Ok(Self { + name: data.name, + enabled: data.enabled, + assembly: Self::assembly_voting(storage, &id)?, + funding: Self::funding(storage, &id)?, + token: Self::public_voting(storage, &id)?, + cancel_deadline: data.cancel_deadline, + }) + } + + pub fn may_load(storage: &S, id: &Uint128) -> StdResult> { + if id > &ID::profile(storage)? { + return Ok(None); + } + Ok(Some(Self::load(storage, id)?)) + } + + pub fn save(&self, storage: &mut S, id: &Uint128) -> StdResult<()> { + ProfileData { + name: self.name.clone(), + enabled: self.enabled, + cancel_deadline: self.cancel_deadline, + } + .save(storage, &id.to_be_bytes())?; + + Self::save_assembly_voting(storage, &id, self.assembly.clone())?; + + Self::save_public_voting(storage, &id, self.token.clone())?; + + Self::save_funding(storage, &id, self.funding.clone())?; + + Ok(()) + } + + pub fn data(storage: &S, id: &Uint128) -> StdResult { + ProfileData::load(storage, &id.to_be_bytes()) + } + + pub fn save_data( + storage: &mut S, + id: &Uint128, + data: ProfileData, + ) -> StdResult<()> { + data.save(storage, &id.to_be_bytes()) + } + + pub fn assembly_voting( + storage: &S, + id: &Uint128, + ) -> StdResult> { + Ok(VoteProfileType::load(storage, COMMITTEE_PROFILE_KEY, &id.to_be_bytes())?.0) + } + + pub fn save_assembly_voting( + storage: &mut S, + id: &Uint128, + assembly: Option, + ) -> StdResult<()> { + VoteProfileType(assembly).save(storage, COMMITTEE_PROFILE_KEY, &id.to_be_bytes()) + } + + pub fn public_voting(storage: &S, id: &Uint128) -> StdResult> { + Ok(VoteProfileType::load(storage, TOKEN_PROFILE_KEY, &id.to_be_bytes())?.0) + } + + pub fn save_public_voting( + storage: &mut S, + id: &Uint128, + token: Option, + ) -> StdResult<()> { + VoteProfileType(token).save(storage, TOKEN_PROFILE_KEY, &id.to_be_bytes()) + } + + pub fn funding(storage: &S, id: &Uint128) -> StdResult> { + Ok(FundProfileType::load(storage, &id.to_be_bytes())?.0) + } + + pub fn save_funding( + storage: &mut S, + id: &Uint128, + funding: Option, + ) -> StdResult<()> { + FundProfileType(funding).save(storage, &id.to_be_bytes()) + } +} + +#[cfg(feature = "governance-impl")] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct ProfileData { + pub name: String, + pub enabled: bool, + pub cancel_deadline: u64, +} + +#[cfg(feature = "governance-impl")] +impl BucketStorage for ProfileData { + const NAMESPACE: &'static [u8] = b"profile_data-"; +} + +#[cfg(feature = "governance-impl")] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +// NOTE: 100% = Uint128(10000) +pub struct VoteProfile { + // Deadline for voting + pub deadline: u64, + // Expected participation threshold + pub threshold: Count, + // Expected yes votes + pub yes_threshold: Count, + // Expected veto votes + pub veto_threshold: Count, +} + +#[cfg(feature = "governance-impl")] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +struct VoteProfileType(pub Option); + +#[cfg(feature = "governance-impl")] +impl NaiveBucketStorage for VoteProfileType {} + +#[cfg(feature = "governance-impl")] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct FundProfile { + // Deadline for funding + pub deadline: u64, + // Amount required to fund + pub required: Uint128, + // Display voter information + pub privacy: bool, + // Deposit loss on vetoed proposal + pub veto_deposit_loss: Uint128, +} + +#[cfg(feature = "governance-impl")] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +struct FundProfileType(pub Option); + +#[cfg(feature = "governance-impl")] +impl BucketStorage for FundProfileType { + const NAMESPACE: &'static [u8] = b"fund_profile-"; +} + +/// Helps simplify the given limits +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum Count { + Percentage { percent: u16 }, + LiteralCount { count: Uint128 }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct UpdateProfile { + pub name: Option, + // State of the current profile and its subsequent assemblies + pub enabled: Option, + // Assembly status + pub disable_assembly: bool, + // Require assembly voting + pub assembly: Option, + // Funding status + pub disable_funding: bool, + // Require funding + pub funding: Option, + // Require token voting + pub disable_token: bool, + // Require token voting + pub token: Option, + // Once the contract is approved, theres a deadline for the tx to be executed and completed + // else it will just be canceled and assume that the tx failed + pub cancel_deadline: Option, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct UpdateVoteProfile { + // Deadline for voting + pub deadline: Option, + // Expected participation threshold + pub threshold: Option, + // Expected yes votes + pub yes_threshold: Option, + // Expected veto votes + pub veto_threshold: Option, +} + +impl UpdateVoteProfile { + pub fn update_profile(&self, profile: &Option) -> StdResult { + let new_profile: VoteProfile; + + if let Some(profile) = profile { + new_profile = VoteProfile { + deadline: self.deadline.unwrap_or(profile.deadline), + threshold: self.threshold.clone().unwrap_or(profile.threshold.clone()), + yes_threshold: self + .yes_threshold + .clone() + .unwrap_or(profile.yes_threshold.clone()), + veto_threshold: self + .veto_threshold + .clone() + .unwrap_or(profile.veto_threshold.clone()), + }; + } else { + new_profile = VoteProfile { + deadline: match self.deadline { + None => Err(StdError::generic_err("Vote profile must be set")), + Some(ret) => Ok(ret), + }?, + threshold: match self.threshold.clone() { + None => Err(StdError::generic_err("Vote profile must be set")), + Some(ret) => Ok(ret), + }?, + yes_threshold: match self.yes_threshold.clone() { + None => Err(StdError::generic_err("Vote profile must be set")), + Some(ret) => Ok(ret), + }?, + veto_threshold: match self.veto_threshold.clone() { + None => Err(StdError::generic_err("Vote profile must be set")), + Some(ret) => Ok(ret), + }?, + }; + } + + Ok(new_profile) + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct UpdateFundProfile { + // Deadline for funding + pub deadline: Option, + // Amount required to fund + pub required: Option, + // Display voter information + pub privacy: Option, + // Deposit loss on vetoed proposal + pub veto_deposit_loss: Option, +} + +impl UpdateFundProfile { + pub fn update_profile(&self, profile: &Option) -> StdResult { + let new_profile: FundProfile; + + if let Some(profile) = profile { + new_profile = FundProfile { + deadline: self.deadline.unwrap_or(profile.deadline), + required: self.required.unwrap_or(profile.required), + privacy: self.privacy.unwrap_or(profile.privacy), + veto_deposit_loss: self + .veto_deposit_loss + .clone() + .unwrap_or(profile.veto_deposit_loss.clone()), + }; + } else { + new_profile = FundProfile { + deadline: match self.deadline { + None => Err(StdError::generic_err("Fund profile must be set")), + Some(ret) => Ok(ret), + }?, + required: match self.required { + None => Err(StdError::generic_err("Fund profile must be set")), + Some(ret) => Ok(ret), + }?, + privacy: match self.privacy { + None => Err(StdError::generic_err("Fund profile must be set")), + Some(ret) => Ok(ret), + }?, + veto_deposit_loss: match self.veto_deposit_loss.clone() { + None => Err(StdError::generic_err("Fund profile must be set")), + Some(ret) => Ok(ret), + }?, + }; + } + + Ok(new_profile) + } +} diff --git a/packages/shade_protocol/src/contract_interfaces/governance/proposal.rs b/packages/shade_protocol/src/contract_interfaces/governance/proposal.rs index eb8a6d94c..327f3955f 100644 --- a/packages/shade_protocol/src/contract_interfaces/governance/proposal.rs +++ b/packages/shade_protocol/src/contract_interfaces/governance/proposal.rs @@ -1,49 +1,427 @@ -use crate::utils::generic_response::ResponseStatus; +use crate::{ + contract_interfaces::governance::{ + assembly::Assembly, + profile::Profile, + stored_id::ID, + vote::Vote, + }, + utils::{asset::Contract, generic_response::ResponseStatus}, +}; use cosmwasm_math_compat::Uint128; -use cosmwasm_std::Binary; +use cosmwasm_std::{Binary, Coin, HumanAddr, StdResult, Storage}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +#[cfg(feature = "governance-impl")] +use crate::utils::storage::default::BucketStorage; +use crate::utils::storage::default::NaiveBucketStorage; + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct Proposal { - // Proposal ID - pub id: Uint128, - // Target smart contract - pub target: String, - // Message to execute - pub msg: Binary, - // Description of proposal - pub description: String, + // Description + // Address of the proposal proposer + pub proposer: HumanAddr, + // Proposal title + pub title: String, + // Description of proposal, can be in base64 + pub metadata: String, + + // Msg + #[serde(skip_serializing_if = "Option::is_none")] + pub msgs: Option>, + + // Assembly + // Assembly that called the proposal + pub assembly: Uint128, + + #[serde(skip_serializing_if = "Option::is_none")] + pub assembly_vote_tally: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub public_vote_tally: Option, + + // Status + pub status: Status, + + // Status History + pub status_history: Vec, + + // Funders + // Leave as an option so we can hide the data if None + #[serde(skip_serializing_if = "Option::is_none")] + pub funders: Option>, +} + +const ASSEMBLY_VOTE: &'static [u8] = b"user-assembly-vote-"; +const ASSEMBLY_VOTES: &'static [u8] = b"total-assembly-votes-"; +const PUBLIC_VOTE: &'static [u8] = b"user-public-vote-"; +const PUBLIC_VOTES: &'static [u8] = b"total-public-votes-"; + +#[cfg(feature = "governance-impl")] +impl Proposal { + pub fn save(&self, storage: &mut S, id: &Uint128) -> StdResult<()> { + if let Some(msgs) = self.msgs.clone() { + Self::save_msg(storage, &id, msgs)?; + } + + Self::save_description(storage, &id, ProposalDescription { + proposer: self.proposer.clone(), + title: self.title.clone(), + metadata: self.metadata.clone(), + })?; + + Self::save_assembly(storage, &id, self.assembly)?; + + Self::save_status(storage, &id, self.status.clone())?; + + Self::save_status_history(storage, &id, self.status_history.clone())?; + + if let Some(funder_list) = self.funders.clone() { + let mut funders = vec![]; + for (funder, funding) in funder_list.iter() { + funders.push(funder.clone()); + Self::save_funding(storage, id, &funder, Funding { + amount: *funding, + claimed: false, + })? + } + Self::save_funders(storage, id, funders)?; + } + + Ok(()) + } + + pub fn may_load(storage: &S, id: &Uint128) -> StdResult> { + if id > &ID::proposal(storage)? { + return Ok(None); + } + Ok(Some(Self::load(storage, id)?)) + } + + pub fn load(storage: &S, id: &Uint128) -> StdResult { + let msgs = Self::msg(storage, id)?; + let description = Self::description(storage, &id)?; + let assembly = Self::assembly(storage, &id)?; + let status = Self::status(storage, &id)?; + let status_history = Self::status_history(storage, &id)?; + + let mut funders_arr = vec![]; + for funder in Self::funders(storage, &id)?.iter() { + funders_arr.push((funder.clone(), Self::funding(storage, &id, &funder)?.amount)) + } + + let mut funders: Option> = None; + if !funders_arr.is_empty() { + if let Some(prof) = + Profile::funding(storage, &Assembly::data(storage, &assembly)?.profile)? + { + if !prof.privacy { + funders = Some(funders_arr); + } + } + } + + let assembly_data = Assembly::data(storage, &assembly)?; + + Ok(Self { + title: description.title, + proposer: description.proposer, + metadata: description.metadata, + msgs, + assembly, + assembly_vote_tally: match Profile::assembly_voting(storage, &assembly_data.profile)? { + None => None, + Some(_) => Some(Self::assembly_votes(storage, &id)?), + }, + public_vote_tally: match Profile::public_voting(storage, &assembly_data.profile)? { + None => None, + Some(_) => Some(Self::public_votes(storage, &id)?), + }, + status, + status_history, + funders, + }) + } + + pub fn msg(storage: &S, id: &Uint128) -> StdResult>> { + match ProposalMsgs::may_load(storage, &id.to_be_bytes())? { + None => Ok(None), + Some(i) => Ok(Some(i.0)), + } + } + + pub fn save_msg( + storage: &mut S, + id: &Uint128, + data: Vec, + ) -> StdResult<()> { + ProposalMsgs(data).save(storage, &id.to_be_bytes()) + } + + pub fn description(storage: &S, id: &Uint128) -> StdResult { + ProposalDescription::load(storage, &id.to_be_bytes()) + } + + pub fn save_description( + storage: &mut S, + id: &Uint128, + data: ProposalDescription, + ) -> StdResult<()> { + data.save(storage, &id.to_be_bytes()) + } + + pub fn assembly(storage: &S, id: &Uint128) -> StdResult { + Ok(ProposalAssembly::load(storage, &id.to_be_bytes())?.0) + } + + pub fn save_assembly( + storage: &mut S, + id: &Uint128, + data: Uint128, + ) -> StdResult<()> { + ProposalAssembly(data).save(storage, &id.to_be_bytes()) + } + + pub fn status(storage: &S, id: &Uint128) -> StdResult { + Status::load(storage, &id.to_be_bytes()) + } + + pub fn save_status(storage: &mut S, id: &Uint128, data: Status) -> StdResult<()> { + data.save(storage, &id.to_be_bytes()) + } + + pub fn status_history(storage: &S, id: &Uint128) -> StdResult> { + Ok(StatusHistory::load(storage, &id.to_be_bytes())?.0) + } + + pub fn save_status_history( + storage: &mut S, + id: &Uint128, + data: Vec, + ) -> StdResult<()> { + StatusHistory(data).save(storage, &id.to_be_bytes()) + } + + pub fn funders(storage: &S, id: &Uint128) -> StdResult> { + let funders = match Funders::may_load(storage, &id.to_be_bytes())? { + None => vec![], + Some(item) => item.0, + }; + Ok(funders) + } + + pub fn save_funders( + storage: &mut S, + id: &Uint128, + data: Vec, + ) -> StdResult<()> { + Funders(data).save(storage, &id.to_be_bytes()) + } + + pub fn funding(storage: &S, id: &Uint128, user: &HumanAddr) -> StdResult { + let key = id.to_string() + "-" + user.as_str(); + Funding::load(storage, key.as_bytes()) + } + + pub fn save_funding( + storage: &mut S, + id: &Uint128, + user: &HumanAddr, + data: Funding, + ) -> StdResult<()> { + let key = id.to_string() + "-" + user.as_str(); + data.save(storage, key.as_bytes()) + } + + // User assembly votes + pub fn assembly_vote( + storage: &S, + id: &Uint128, + user: &HumanAddr, + ) -> StdResult> { + let key = id.to_string() + "-" + user.as_str(); + Ok(Vote::may_load(storage, ASSEMBLY_VOTE, key.as_bytes())?) + } + + pub fn save_assembly_vote( + storage: &mut S, + id: &Uint128, + user: &HumanAddr, + data: &Vote, + ) -> StdResult<()> { + let key = id.to_string() + "-" + user.as_str(); + Vote::write(storage, ASSEMBLY_VOTE).save(key.as_bytes(), data) + } + + // Total assembly votes + pub fn assembly_votes(storage: &S, id: &Uint128) -> StdResult { + match Vote::may_load(storage, ASSEMBLY_VOTES, &id.to_be_bytes())? { + None => Ok(Vote::default()), + Some(vote) => Ok(vote), + } + } + + pub fn save_assembly_votes( + storage: &mut S, + id: &Uint128, + data: &Vote, + ) -> StdResult<()> { + Vote::write(storage, ASSEMBLY_VOTES).save(&id.to_be_bytes(), data) + } + + // User public votes + pub fn public_vote( + storage: &S, + id: &Uint128, + user: &HumanAddr, + ) -> StdResult> { + let key = id.to_string() + "-" + user.as_str(); + Ok(Vote::may_load(storage, PUBLIC_VOTE, key.as_bytes())?) + } + + pub fn save_public_vote( + storage: &mut S, + id: &Uint128, + user: &HumanAddr, + data: &Vote, + ) -> StdResult<()> { + let key = id.to_string() + "-" + user.as_str(); + Vote::write(storage, PUBLIC_VOTE).save(key.as_bytes(), data) + } + + // Total public votes + pub fn public_votes(storage: &S, id: &Uint128) -> StdResult { + match Vote::may_load(storage, PUBLIC_VOTES, &id.to_be_bytes())? { + None => Ok(Vote::default()), + Some(vote) => Ok(vote), + } + } + + pub fn save_public_votes( + storage: &mut S, + id: &Uint128, + data: &Vote, + ) -> StdResult<()> { + Vote::write(storage, PUBLIC_VOTES).save(&id.to_be_bytes(), data) + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct ProposalDescription { + pub proposer: HumanAddr, + pub title: String, + pub metadata: String, +} + +#[cfg(feature = "governance-impl")] +impl BucketStorage for ProposalDescription { + const NAMESPACE: &'static [u8] = b"proposal_description-"; } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] -pub struct QueriedProposal { - pub id: Uint128, - pub target: String, +pub struct ProposalMsg { + pub target: Uint128, + pub assembly_msg: Uint128, + // Used as both Vec when calling a handleMsg and Vec when saving the msg pub msg: Binary, - pub description: String, - pub funding_deadline: u64, - pub voting_deadline: Option, - pub total_funding: Uint128, - pub status: ProposalStatus, - pub run_status: Option, + pub send: Vec, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +struct ProposalMsgs(pub Vec); + +#[cfg(feature = "governance-impl")] +impl BucketStorage for ProposalMsgs { + const NAMESPACE: &'static [u8] = b"proposal_msgs-"; +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +struct ProposalAssembly(pub Uint128); + +#[cfg(feature = "governance-impl")] +impl BucketStorage for ProposalAssembly { + const NAMESPACE: &'static [u8] = b"proposal_assembly-"; } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] -pub enum ProposalStatus { - // Admin command called - AdminRequested, +pub enum Status { + // Assembly voting period + AssemblyVote { + start: u64, + end: u64, + }, // In funding period - Funding, + Funding { + amount: Uint128, + start: u64, + end: u64, + }, // Voting in progress - Voting, + Voting { + start: u64, + end: u64, + }, // Total votes did not reach minimum total votes Expired, - // Majority voted No + // Proposal was rejected Rejected, - // Majority votes yes - Passed, + // Proposal was vetoed + // NOTE: percent it stored because proposal settings can change before claiming + Vetoed { + slash_percent: Uint128, + }, + // Proposal was approved, has a set timeline before it can be canceled + Passed { + start: u64, + end: u64, + }, + // If proposal is a msg then it was executed and was successful + Success, + // Proposal never got executed after a cancel deadline, + // assumed that tx failed everytime it got triggered + Canceled, +} + +#[cfg(feature = "governance-impl")] +impl BucketStorage for Status { + const NAMESPACE: &'static [u8] = b"proposal_status-"; +} + +#[cfg(feature = "governance-impl")] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +struct StatusHistory(pub Vec); + +#[cfg(feature = "governance-impl")] +impl BucketStorage for StatusHistory { + const NAMESPACE: &'static [u8] = b"proposal_status_history-"; +} + +#[cfg(feature = "governance-impl")] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +struct Funders(pub Vec); + +#[cfg(feature = "governance-impl")] +impl BucketStorage for Funders { + const NAMESPACE: &'static [u8] = b"proposal_funders-"; +} + +#[cfg(feature = "governance-impl")] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct Funding { + pub amount: Uint128, + pub claimed: bool, +} + +#[cfg(feature = "governance-impl")] +impl BucketStorage for Funding { + const NAMESPACE: &'static [u8] = b"proposal_funding-"; } diff --git a/packages/shade_protocol/src/contract_interfaces/governance/stored_id.rs b/packages/shade_protocol/src/contract_interfaces/governance/stored_id.rs new file mode 100644 index 000000000..b9e2e6c1b --- /dev/null +++ b/packages/shade_protocol/src/contract_interfaces/governance/stored_id.rs @@ -0,0 +1,105 @@ +use crate::utils::storage::default::NaiveSingletonStorage; +use cosmwasm_math_compat::Uint128; +use cosmwasm_std::{StdResult, Storage}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[serde(rename_all = "snake_case")] +// Used to get total IDs +pub struct ID(Uint128); + +impl NaiveSingletonStorage for ID {} + +const PROP_KEY: &'static [u8] = b"proposal_id-"; +const COMMITTEE_KEY: &'static [u8] = b"assembly_id-"; +const COMMITTEE_MSG_KEY: &'static [u8] = b"assembly_msg_id-"; +const PROFILE_KEY: &'static [u8] = b"profile_id-"; +const CONTRACT_KEY: &'static [u8] = b"allowed_contract_id-"; + +impl ID { + // Load current ID related proposals + pub fn set_proposal(storage: &mut S, id: Uint128) -> StdResult<()> { + ID(id).save(storage, PROP_KEY) + } + + pub fn proposal(storage: &S) -> StdResult { + Ok(ID::load(storage, PROP_KEY)?.0) + } + + pub fn add_proposal(storage: &mut S) -> StdResult { + let item = match ID::may_load(storage, PROP_KEY)? { + None => ID(Uint128::zero()), + Some(i) => { + let item = ID(i.0.checked_add(Uint128::new(1))?); + item + } + }; + item.save(storage, PROP_KEY)?; + Ok(item.0) + } + + // Assembly + pub fn set_assembly(storage: &mut S, id: Uint128) -> StdResult<()> { + ID(id).save(storage, COMMITTEE_KEY) + } + + pub fn assembly(storage: &S) -> StdResult { + Ok(ID::load(storage, COMMITTEE_KEY)?.0) + } + + pub fn add_assembly(storage: &mut S) -> StdResult { + let mut item = ID::load(storage, COMMITTEE_KEY)?; + item.0 += Uint128::new(1); + item.save(storage, COMMITTEE_KEY)?; + Ok(item.0) + } + + // Assembly Msg + pub fn set_assembly_msg(storage: &mut S, id: Uint128) -> StdResult<()> { + ID(id).save(storage, COMMITTEE_MSG_KEY) + } + + pub fn assembly_msg(storage: &S) -> StdResult { + Ok(ID::load(storage, COMMITTEE_MSG_KEY)?.0) + } + + pub fn add_assembly_msg(storage: &mut S) -> StdResult { + let mut item = ID::load(storage, COMMITTEE_MSG_KEY)?; + item.0 += Uint128::new(1); + item.save(storage, COMMITTEE_MSG_KEY)?; + Ok(item.0) + } + + // Profile + pub fn set_profile(storage: &mut S, id: Uint128) -> StdResult<()> { + ID(id).save(storage, PROFILE_KEY) + } + + pub fn profile(storage: &S) -> StdResult { + Ok(ID::load(storage, PROFILE_KEY)?.0) + } + + pub fn add_profile(storage: &mut S) -> StdResult { + let mut item = ID::load(storage, PROFILE_KEY)?; + item.0 += Uint128::new(1); + item.save(storage, PROFILE_KEY)?; + Ok(item.0) + } + + // Contract + // Profile + pub fn set_contract(storage: &mut S, id: Uint128) -> StdResult<()> { + ID(id).save(storage, CONTRACT_KEY) + } + + pub fn contract(storage: &S) -> StdResult { + Ok(ID::load(storage, CONTRACT_KEY)?.0) + } + + pub fn add_contract(storage: &mut S) -> StdResult { + let mut item = ID::load(storage, CONTRACT_KEY)?; + item.0 += Uint128::new(1); + item.save(storage, CONTRACT_KEY)?; + Ok(item.0) + } +} diff --git a/packages/shade_protocol/src/contract_interfaces/governance/vote.rs b/packages/shade_protocol/src/contract_interfaces/governance/vote.rs index 7f30d5826..6252b77a4 100644 --- a/packages/shade_protocol/src/contract_interfaces/governance/vote.rs +++ b/packages/shade_protocol/src/contract_interfaces/governance/vote.rs @@ -1,27 +1,82 @@ use cosmwasm_math_compat::Uint128; +use cosmwasm_std::{StdResult, Storage}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +#[cfg(feature = "governance-impl")] +use crate::utils::storage::default::NaiveBucketStorage; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct ReceiveBalanceMsg { + pub vote: Vote, + pub proposal: Uint128, +} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] -pub struct VoteTally { +pub struct Vote { pub yes: Uint128, pub no: Uint128, + pub no_with_veto: Uint128, pub abstain: Uint128, } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum Vote { - Yes, - No, - Abstain, +#[cfg(feature = "governance-impl")] +impl NaiveBucketStorage for Vote {} + +impl Default for Vote { + fn default() -> Self { + Self { + yes: Uint128::zero(), + no: Uint128::zero(), + no_with_veto: Uint128::zero(), + abstain: Uint128::zero(), + } + } } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case")] -/// Used to give weight to votes per user -pub struct UserVote { - pub vote: Vote, - pub weight: u8, +impl Vote { + pub fn total_count(&self) -> StdResult { + Ok(self.yes.checked_add( + self.no + .checked_add(self.no_with_veto.checked_add(self.abstain)?)?, + )?) + } + + pub fn checked_sub(&self, vote: &Self) -> StdResult { + Ok(Self { + yes: self.yes.checked_sub(vote.yes)?, + no: self.no.checked_sub(vote.no)?, + no_with_veto: self.no_with_veto.checked_sub(vote.no_with_veto)?, + abstain: self.abstain.checked_sub(vote.abstain)?, + }) + } + + pub fn checked_add(&self, vote: &Self) -> StdResult { + Ok(Self { + yes: self.yes.checked_add(vote.yes)?, + no: self.no.checked_add(vote.no)?, + no_with_veto: self.no_with_veto.checked_add(vote.no_with_veto)?, + abstain: self.abstain.checked_add(vote.abstain)?, + }) + } +} + +pub struct TalliedVotes { + pub yes: Uint128, + pub no: Uint128, + pub veto: Uint128, + pub total: Uint128, +} + +impl TalliedVotes { + pub fn tally(votes: Vote) -> Self { + Self { + yes: votes.yes, + no: votes.no + votes.no_with_veto, + veto: votes.no_with_veto, + total: votes.yes + votes.no + votes.no_with_veto + votes.abstain, + } + } }