diff --git a/Cargo.lock b/Cargo.lock index 2e345b014..2c7355eea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2551,6 +2551,7 @@ dependencies = [ "anyhow", "cosmwasm-schema", "cosmwasm-std", + "cw-denom", "cw-multi-test", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", diff --git a/contracts/gauges/gauge-adapter/Cargo.toml b/contracts/gauges/gauge-adapter/Cargo.toml index a03773ebe..0a61e4b8f 100644 --- a/contracts/gauges/gauge-adapter/Cargo.toml +++ b/contracts/gauges/gauge-adapter/Cargo.toml @@ -27,6 +27,7 @@ cw20 = { workspace = true } cw-utils = { workspace = true } semver = { workspace = true } thiserror = { workspace = true } +cw-denom = { workspace = true } [dev-dependencies] anyhow = { workspace = true } diff --git a/contracts/gauges/gauge-adapter/src/contract.rs b/contracts/gauges/gauge-adapter/src/contract.rs index a53cab416..7494f3653 100644 --- a/contracts/gauges/gauge-adapter/src/contract.rs +++ b/contracts/gauges/gauge-adapter/src/contract.rs @@ -1,15 +1,19 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - coins, from_json, to_json_binary, Addr, BankMsg, Binary, CosmosMsg, Deps, DepsMut, Env, - MessageInfo, Order, Response, StdError, StdResult, Uint128, WasmMsg, + from_json, to_json_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Order, Response, + StdResult, Uint128, }; use cw2::set_contract_version; -use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg}; +use cw20::Cw20ReceiveMsg; +use cw_denom::UncheckedDenom; +use cw_utils::{one_coin, PaymentError}; use crate::error::ContractError; -use crate::msg::{AdapterQueryMsg, ExecuteMsg, InstantiateMsg, MigrateMsg, ReceiveMsg}; -use crate::state::{AssetType, Config, Submission, CONFIG, SUBMISSIONS}; +use crate::msg::{ + AdapterQueryMsg, AssetUnchecked, ExecuteMsg, InstantiateMsg, MigrateMsg, ReceiveMsg, +}; +use crate::state::{Config, Submission, CONFIG, SUBMISSIONS}; // Version info for migration info. const CONTRACT_NAME: &str = "crates.io:marketing-gauge-adapter"; @@ -37,9 +41,12 @@ pub fn instantiate( let config = Config { admin: deps.api.addr_validate(&msg.admin)?, - required_deposit: msg.required_deposit, + required_deposit: msg + .required_deposit + .map(|x| x.into_checked(deps.as_ref())) + .transpose()?, community_pool, - reward: msg.reward, + reward: msg.reward.into_checked(deps.as_ref())?, }; CONFIG.save(deps.storage, &config)?; @@ -56,17 +63,17 @@ pub fn execute( match msg { ExecuteMsg::Receive(msg) => receive_cw20_message(deps, info, msg), ExecuteMsg::CreateSubmission { name, url, address } => { - // TODO this is very hacky - let received = info.funds.into_iter().next().unwrap_or_default(); - execute::create_submission( - deps, - info.sender, - name, - url, - address, - received.amount, - AssetType::Native(received.denom), - ) + let received = match one_coin(&info) { + Ok(coin) => Ok(Some(coin)), + Err(PaymentError::NoFunds {}) => Ok(None), + Err(error) => Err(error), + }? + .map(|x| AssetUnchecked { + denom: UncheckedDenom::Native(x.denom), + amount: x.amount, + }); + + execute::create_submission(deps, info.sender, name, url, address, received) } ExecuteMsg::ReturnDeposits {} => execute::return_deposits(deps, info.sender), } @@ -78,44 +85,17 @@ fn receive_cw20_message( msg: Cw20ReceiveMsg, ) -> Result { match from_json(&msg.msg)? { - ReceiveMsg::CreateSubmission { name, url, address } => { - let denom = AssetType::Cw20(info.sender.to_string()); - execute::create_submission( - deps, - Addr::unchecked(msg.sender), - name, - url, - address, - msg.amount, - denom, - ) - } - } -} - -fn create_bank_msg( - denom: &AssetType, - amount: Uint128, - recipient: Addr, -) -> Result { - match denom { - AssetType::Cw20(address) => Ok(WasmMsg::Execute { - contract_addr: address.to_string(), - msg: to_json_binary(&Cw20ExecuteMsg::Transfer { - recipient: recipient.to_string(), - amount, - })?, - funds: vec![], - } - .into()), - AssetType::Native(denom) => { - let amount = coins(amount.u128(), denom); - Ok(BankMsg::Send { - to_address: recipient.to_string(), - amount, - } - .into()) - } + ReceiveMsg::CreateSubmission { name, url, address } => execute::create_submission( + deps, + Addr::unchecked(msg.sender), + name, + url, + address, + Some(AssetUnchecked::new_cw20( + info.sender.as_str(), + msg.amount.u128(), + )), + ), } } @@ -130,8 +110,7 @@ pub mod execute { name: String, url: String, address: String, - received_amount: Uint128, - received_denom: AssetType, + received: Option, ) -> Result { let address = deps.api.addr_validate(&address)?; @@ -142,20 +121,23 @@ pub mod execute { admin: _, } = CONFIG.load(deps.storage)?; if let Some(required_deposit) = required_deposit { - if AssetType::Native("".into()) == received_denom { - return Err(ContractError::DepositRequired {}); - } - if required_deposit.denom != received_denom { - return Err(ContractError::InvalidDepositType {}); + if let Some(received) = received { + let received_denom = received.denom.into_checked(deps.as_ref())?; + + if required_deposit.denom != received_denom { + return Err(ContractError::InvalidDepositType {}); + } + if received.amount != required_deposit.amount { + return Err(ContractError::InvalidDepositAmount { + correct_amount: required_deposit.amount, + }); + } + } else { + return Err(ContractError::PaymentError(PaymentError::NoFunds {})); } - if received_amount != required_deposit.amount { - return Err(ContractError::InvalidDepositAmount { - correct_amount: required_deposit.amount, - }); - } - } else { + } else if let Some(received) = received { // If no deposit is required, then any deposit invalidates a submission. - if !received_amount.is_zero() { + if !received.amount.is_zero() { return Err(ContractError::InvalidDepositAmount { correct_amount: Uint128::zero(), }); @@ -190,11 +172,10 @@ pub mod execute { .range(deps.storage, None, None, Order::Ascending) .map(|item| { let (_submission_recipient, submission) = item?; - create_bank_msg( - &required_deposit.denom, - required_deposit.amount, - submission.sender, - ) + + required_deposit + .denom + .get_transfer_to_message(&submission.sender, required_deposit.amount) }) .collect::>>()?; @@ -221,7 +202,7 @@ pub fn query(deps: Deps, _env: Env, msg: AdapterQueryMsg) -> StdResult { } mod query { - use cosmwasm_std::{CosmosMsg, Decimal}; + use cosmwasm_std::{CosmosMsg, Decimal, StdError}; use crate::msg::{ AllOptionsResponse, AllSubmissionsResponse, CheckOptionResponse, SampleGaugeMsgsResponse, @@ -254,12 +235,16 @@ mod query { let execute = winners .into_iter() .map(|(to_address, fraction)| { - // Gauge already sents chosen tally to this query by using results we send in + // Gauge already sends chosen tally to this query by using results we send in // all_options query; they are already validated - create_bank_msg( - &reward.denom, - fraction * reward.amount, - Addr::unchecked(to_address), + let to_address = deps.api.addr_validate(&to_address)?; + + reward.denom.get_transfer_to_message( + &to_address, + reward + .amount + .checked_mul_floor(fraction) + .map_err(|x| StdError::generic_err(x.to_string()))?, ) }) .collect::>>()?; @@ -306,20 +291,23 @@ mod tests { use super::*; use cosmwasm_std::{ + coins, testing::{mock_dependencies, mock_env, mock_info}, - Decimal, Uint128, + BankMsg, CosmosMsg, Decimal, Uint128, WasmMsg, }; + use cw20::Cw20ExecuteMsg; + use cw_denom::CheckedDenom; - use crate::state::Asset; + use crate::{msg::AssetUnchecked, state::Asset}; #[test] fn proper_initialization() { let mut deps = mock_dependencies(); let msg = InstantiateMsg { admin: "admin".to_owned(), - required_deposit: Some(Asset::new_cw20("wynd", 10_000_000)), + required_deposit: Some(AssetUnchecked::new_cw20("wynd", 10_000_000)), community_pool: "community".to_owned(), - reward: Asset::new_native("ujuno", 150_000_000_000), + reward: AssetUnchecked::new_native("ujuno", 150_000_000_000), }; instantiate( deps.as_mut(), @@ -335,7 +323,7 @@ mod tests { assert_eq!( config.required_deposit, Some(Asset { - denom: AssetType::Cw20("wynd".to_owned()), + denom: CheckedDenom::Cw20(Addr::unchecked("wynd")), amount: Uint128::new(10_000_000) }) ); @@ -343,13 +331,13 @@ mod tests { assert_eq!( config.reward, Asset { - denom: AssetType::Native("ujuno".to_owned()), + denom: CheckedDenom::Native("ujuno".to_owned()), amount: Uint128::new(150_000_000_000) } ); let msg = InstantiateMsg { - reward: Asset::new_native("ujuno", 10_000_000), + reward: AssetUnchecked::new_native("ujuno", 10_000_000), ..msg }; instantiate( @@ -363,7 +351,7 @@ mod tests { assert_eq!( config.reward, Asset { - denom: AssetType::Native("ujuno".to_owned()), + denom: CheckedDenom::Native("ujuno".to_owned()), amount: Uint128::new(10_000_000) } ); @@ -384,9 +372,9 @@ mod tests { let reward = Uint128::new(150_000_000_000); let msg = InstantiateMsg { admin: "admin".to_owned(), - required_deposit: Some(Asset::new_cw20("wynd", 10_000_000)), + required_deposit: Some(AssetUnchecked::new_cw20("wynd", 10_000_000)), community_pool: "community".to_owned(), - reward: Asset::new_native("ujuno", reward.into()), + reward: AssetUnchecked::new_native("ujuno", reward.into()), }; instantiate(deps.as_mut(), mock_env(), mock_info("user", &[]), msg).unwrap(); @@ -432,9 +420,9 @@ mod tests { let reward = Uint128::new(150_000_000_000); let msg = InstantiateMsg { admin: "admin".to_owned(), - required_deposit: Some(Asset::new_cw20("wynd", 10_000_000)), + required_deposit: Some(AssetUnchecked::new_cw20("wynd", 10_000_000)), community_pool: "community".to_owned(), - reward: Asset::new_cw20("wynd", reward.into()), + reward: AssetUnchecked::new_cw20("wynd", reward.into()), }; instantiate(deps.as_mut(), mock_env(), mock_info("user", &[]), msg).unwrap(); @@ -495,7 +483,7 @@ mod tests { admin: "admin".to_owned(), required_deposit: None, community_pool: "community".to_owned(), - reward: Asset::new_native("ujuno", 150_000_000_000), + reward: AssetUnchecked::new_native("ujuno", 150_000_000_000), }; instantiate( deps.as_mut(), @@ -509,7 +497,7 @@ mod tests { assert_eq!(err, ContractError::NoDepositToRefund {}); let msg = InstantiateMsg { - required_deposit: Some(Asset::new_native("ujuno", 10_000_000)), + required_deposit: Some(AssetUnchecked::new_native("ujuno", 10_000_000)), ..msg }; instantiate(deps.as_mut(), mock_env(), mock_info("user", &[]), msg).unwrap(); diff --git a/contracts/gauges/gauge-adapter/src/error.rs b/contracts/gauges/gauge-adapter/src/error.rs index 75521219d..6994dc494 100644 --- a/contracts/gauges/gauge-adapter/src/error.rs +++ b/contracts/gauges/gauge-adapter/src/error.rs @@ -1,4 +1,6 @@ use cosmwasm_std::{StdError, Uint128}; +use cw_denom::DenomError; +use cw_utils::PaymentError; use thiserror::Error; #[derive(Error, Debug, PartialEq)] @@ -6,6 +8,12 @@ pub enum ContractError { #[error("{0}")] Std(#[from] StdError), + #[error("{0}")] + PaymentError(#[from] PaymentError), + + #[error("{0}")] + DenomError(#[from] DenomError), + #[error("Operation unauthorized - only admin can release deposits")] Unauthorized {}, @@ -20,7 +28,4 @@ pub enum ContractError { #[error("No deposit was required, therefore no deposit can be returned")] NoDepositToRefund {}, - - #[error("Deposit required, cannot create submission.")] - DepositRequired {}, } diff --git a/contracts/gauges/gauge-adapter/src/helpers.rs b/contracts/gauges/gauge-adapter/src/helpers.rs new file mode 100644 index 000000000..862be36d4 --- /dev/null +++ b/contracts/gauges/gauge-adapter/src/helpers.rs @@ -0,0 +1,27 @@ +use cosmwasm_std::Deps; +use cw_denom::{DenomError, UncheckedDenom}; + +use crate::{msg::AssetUnchecked, state::Asset}; + +impl AssetUnchecked { + pub fn into_checked(self, deps: Deps) -> Result { + Ok(Asset { + denom: self.denom.into_checked(deps)?, + amount: self.amount, + }) + } + + pub fn new_native(denom: &str, amount: u128) -> Self { + Self { + denom: UncheckedDenom::Native(denom.to_owned()), + amount: amount.into(), + } + } + + pub fn new_cw20(denom: &str, amount: u128) -> Self { + Self { + denom: UncheckedDenom::Cw20(denom.to_owned()), + amount: amount.into(), + } + } +} diff --git a/contracts/gauges/gauge-adapter/src/lib.rs b/contracts/gauges/gauge-adapter/src/lib.rs index 31d9a5aa4..aae805d76 100644 --- a/contracts/gauges/gauge-adapter/src/lib.rs +++ b/contracts/gauges/gauge-adapter/src/lib.rs @@ -1,5 +1,6 @@ pub mod contract; mod error; +mod helpers; pub mod msg; pub mod state; diff --git a/contracts/gauges/gauge-adapter/src/msg.rs b/contracts/gauges/gauge-adapter/src/msg.rs index dc1ae2d7c..928155cf7 100644 --- a/contracts/gauges/gauge-adapter/src/msg.rs +++ b/contracts/gauges/gauge-adapter/src/msg.rs @@ -1,19 +1,18 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, CosmosMsg, Decimal}; +use cosmwasm_std::{Addr, CosmosMsg, Decimal, Uint128}; use cw20::Cw20ReceiveMsg; - -use crate::state::Asset; +use cw_denom::UncheckedDenom; #[cw_serde] pub struct InstantiateMsg { /// Address that is allowed to return deposits. pub admin: String, /// Deposit required for valid submission. This option allows to reduce spam. - pub required_deposit: Option, + pub required_deposit: Option, /// Address of contract where each deposit is transferred. pub community_pool: String, /// Total reward amount. - pub reward: Asset, + pub reward: AssetUnchecked, } #[cw_serde] @@ -97,3 +96,9 @@ pub struct SubmissionResponse { pub struct AllSubmissionsResponse { pub submissions: Vec, } + +#[cw_serde] +pub struct AssetUnchecked { + pub denom: UncheckedDenom, + pub amount: Uint128, +} diff --git a/contracts/gauges/gauge-adapter/src/multitest/submission.rs b/contracts/gauges/gauge-adapter/src/multitest/submission.rs index 4243ace50..34780c613 100644 --- a/contracts/gauges/gauge-adapter/src/multitest/submission.rs +++ b/contracts/gauges/gauge-adapter/src/multitest/submission.rs @@ -162,9 +162,12 @@ fn create_submission_required_deposit() { ) .unwrap_err(); - assert_eq!(ContractError::DepositRequired {}, err.downcast().unwrap()); + assert_eq!( + ContractError::PaymentError(cw_utils::PaymentError::NoFunds {}), + err.downcast().unwrap() + ); - // Fails if correct denom but not enought amount. + // Fails if correct denom but not enough amount. let err = suite .execute_create_submission( suite.owner.clone(), diff --git a/contracts/gauges/gauge-adapter/src/multitest/suite.rs b/contracts/gauges/gauge-adapter/src/multitest/suite.rs index 53ac2b14d..1b72bba7d 100644 --- a/contracts/gauges/gauge-adapter/src/multitest/suite.rs +++ b/contracts/gauges/gauge-adapter/src/multitest/suite.rs @@ -1,20 +1,18 @@ use cosmwasm_std::{to_json_binary, Addr, Binary, Coin, Uint128}; use cw20::{BalanceResponse, Cw20QueryMsg}; use cw20::{Cw20Coin, MinterResponse}; +use cw_denom::UncheckedDenom; use cw_multi_test::{App, AppResponse, ContractWrapper, Executor}; use anyhow::Result as AnyResult; use cw20_base::msg::ExecuteMsg as Cw20BaseExecuteMsg; use cw20_base::msg::InstantiateMsg as Cw20BaseInstantiateMsg; -use crate::msg::CheckOptionResponse; -use crate::{ - msg::{ - AdapterQueryMsg, AllOptionsResponse, AllSubmissionsResponse, ExecuteMsg, ReceiveMsg, - SubmissionResponse, - }, - state::{Asset, AssetType}, +use crate::msg::{ + AdapterQueryMsg, AllOptionsResponse, AllSubmissionsResponse, ExecuteMsg, ReceiveMsg, + SubmissionResponse, }; +use crate::msg::{AssetUnchecked, CheckOptionResponse}; pub const NATIVE: &str = "juno"; pub const CW20: &str = "wynd"; @@ -46,8 +44,8 @@ fn store_cw20(app: &mut App) -> u64 { pub struct SuiteBuilder { // Gauge adapter's instantiate params community_pool: String, - required_deposit: Option, - reward: Asset, + required_deposit: Option, + reward: AssetUnchecked, funds: Vec<(Addr, Vec)>, cw20_funds: Vec, } @@ -57,8 +55,8 @@ impl SuiteBuilder { Self { community_pool: "community".to_owned(), required_deposit: None, - reward: Asset { - denom: AssetType::Native(NATIVE.into()), + reward: AssetUnchecked { + denom: UncheckedDenom::Native(NATIVE.into()), amount: Uint128::new(1_000_000), }, funds: vec![], @@ -88,8 +86,8 @@ impl SuiteBuilder { // Allows to initialize the marketing gauge adapter with required native coins in the config. pub fn with_native_deposit(mut self, amount: u128) -> Self { - self.required_deposit = Some(Asset { - denom: AssetType::Native(NATIVE.into()), + self.required_deposit = Some(AssetUnchecked { + denom: UncheckedDenom::Native(NATIVE.into()), amount: Uint128::from(amount), }); self @@ -97,8 +95,8 @@ impl SuiteBuilder { // Allows to initialize the marketing gauge adapter with required cw20 tokens in the config. pub fn with_cw20_deposit(mut self, amount: u128) -> Self { - self.required_deposit = Some(Asset { - denom: AssetType::Cw20("contract1".to_string()), + self.required_deposit = Some(AssetUnchecked { + denom: UncheckedDenom::Cw20("contract1".to_string()), amount: Uint128::from(amount), }); self diff --git a/contracts/gauges/gauge-adapter/src/state.rs b/contracts/gauges/gauge-adapter/src/state.rs index e2d836943..68bd19d12 100644 --- a/contracts/gauges/gauge-adapter/src/state.rs +++ b/contracts/gauges/gauge-adapter/src/state.rs @@ -1,5 +1,6 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Uint128}; +use cw_denom::CheckedDenom; use cw_storage_plus::{Item, Map}; #[cw_serde] @@ -16,34 +17,12 @@ pub struct Config { pub const CONFIG: Item = Item::new("config"); -#[cw_serde] -pub enum AssetType { - Native(String), - Cw20(String), -} - #[cw_serde] pub struct Asset { - pub denom: AssetType, + pub denom: CheckedDenom, pub amount: Uint128, } -impl Asset { - pub fn new_native(denom: &str, amount: u128) -> Self { - Self { - denom: AssetType::Native(denom.to_owned()), - amount: amount.into(), - } - } - - pub fn new_cw20(denom: &str, amount: u128) -> Self { - Self { - denom: AssetType::Cw20(denom.to_owned()), - amount: amount.into(), - } - } -} - #[cw_serde] pub struct Submission { pub sender: Addr, @@ -51,5 +30,5 @@ pub struct Submission { pub url: String, } -// All submissions indexed by submition's fund destination address. +// All submissions mapped by fund destination address. pub const SUBMISSIONS: Map = Map::new("submissions"); diff --git a/contracts/gauges/gauge/README.md b/contracts/gauges/gauge/README.md index de8f2c035..ef87daf0c 100644 --- a/contracts/gauges/gauge/README.md +++ b/contracts/gauges/gauge/README.md @@ -24,13 +24,13 @@ and eventually stopping them if we don't need them anymore (to avoid extra write ## Gauge Functionality -A gauge is initialised with a set of options. Anyone with voting power may vote for any option at any time, -which is recorded, and also updates the tally. If they revote, it checks their last vote to reduce power on +A gauge is initialized with a set of options. Anyone with voting power may vote for any option at any time, +which is recorded, and also updates the tally. If they re-vote, it checks their last vote to reduce power on that before adding to the new one. When an "update hook" is triggered, it updates the voting power of that user's vote, while maintaining the same option. Either increasing or decreasing the tally for the given option as appropriate. Every epoch (eg 1/week), the current tally of the gauge is sampled, and some cut-off applies (top 20, min 0.5% of votes, etc). The resulting set is the "selected set" and the options along with -their relative vote counts (normalised to 1.0 = total votes within this set) is used to initiate some +their relative vote counts (normalized to 1.0 = total votes within this set) is used to initiate some action (eg. distribute reward tokens). ## Extensibility