From 94497667c41ed7cc643778a948bcd0dd5b8f8505 Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Wed, 30 Nov 2022 23:23:40 +0100 Subject: [PATCH 01/36] wip --- contracts/treasury/.cargo/config | 6 + contracts/treasury/Cargo.toml | 22 ++ contracts/treasury/Makefile | 9 + contracts/treasury/README.md | 1 + contracts/treasury/examples/schema.rs | 31 ++ contracts/treasury/schema/execute_msg.json | 114 ++++++ .../treasury/schema/instantiate_msg.json | 34 ++ contracts/treasury/schema/query_msg.json | 43 +++ contracts/treasury/src/contract.rs | 324 ++++++++++++++++++ contracts/treasury/src/lib.rs | 3 + contracts/treasury/src/msg.rs | 63 ++++ contracts/treasury/src/state.rs | 26 ++ 12 files changed, 676 insertions(+) create mode 100644 contracts/treasury/.cargo/config create mode 100644 contracts/treasury/Cargo.toml create mode 100644 contracts/treasury/Makefile create mode 100644 contracts/treasury/README.md create mode 100644 contracts/treasury/examples/schema.rs create mode 100644 contracts/treasury/schema/execute_msg.json create mode 100644 contracts/treasury/schema/instantiate_msg.json create mode 100644 contracts/treasury/schema/query_msg.json create mode 100644 contracts/treasury/src/contract.rs create mode 100644 contracts/treasury/src/lib.rs create mode 100644 contracts/treasury/src/msg.rs create mode 100644 contracts/treasury/src/state.rs diff --git a/contracts/treasury/.cargo/config b/contracts/treasury/.cargo/config new file mode 100644 index 00000000..7c115322 --- /dev/null +++ b/contracts/treasury/.cargo/config @@ -0,0 +1,6 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +wasm-debug = "build --target wasm32-unknown-unknown" +unit-test = "test --lib --features backtraces" +integration-test = "test --test integration" +schema = "run --example schema" diff --git a/contracts/treasury/Cargo.toml b/contracts/treasury/Cargo.toml new file mode 100644 index 00000000..d6a3e41c --- /dev/null +++ b/contracts/treasury/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "neutron-treasury" +version = "0.1.0" +authors = ["Sergey Ratiashvili "] +edition = "2018" +license = "Apache-2.0" +repository = "https://github.com/neutron/neutron-dao" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +backtraces = ["cosmwasm-std/backtraces"] + +[dependencies] +cosmwasm-std = { version = "1.0" } +cw-storage-plus = "0.13" +schemars = "0.8.1" +serde = { version = "1.0.103", default-features = false, features = ["derive"] } + +[dev-dependencies] +cosmwasm-schema = { version = "1.0.0", default-features = false } diff --git a/contracts/treasury/Makefile b/contracts/treasury/Makefile new file mode 100644 index 00000000..e2aa6a26 --- /dev/null +++ b/contracts/treasury/Makefile @@ -0,0 +1,9 @@ +CURRENT_DIR = $(shell pwd) +CURRENT_DIR_RELATIVE = $(notdir $(shell pwd)) + +clippy: + rustup component add clippy || true + cargo clippy --all-targets --all-features --workspace -- -D warnings + +test: clippy + cargo unit-test diff --git a/contracts/treasury/README.md b/contracts/treasury/README.md new file mode 100644 index 00000000..9db39578 --- /dev/null +++ b/contracts/treasury/README.md @@ -0,0 +1 @@ +# Neutron treasury diff --git a/contracts/treasury/examples/schema.rs b/contracts/treasury/examples/schema.rs new file mode 100644 index 00000000..7fa49979 --- /dev/null +++ b/contracts/treasury/examples/schema.rs @@ -0,0 +1,31 @@ +// Copyright 2022 Neutron +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::env::current_dir; +use std::fs::create_dir_all; + +use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; + +use neutron_treasury::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + let mut out_dir = current_dir().unwrap(); + out_dir.push("schema"); + create_dir_all(&out_dir).unwrap(); + remove_schemas(&out_dir).unwrap(); + + export_schema(&schema_for!(InstantiateMsg), &out_dir); + export_schema(&schema_for!(ExecuteMsg), &out_dir); + export_schema(&schema_for!(QueryMsg), &out_dir); +} diff --git a/contracts/treasury/schema/execute_msg.json b/contracts/treasury/schema/execute_msg.json new file mode 100644 index 00000000..311dbc0c --- /dev/null +++ b/contracts/treasury/schema/execute_msg.json @@ -0,0 +1,114 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "description": "Transfer the contract's ownership to another account", + "type": "object", + "required": [ + "transfer_ownership" + ], + "properties": { + "transfer_ownership": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "set_shares" + ], + "properties": { + "set_shares": { + "type": "object", + "required": [ + "shares" + ], + "properties": { + "shares": { + "type": "array", + "items": { + "type": "array", + "items": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/Uint128" + } + ], + "maxItems": 2, + "minItems": 2 + } + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Distribute funds to the and distribution accounts according to their shares", + "type": "object", + "required": [ + "distribute" + ], + "properties": { + "distribute": { + "type": "array", + "items": [], + "maxItems": 0, + "minItems": 0 + } + }, + "additionalProperties": false + }, + { + "description": "Distribute pending funds between Bank and Distribution accounts", + "type": "object", + "required": [ + "grab" + ], + "properties": { + "grab": { + "type": "array", + "items": [], + "maxItems": 0, + "minItems": 0 + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "payout" + ], + "properties": { + "payout": { + "type": "object", + "required": [ + "amount", + "recipient" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "recipient": { + "type": "string" + } + } + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/treasury/schema/instantiate_msg.json b/contracts/treasury/schema/instantiate_msg.json new file mode 100644 index 00000000..371bf565 --- /dev/null +++ b/contracts/treasury/schema/instantiate_msg.json @@ -0,0 +1,34 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "dao", + "denom", + "distribution_rate", + "min_time_elapsed_between_fundings", + "owner" + ], + "properties": { + "dao": { + "type": "string" + }, + "denom": { + "type": "string" + }, + "distribution_rate": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "min_time_elapsed_between_fundings": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "owner": { + "description": "The contract's owner", + "type": "string" + } + } +} diff --git a/contracts/treasury/schema/query_msg.json b/contracts/treasury/schema/query_msg.json new file mode 100644 index 00000000..92e8cc80 --- /dev/null +++ b/contracts/treasury/schema/query_msg.json @@ -0,0 +1,43 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "description": "The contract's configurations; returns [`ConfigResponse`]", + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "stats" + ], + "properties": { + "stats": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "shares" + ], + "properties": { + "shares": { + "type": "object" + } + }, + "additionalProperties": false + } + ] +} diff --git a/contracts/treasury/src/contract.rs b/contracts/treasury/src/contract.rs new file mode 100644 index 00000000..016b3d3d --- /dev/null +++ b/contracts/treasury/src/contract.rs @@ -0,0 +1,324 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + to_binary, Addr, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Order, + Response, StdError, StdResult, Storage, Uint128, +}; +use cw_storage_plus::KeyDeserialize; + +use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg, StatsResponse}; +use crate::state::{ + Config, BANK_BALANCE, CONFIG, DISTRIBUTION_BALANCE, LAST_BALANCE, PENDING_DISTRIBUTION, SHARES, + TOTAL_BANK_SPENT, TOTAL_DISTRIBUTED, TOTAL_RECEIVED, +}; + +// const CONTRACT_NAME: &str = "crates.io:neutron-treasury"; +// const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +//-------------------------------------------------------------------------------------------------- +// Instantiation +//-------------------------------------------------------------------------------------------------- + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> StdResult { + let config = Config { + denom: msg.denom, + min_time_elapsed_between_fundings: msg.min_time_elapsed_between_fundings, + distribution_rate: msg.distribution_rate, + owner: deps.api.addr_validate(&msg.owner)?, + dao: deps.api.addr_validate(&msg.dao)?, + }; + CONFIG.save(deps.storage, &config)?; + TOTAL_RECEIVED.save(deps.storage, &Uint128::zero())?; + TOTAL_BANK_SPENT.save(deps.storage, &Uint128::zero())?; + TOTAL_DISTRIBUTED.save(deps.storage, &Uint128::zero())?; + LAST_BALANCE.save(deps.storage, &Uint128::zero())?; + DISTRIBUTION_BALANCE.save(deps.storage, &Uint128::zero())?; + BANK_BALANCE.save(deps.storage, &Uint128::zero())?; + + Ok(Response::new()) +} + +//-------------------------------------------------------------------------------------------------- +// Executions +//-------------------------------------------------------------------------------------------------- + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> StdResult { + let api = deps.api; + match msg { + // permissioned - owner + ExecuteMsg::TransferOwnership(new_owner) => { + exec_transfer_ownership(deps, info.sender, api.addr_validate(&new_owner)?) + } + // permissioned - dao + ExecuteMsg::SetShares { shares } => exec_set_shares(deps, info, shares), + // permissionless + ExecuteMsg::Distribute {} => exec_distribute(deps, env), + // permissionless + ExecuteMsg::Grab {} => exec_grab(deps, env), + // permissioned - dao + ExecuteMsg::Payout { amount, recipient } => exec_payout(deps, info, env, amount, recipient), + } +} + +pub fn exec_transfer_ownership( + deps: DepsMut, + sender_addr: Addr, + new_owner_addr: Addr, +) -> StdResult { + let config = CONFIG.load(deps.storage)?; + let old_owner = config.owner; + if sender_addr != old_owner { + return Err(StdError::generic_err("only owner can transfer ownership")); + } + + CONFIG.update(deps.storage, |mut config| -> StdResult<_> { + config.owner = new_owner_addr.clone(); + Ok(config) + })?; + + Ok(Response::new() + .add_attribute("action", "neutron/treasury/transfer_ownership") + .add_attribute("previous_owner", old_owner) + .add_attribute("new_owner", new_owner_addr)) +} + +pub fn exec_grab(deps: DepsMut, env: Env) -> StdResult { + let config = CONFIG.load(deps.storage)?; + if config.distribution_rate == 0 { + return Err(StdError::generic_err("distribution rate is zero")); + } + let last_balance = LAST_BALANCE.load(deps.storage)?; + let current_balance = deps + .querier + .query_balance(env.contract.address, config.denom)?; + if current_balance.amount.eq(&last_balance) { + return Err(StdError::generic_err("no new funds to grab")); + } + let to_distribute = current_balance.amount.checked_sub(last_balance)?; + let mut to_bank = to_distribute + .checked_mul(config.distribution_rate.into())? + .checked_div(100u128.into())?; + let to_distribution = to_distribute.checked_sub(to_bank)?; + // update bank + let bank_balance = BANK_BALANCE.load(deps.storage)?; + BANK_BALANCE.save(deps.storage, &(bank_balance.checked_add(to_bank)?))?; + // update total received + let total_received = TOTAL_RECEIVED.load(deps.storage)?; + TOTAL_RECEIVED.save(deps.storage, &(total_received.checked_add(to_distribute)?))?; + + // // distribute to shares + let shares = SHARES + .range(deps.storage, None, None, Order::Ascending) + .into_iter() + .collect::>>()?; + let sum_of_shares: Uint128 = shares.iter().fold(Uint128::zero(), |acc, (_, v)| acc + *v); + let mut distributed = Uint128::zero(); + for (addr, share) in shares { + let amount = to_distribution + .checked_mul(share)? + .checked_div(sum_of_shares)?; + let p = PENDING_DISTRIBUTION.load(deps.storage, &addr); + match p { + Ok(p) => { + PENDING_DISTRIBUTION.save(deps.storage, &addr, &(p.checked_add(amount)?))?; + } + Err(_) => { + PENDING_DISTRIBUTION.save(deps.storage, &addr, &amount)?; + } + } + distributed = distributed.checked_add(amount)?; + } + + if distributed != to_distribution { + to_bank = to_bank.checked_add(to_distribution.checked_sub(distributed)?)?; + } + + // update bank + let bank_balance = BANK_BALANCE.load(deps.storage)?; + BANK_BALANCE.save(deps.storage, &(bank_balance.checked_add(to_bank)?))?; + + // update distribution balance + let distribution_balance = DISTRIBUTION_BALANCE.load(deps.storage)?; + DISTRIBUTION_BALANCE.save( + deps.storage, + &(distribution_balance.checked_add(distributed)?), + )?; + + LAST_BALANCE.save(deps.storage, ¤t_balance.amount)?; + + Ok(Response::default() + .add_attribute("action", "neutron/treasury/grab") + .add_attribute("bank_balance", bank_balance) + .add_attribute("distribution_balance", distribution_balance)) +} + +pub fn exec_payout( + deps: DepsMut, + info: MessageInfo, + env: Env, + amount: Uint128, + recipient: String, +) -> StdResult { + let config: Config = CONFIG.load(deps.storage)?; + let denom = config.denom; + if info.sender != config.dao { + return Err(StdError::generic_err("only dao can payout")); + } + let bank_balance = BANK_BALANCE.load(deps.storage)?; + let distribute_balance = DISTRIBUTION_BALANCE.load(deps.storage)?; + if amount > bank_balance { + return Err(StdError::generic_err("insufficient funds")); + } + let current_balance = deps + .querier + .query_balance(env.contract.address, denom.clone())?; + if bank_balance.checked_add(distribute_balance)? != current_balance.amount { + return Err(StdError::generic_err("inconsistent state")); + } + BANK_BALANCE.save(deps.storage, &(bank_balance.checked_sub(amount)?))?; + let total_bank_spent = TOTAL_BANK_SPENT.load(deps.storage)?; + TOTAL_BANK_SPENT.save(deps.storage, &(total_bank_spent.checked_add(amount)?))?; + LAST_BALANCE.save(deps.storage, ¤t_balance.amount.checked_sub(amount)?)?; + + Ok(Response::new() + .add_message(CosmosMsg::Bank(BankMsg::Send { + to_address: recipient.clone(), + amount: vec![Coin { denom, amount }], + })) + .add_attribute("action", "neutron/treasury/payout") + .add_attribute("amount", amount) + .add_attribute("recipient", recipient)) +} + +pub fn exec_distribute(deps: DepsMut, env: Env) -> StdResult { + let config: Config = CONFIG.load(deps.storage)?; + let denom = config.denom.as_str(); + let distribution_balance = DISTRIBUTION_BALANCE.load(deps.storage)?; + let bank_balance = BANK_BALANCE.load(deps.storage)?; + let current_balance = deps.querier.query_balance(env.contract.address, denom)?; + if bank_balance.checked_add(distribution_balance)? != current_balance.amount { + return Err(StdError::generic_err("inconsistent state")); + } + let pending_distribution = PENDING_DISTRIBUTION + .range(deps.storage, None, None, cosmwasm_std::Order::Ascending) + .into_iter() + .collect::>>()?; + let mut msgs = vec![]; + let mut spent = Uint128::zero(); + for one in pending_distribution { + let (addr, amount) = one; + spent = spent.checked_add(amount)?; + let msg = CosmosMsg::Bank(BankMsg::Send { + to_address: Addr::from_slice(&addr.clone())?.to_string(), + amount: vec![Coin { + denom: denom.to_string(), + amount, + }], + }); + msgs.push(msg); + PENDING_DISTRIBUTION.remove(deps.storage, &addr); + } + + LAST_BALANCE.save(deps.storage, ¤t_balance.amount.checked_sub(spent)?)?; + let total_distributed = TOTAL_DISTRIBUTED.load(deps.storage)?; + TOTAL_DISTRIBUTED.save(deps.storage, &(&total_distributed.checked_add(spent)?))?; + + Ok(Response::new() + .add_messages(msgs) + .add_attribute("action", "neutron/treasury/distribute")) +} + +pub fn exec_set_shares( + deps: DepsMut, + info: MessageInfo, + shares: Vec<(String, Uint128)>, +) -> StdResult { + let config = CONFIG.load(deps.storage)?; + if info.sender != config.dao { + return Err(StdError::generic_err("only dao can set shares")); + } + let mut new_shares = vec![]; + for (addr, share) in shares { + let addr = deps.api.addr_validate(&addr)?; + let addr_raw = addr.as_bytes(); + SHARES.save(deps.storage, addr_raw, &share)?; + new_shares.push((addr_raw.to_vec(), share)); + } + remove_all_shares(deps.storage)?; + for (addr, shares) in new_shares.clone() { + SHARES.save(deps.storage, &addr, &shares)?; + } + Ok(Response::new() + .add_attribute("action", "neutron/treasury/set_shares") + .add_attribute("shares", format!("{:?}", new_shares))) +} + +fn remove_all_shares(storage: &mut dyn Storage) -> StdResult<()> { + let keys = SHARES + .keys(storage, None, None, Order::Ascending) + .into_iter() + .fold(vec![], |mut acc, key| { + acc.push(key.unwrap()); + acc + }); + + for key in keys.iter() { + SHARES.remove(storage, &key); + } + Ok(()) +} + +//-------------------------------------------------------------------------------------------------- +// Queries +//-------------------------------------------------------------------------------------------------- + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Config {} => to_binary(&query_config(deps)?), + QueryMsg::Stats {} => to_binary(&query_stats(deps)?), + QueryMsg::Shares {} => to_binary(&query_shares(deps)?), + } +} + +pub fn query_config(deps: Deps) -> StdResult { + let config = CONFIG.load(deps.storage)?; + Ok(config) +} + +pub fn query_stats(deps: Deps) -> StdResult { + let total_received = TOTAL_RECEIVED.load(deps.storage)?; + let total_bank_spent = TOTAL_BANK_SPENT.load(deps.storage)?; + let total_distributed = TOTAL_DISTRIBUTED.load(deps.storage)?; + let last_balance = LAST_BALANCE.load(deps.storage)?; + let distribution_balance = DISTRIBUTION_BALANCE.load(deps.storage)?; + let bank_balance = BANK_BALANCE.load(deps.storage)?; + + Ok(StatsResponse { + total_received, + total_bank_spent, + total_distributed, + last_balance, + distribution_balance, + bank_balance, + }) +} + +pub fn query_shares(deps: Deps) -> StdResult> { + let shares = SHARES + .range(deps.storage, None, None, cosmwasm_std::Order::Ascending) + .into_iter() + .collect::>>()?; + let mut res: Vec<(String, Uint128)> = vec![]; + for (addr, shares) in shares.iter() { + res.push((Addr::from_slice(addr)?.to_string(), *shares)); + } + Ok(res) +} diff --git a/contracts/treasury/src/lib.rs b/contracts/treasury/src/lib.rs new file mode 100644 index 00000000..4934c19d --- /dev/null +++ b/contracts/treasury/src/lib.rs @@ -0,0 +1,3 @@ +pub mod contract; +pub mod msg; +pub mod state; diff --git a/contracts/treasury/src/msg.rs b/contracts/treasury/src/msg.rs new file mode 100644 index 00000000..ce749ab5 --- /dev/null +++ b/contracts/treasury/src/msg.rs @@ -0,0 +1,63 @@ +use cosmwasm_std::{Addr, Uint128}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct InstantiateMsg { + /// The contract's owner + pub owner: String, + pub dao: String, + pub denom: String, + pub distribution_rate: u8, + pub min_time_elapsed_between_fundings: u64, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ExecuteMsg { + /// Transfer the contract's ownership to another account + TransferOwnership(String), + + SetShares { + shares: Vec<(String, Uint128)>, + }, + + /// Distribute funds to the and distribution accounts according to their shares + Distribute(), + + /// Distribute pending funds between Bank and Distribution accounts + Grab(), + + // Payout funds at DAO decision + Payout { + amount: Uint128, + recipient: String, + }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum QueryMsg { + /// The contract's configurations; returns [`ConfigResponse`] + Config {}, + Stats {}, + Shares {}, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct StatsResponse { + pub total_received: Uint128, + pub total_bank_spent: Uint128, + pub total_distributed: Uint128, + pub bank_balance: Uint128, + pub distribution_balance: Uint128, + pub last_balance: Uint128, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct ShareResponse { + address: Addr, + shares: Uint128, +} diff --git a/contracts/treasury/src/state.rs b/contracts/treasury/src/state.rs new file mode 100644 index 00000000..f207b19f --- /dev/null +++ b/contracts/treasury/src/state.rs @@ -0,0 +1,26 @@ +use cosmwasm_std::{Addr, Uint128}; +use cw_storage_plus::{Item, Map}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct Config { + pub distribution_rate: u8, + pub min_time_elapsed_between_fundings: u64, + pub denom: String, + pub owner: Addr, + pub dao: Addr, +} + +pub const TOTAL_RECEIVED: Item = Item::new("total_received"); +pub const TOTAL_BANK_SPENT: Item = Item::new("total_bank_spent"); +pub const TOTAL_DISTRIBUTED: Item = Item::new("total_distributed"); + +pub const LAST_BALANCE: Item = Item::new("last_balance"); +pub const DISTRIBUTION_BALANCE: Item = Item::new("distribution_balance"); +pub const PENDING_DISTRIBUTION: Map<&[u8], Uint128> = Map::new("pending_distribution"); +pub const BANK_BALANCE: Item = Item::new("bank_balance"); + +pub const SHARES: Map<&[u8], Uint128> = Map::new("shares"); + +pub const CONFIG: Item = Item::new("config"); From 5be8258ebadf2d123da0f8444a4f7a8948016f18 Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Fri, 2 Dec 2022 16:24:08 +0100 Subject: [PATCH 02/36] wip --- contracts/distribution/.cargo/config | 6 + contracts/distribution/Cargo.toml | 22 ++ contracts/distribution/Makefile | 9 + contracts/distribution/README.md | 1 + contracts/distribution/examples/schema.rs | 31 ++ .../distribution/schema/execute_msg.json | 114 ++++++ .../distribution/schema/instantiate_msg.json | 40 ++ contracts/distribution/schema/query_msg.json | 43 +++ contracts/distribution/src/contract.rs | 352 ++++++++++++++++++ contracts/distribution/src/lib.rs | 3 + contracts/distribution/src/msg.rs | 64 ++++ contracts/distribution/src/state.rs | 29 ++ contracts/treasury/schema/execute_msg.json | 53 +-- .../treasury/schema/instantiate_msg.json | 8 +- contracts/treasury/schema/query_msg.json | 12 - contracts/treasury/src/contract.rs | 201 +++------- contracts/treasury/src/lib.rs | 2 + contracts/treasury/src/msg.rs | 20 +- contracts/treasury/src/state.rs | 10 +- .../treasury/src/testing/mock_querier.rs | 22 ++ contracts/treasury/src/testing/mod.rs | 2 + contracts/treasury/src/testing/tests.rs | 144 +++++++ 22 files changed, 949 insertions(+), 239 deletions(-) create mode 100644 contracts/distribution/.cargo/config create mode 100644 contracts/distribution/Cargo.toml create mode 100644 contracts/distribution/Makefile create mode 100644 contracts/distribution/README.md create mode 100644 contracts/distribution/examples/schema.rs create mode 100644 contracts/distribution/schema/execute_msg.json create mode 100644 contracts/distribution/schema/instantiate_msg.json create mode 100644 contracts/distribution/schema/query_msg.json create mode 100644 contracts/distribution/src/contract.rs create mode 100644 contracts/distribution/src/lib.rs create mode 100644 contracts/distribution/src/msg.rs create mode 100644 contracts/distribution/src/state.rs create mode 100644 contracts/treasury/src/testing/mock_querier.rs create mode 100644 contracts/treasury/src/testing/mod.rs create mode 100644 contracts/treasury/src/testing/tests.rs diff --git a/contracts/distribution/.cargo/config b/contracts/distribution/.cargo/config new file mode 100644 index 00000000..7c115322 --- /dev/null +++ b/contracts/distribution/.cargo/config @@ -0,0 +1,6 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +wasm-debug = "build --target wasm32-unknown-unknown" +unit-test = "test --lib --features backtraces" +integration-test = "test --test integration" +schema = "run --example schema" diff --git a/contracts/distribution/Cargo.toml b/contracts/distribution/Cargo.toml new file mode 100644 index 00000000..439d10cd --- /dev/null +++ b/contracts/distribution/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "neutron-distribution" +version = "0.1.0" +authors = ["Sergey Ratiashvili "] +edition = "2018" +license = "Apache-2.0" +repository = "https://github.com/neutron/neutron-dao" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +backtraces = ["cosmwasm-std/backtraces"] + +[dependencies] +cosmwasm-std = { version = "1.0" } +cw-storage-plus = "0.13" +schemars = "0.8.1" +serde = { version = "1.0.103", default-features = false, features = ["derive"] } + +[dev-dependencies] +cosmwasm-schema = { version = "1.0.0", default-features = false } diff --git a/contracts/distribution/Makefile b/contracts/distribution/Makefile new file mode 100644 index 00000000..e2aa6a26 --- /dev/null +++ b/contracts/distribution/Makefile @@ -0,0 +1,9 @@ +CURRENT_DIR = $(shell pwd) +CURRENT_DIR_RELATIVE = $(notdir $(shell pwd)) + +clippy: + rustup component add clippy || true + cargo clippy --all-targets --all-features --workspace -- -D warnings + +test: clippy + cargo unit-test diff --git a/contracts/distribution/README.md b/contracts/distribution/README.md new file mode 100644 index 00000000..9db39578 --- /dev/null +++ b/contracts/distribution/README.md @@ -0,0 +1 @@ +# Neutron treasury diff --git a/contracts/distribution/examples/schema.rs b/contracts/distribution/examples/schema.rs new file mode 100644 index 00000000..81843cbb --- /dev/null +++ b/contracts/distribution/examples/schema.rs @@ -0,0 +1,31 @@ +// Copyright 2022 Neutron +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::env::current_dir; +use std::fs::create_dir_all; + +use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; + +use neutron_distribution::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + let mut out_dir = current_dir().unwrap(); + out_dir.push("schema"); + create_dir_all(&out_dir).unwrap(); + remove_schemas(&out_dir).unwrap(); + + export_schema(&schema_for!(InstantiateMsg), &out_dir); + export_schema(&schema_for!(ExecuteMsg), &out_dir); + export_schema(&schema_for!(QueryMsg), &out_dir); +} diff --git a/contracts/distribution/schema/execute_msg.json b/contracts/distribution/schema/execute_msg.json new file mode 100644 index 00000000..311dbc0c --- /dev/null +++ b/contracts/distribution/schema/execute_msg.json @@ -0,0 +1,114 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "description": "Transfer the contract's ownership to another account", + "type": "object", + "required": [ + "transfer_ownership" + ], + "properties": { + "transfer_ownership": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "set_shares" + ], + "properties": { + "set_shares": { + "type": "object", + "required": [ + "shares" + ], + "properties": { + "shares": { + "type": "array", + "items": { + "type": "array", + "items": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/Uint128" + } + ], + "maxItems": 2, + "minItems": 2 + } + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Distribute funds to the and distribution accounts according to their shares", + "type": "object", + "required": [ + "distribute" + ], + "properties": { + "distribute": { + "type": "array", + "items": [], + "maxItems": 0, + "minItems": 0 + } + }, + "additionalProperties": false + }, + { + "description": "Distribute pending funds between Bank and Distribution accounts", + "type": "object", + "required": [ + "grab" + ], + "properties": { + "grab": { + "type": "array", + "items": [], + "maxItems": 0, + "minItems": 0 + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "payout" + ], + "properties": { + "payout": { + "type": "object", + "required": [ + "amount", + "recipient" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "recipient": { + "type": "string" + } + } + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/distribution/schema/instantiate_msg.json b/contracts/distribution/schema/instantiate_msg.json new file mode 100644 index 00000000..6c4562f1 --- /dev/null +++ b/contracts/distribution/schema/instantiate_msg.json @@ -0,0 +1,40 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "dao", + "denom", + "distribution_rate", + "min_time_elapsed_between_distributions", + "min_time_elapsed_between_grabs", + "owner" + ], + "properties": { + "dao": { + "type": "string" + }, + "denom": { + "type": "string" + }, + "distribution_rate": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "min_time_elapsed_between_distributions": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "min_time_elapsed_between_grabs": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "owner": { + "description": "The contract's owner", + "type": "string" + } + } +} diff --git a/contracts/distribution/schema/query_msg.json b/contracts/distribution/schema/query_msg.json new file mode 100644 index 00000000..92e8cc80 --- /dev/null +++ b/contracts/distribution/schema/query_msg.json @@ -0,0 +1,43 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "description": "The contract's configurations; returns [`ConfigResponse`]", + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "stats" + ], + "properties": { + "stats": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "shares" + ], + "properties": { + "shares": { + "type": "object" + } + }, + "additionalProperties": false + } + ] +} diff --git a/contracts/distribution/src/contract.rs b/contracts/distribution/src/contract.rs new file mode 100644 index 00000000..9ef18bbf --- /dev/null +++ b/contracts/distribution/src/contract.rs @@ -0,0 +1,352 @@ +use std::sync::Arc; + +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + to_binary, Addr, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Order, + Response, StdError, StdResult, Storage, Uint128, +}; +use cw_storage_plus::KeyDeserialize; + +use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg, StatsResponse}; +use crate::state::{ + Config, BANK_BALANCE, CONFIG, DISTRIBUTION_BALANCE, LAST_BALANCE, LAST_DISTRIBUTION_TIME, + LAST_GRAB_TIME, PENDING_DISTRIBUTION, SHARES, TOTAL_BANK_SPENT, TOTAL_DISTRIBUTED, + TOTAL_RECEIVED, +}; + +// const CONTRACT_NAME: &str = "crates.io:neutron-treasury"; +// const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +//-------------------------------------------------------------------------------------------------- +// Instantiation +//-------------------------------------------------------------------------------------------------- + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> StdResult { + let config = Config { + denom: msg.denom, + min_time_elapsed_between_distributions: msg.min_time_elapsed_between_distributions, + min_time_elapsed_between_grabs: msg.min_time_elapsed_between_grabs, + distribution_rate: msg.distribution_rate, + owner: deps.api.addr_validate(&msg.owner)?, + dao: deps.api.addr_validate(&msg.dao)?, + }; + CONFIG.save(deps.storage, &config)?; + TOTAL_RECEIVED.save(deps.storage, &Uint128::zero())?; + TOTAL_BANK_SPENT.save(deps.storage, &Uint128::zero())?; + TOTAL_DISTRIBUTED.save(deps.storage, &Uint128::zero())?; + LAST_BALANCE.save(deps.storage, &Uint128::zero())?; + DISTRIBUTION_BALANCE.save(deps.storage, &Uint128::zero())?; + BANK_BALANCE.save(deps.storage, &Uint128::zero())?; + + Ok(Response::new()) +} + +//-------------------------------------------------------------------------------------------------- +// Executions +//-------------------------------------------------------------------------------------------------- + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> StdResult { + let api = deps.api; + match msg { + // permissioned - owner + ExecuteMsg::TransferOwnership(new_owner) => { + exec_transfer_ownership(deps, info.sender, api.addr_validate(&new_owner)?) + } + // permissioned - dao + ExecuteMsg::SetShares { shares } => exec_set_shares(deps, info, shares), + // permissionless + ExecuteMsg::Distribute {} => exec_distribute(deps, env), + // permissionless + ExecuteMsg::Grab {} => exec_grab(deps, env), + // permissioned - dao + ExecuteMsg::Payout { amount, recipient } => exec_payout(deps, info, env, amount, recipient), + } +} + +pub fn exec_transfer_ownership( + deps: DepsMut, + sender_addr: Addr, + new_owner_addr: Addr, +) -> StdResult { + let config = CONFIG.load(deps.storage)?; + let old_owner = config.owner; + if sender_addr != old_owner { + return Err(StdError::generic_err("only owner can transfer ownership")); + } + + CONFIG.update(deps.storage, |mut config| -> StdResult<_> { + config.owner = new_owner_addr.clone(); + Ok(config) + })?; + + Ok(Response::new() + .add_attribute("action", "neutron/treasury/transfer_ownership") + .add_attribute("previous_owner", old_owner) + .add_attribute("new_owner", new_owner_addr)) +} + +pub fn exec_grab(deps: DepsMut, env: Env) -> StdResult { + let config: Config = CONFIG.load(deps.storage)?; + let current_time = env.block.time.seconds(); + if current_time - LAST_GRAB_TIME.load(deps.storage)? < config.min_time_elapsed_between_grabs { + return Err(StdError::generic_err("too soon to grab")); + } + Arc::new(LAST_GRAB_TIME).save(deps.storage, ¤t_time)?; + if config.distribution_rate == 0 { + return Err(StdError::generic_err("distribution rate is zero")); + } + let last_balance = LAST_BALANCE.load(deps.storage)?; + let current_balance = deps + .querier + .query_balance(env.contract.address, config.denom)?; + if current_balance.amount.eq(&last_balance) { + return Err(StdError::generic_err("no new funds to grab")); + } + let to_distribute = current_balance.amount.checked_sub(last_balance)?; + let mut to_bank = to_distribute + .checked_mul(config.distribution_rate.into())? + .checked_div(100u128.into())?; + let to_distribution = to_distribute.checked_sub(to_bank)?; + // update bank + let bank_balance = BANK_BALANCE.load(deps.storage)?; + BANK_BALANCE.save(deps.storage, &(bank_balance.checked_add(to_bank)?))?; + // update total received + let total_received = TOTAL_RECEIVED.load(deps.storage)?; + TOTAL_RECEIVED.save(deps.storage, &(total_received.checked_add(to_distribute)?))?; + + // // distribute to shares + let shares = SHARES + .range(deps.storage, None, None, Order::Ascending) + .into_iter() + .collect::>>()?; + let sum_of_shares: Uint128 = shares.iter().fold(Uint128::zero(), |acc, (_, v)| acc + *v); + let mut distributed = Uint128::zero(); + for (addr, share) in shares { + let amount = to_distribution + .checked_mul(share)? + .checked_div(sum_of_shares)?; + let p = PENDING_DISTRIBUTION.load(deps.storage, &addr); + match p { + Ok(p) => { + PENDING_DISTRIBUTION.save(deps.storage, &addr, &(p.checked_add(amount)?))?; + } + Err(_) => { + PENDING_DISTRIBUTION.save(deps.storage, &addr, &amount)?; + } + } + distributed = distributed.checked_add(amount)?; + } + + if distributed != to_distribution { + to_bank = to_bank.checked_add(to_distribution.checked_sub(distributed)?)?; + } + + // update bank + let bank_balance = BANK_BALANCE.load(deps.storage)?; + BANK_BALANCE.save(deps.storage, &(bank_balance.checked_add(to_bank)?))?; + + // update distribution balance + let distribution_balance = DISTRIBUTION_BALANCE.load(deps.storage)?; + DISTRIBUTION_BALANCE.save( + deps.storage, + &(distribution_balance.checked_add(distributed)?), + )?; + + LAST_BALANCE.save(deps.storage, ¤t_balance.amount)?; + + Ok(Response::default() + .add_attribute("action", "neutron/treasury/grab") + .add_attribute("bank_balance", bank_balance) + .add_attribute("distribution_balance", distribution_balance)) +} + +pub fn exec_payout( + deps: DepsMut, + info: MessageInfo, + env: Env, + amount: Uint128, + recipient: String, +) -> StdResult { + let config: Config = CONFIG.load(deps.storage)?; + let denom = config.denom; + if info.sender != config.dao { + return Err(StdError::generic_err("only dao can payout")); + } + let bank_balance = BANK_BALANCE.load(deps.storage)?; + let distribute_balance = DISTRIBUTION_BALANCE.load(deps.storage)?; + if amount > bank_balance { + return Err(StdError::generic_err("insufficient funds")); + } + let current_balance = deps + .querier + .query_balance(env.contract.address, denom.clone())?; + if bank_balance + .checked_add(distribute_balance)? + .gt(¤t_balance.amount) + { + return Err(StdError::generic_err("inconsistent state")); + } + BANK_BALANCE.save(deps.storage, &(bank_balance.checked_sub(amount)?))?; + let total_bank_spent = TOTAL_BANK_SPENT.load(deps.storage)?; + TOTAL_BANK_SPENT.save(deps.storage, &(total_bank_spent.checked_add(amount)?))?; + LAST_BALANCE.save(deps.storage, ¤t_balance.amount.checked_sub(amount)?)?; + + Ok(Response::new() + .add_message(CosmosMsg::Bank(BankMsg::Send { + to_address: recipient.clone(), + amount: vec![Coin { denom, amount }], + })) + .add_attribute("action", "neutron/treasury/payout") + .add_attribute("amount", amount) + .add_attribute("recipient", recipient)) +} + +pub fn exec_distribute(deps: DepsMut, env: Env) -> StdResult { + //get current time + let current_time = env.block.time.seconds(); + let config: Config = CONFIG.load(deps.storage)?; + if LAST_DISTRIBUTION_TIME.load(deps.storage)? - current_time + < config.min_time_elapsed_between_distributions + { + return Err(StdError::generic_err( + "can't distribute yet. min time not elapsed", + )); + } + LAST_DISTRIBUTION_TIME.save(deps.storage, ¤t_time)?; + let distribution_balance = DISTRIBUTION_BALANCE.load(deps.storage)?; + if distribution_balance.is_zero() { + return Err(StdError::generic_err("no funds to distribute")); + } + let denom = config.denom.as_str(); + let bank_balance = BANK_BALANCE.load(deps.storage)?; + let current_balance = deps.querier.query_balance(env.contract.address, denom)?; + if bank_balance + .checked_add(distribution_balance)? + .gt(¤t_balance.amount) + { + return Err(StdError::generic_err("inconsistent state")); + } + let pending_distribution = PENDING_DISTRIBUTION + .range(deps.storage, None, None, cosmwasm_std::Order::Ascending) + .into_iter() + .collect::>>()?; + let mut msgs = vec![]; + let mut spent = Uint128::zero(); + for one in pending_distribution { + let (addr, amount) = one; + spent = spent.checked_add(amount)?; + let msg = CosmosMsg::Bank(BankMsg::Send { + to_address: Addr::from_slice(&addr.clone())?.to_string(), + amount: vec![Coin { + denom: denom.to_string(), + amount, + }], + }); + msgs.push(msg); + PENDING_DISTRIBUTION.remove(deps.storage, &addr); + } + DISTRIBUTION_BALANCE.save(deps.storage, &Uint128::zero())?; + LAST_BALANCE.save(deps.storage, ¤t_balance.amount.checked_sub(spent)?)?; + let total_distributed = TOTAL_DISTRIBUTED.load(deps.storage)?; + TOTAL_DISTRIBUTED.save(deps.storage, &(&total_distributed.checked_add(spent)?))?; + + Ok(Response::new() + .add_messages(msgs) + .add_attribute("action", "neutron/treasury/distribute")) +} + +pub fn exec_set_shares( + deps: DepsMut, + info: MessageInfo, + shares: Vec<(String, Uint128)>, +) -> StdResult { + let config = CONFIG.load(deps.storage)?; + if info.sender != config.dao { + return Err(StdError::generic_err("only dao can set shares")); + } + let mut new_shares = vec![]; + for (addr, share) in shares { + let addr = deps.api.addr_validate(&addr)?; + let addr_raw = addr.as_bytes(); + SHARES.save(deps.storage, addr_raw, &share)?; + new_shares.push((addr_raw.to_vec(), share)); + } + remove_all_shares(deps.storage)?; + for (addr, shares) in new_shares.clone() { + SHARES.save(deps.storage, &addr, &shares)?; + } + Ok(Response::new() + .add_attribute("action", "neutron/treasury/set_shares") + .add_attribute("shares", format!("{:?}", new_shares))) +} + +fn remove_all_shares(storage: &mut dyn Storage) -> StdResult<()> { + let keys = SHARES + .keys(storage, None, None, Order::Ascending) + .into_iter() + .fold(vec![], |mut acc, key| { + acc.push(key.unwrap()); + acc + }); + + for key in keys.iter() { + SHARES.remove(storage, &key); + } + Ok(()) +} + +//-------------------------------------------------------------------------------------------------- +// Queries +//-------------------------------------------------------------------------------------------------- + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Config {} => to_binary(&query_config(deps)?), + QueryMsg::Stats {} => to_binary(&query_stats(deps)?), + QueryMsg::Shares {} => to_binary(&query_shares(deps)?), + } +} + +pub fn query_config(deps: Deps) -> StdResult { + let config = CONFIG.load(deps.storage)?; + Ok(config) +} + +pub fn query_stats(deps: Deps) -> StdResult { + let total_received = TOTAL_RECEIVED.load(deps.storage)?; + let total_bank_spent = TOTAL_BANK_SPENT.load(deps.storage)?; + let total_distributed = TOTAL_DISTRIBUTED.load(deps.storage)?; + let last_balance = LAST_BALANCE.load(deps.storage)?; + let distribution_balance = DISTRIBUTION_BALANCE.load(deps.storage)?; + let bank_balance = BANK_BALANCE.load(deps.storage)?; + + Ok(StatsResponse { + total_received, + total_bank_spent, + total_distributed, + last_balance, + distribution_balance, + bank_balance, + }) +} + +pub fn query_shares(deps: Deps) -> StdResult> { + let shares = SHARES + .range(deps.storage, None, None, cosmwasm_std::Order::Ascending) + .into_iter() + .collect::>>()?; + let mut res: Vec<(String, Uint128)> = vec![]; + for (addr, shares) in shares.iter() { + res.push((Addr::from_slice(addr)?.to_string(), *shares)); + } + Ok(res) +} diff --git a/contracts/distribution/src/lib.rs b/contracts/distribution/src/lib.rs new file mode 100644 index 00000000..4934c19d --- /dev/null +++ b/contracts/distribution/src/lib.rs @@ -0,0 +1,3 @@ +pub mod contract; +pub mod msg; +pub mod state; diff --git a/contracts/distribution/src/msg.rs b/contracts/distribution/src/msg.rs new file mode 100644 index 00000000..394a06e9 --- /dev/null +++ b/contracts/distribution/src/msg.rs @@ -0,0 +1,64 @@ +use cosmwasm_std::{Addr, Uint128}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct InstantiateMsg { + /// The contract's owner + pub owner: String, + pub dao: String, + pub denom: String, + pub distribution_rate: u8, + pub min_time_elapsed_between_distributions: u64, + pub min_time_elapsed_between_grabs: u64, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ExecuteMsg { + /// Transfer the contract's ownership to another account + TransferOwnership(String), + + SetShares { + shares: Vec<(String, Uint128)>, + }, + + /// Distribute funds to the and distribution accounts according to their shares + Distribute(), + + /// Distribute pending funds between Bank and Distribution accounts + Grab(), + + // Payout funds at DAO decision + Payout { + amount: Uint128, + recipient: String, + }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum QueryMsg { + /// The contract's configurations; returns [`ConfigResponse`] + Config {}, + Stats {}, + Shares {}, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct StatsResponse { + pub total_received: Uint128, + pub total_bank_spent: Uint128, + pub total_distributed: Uint128, + pub bank_balance: Uint128, + pub distribution_balance: Uint128, + pub last_balance: Uint128, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct ShareResponse { + address: Addr, + shares: Uint128, +} diff --git a/contracts/distribution/src/state.rs b/contracts/distribution/src/state.rs new file mode 100644 index 00000000..23621005 --- /dev/null +++ b/contracts/distribution/src/state.rs @@ -0,0 +1,29 @@ +use cosmwasm_std::{Addr, Uint128}; +use cw_storage_plus::{Item, Map}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct Config { + pub distribution_rate: u8, + pub min_time_elapsed_between_distributions: u64, + pub min_time_elapsed_between_grabs: u64, + pub denom: String, + pub owner: Addr, + pub dao: Addr, +} + +pub const TOTAL_RECEIVED: Item = Item::new("total_received"); +pub const TOTAL_BANK_SPENT: Item = Item::new("total_bank_spent"); +pub const TOTAL_DISTRIBUTED: Item = Item::new("total_distributed"); + +pub const LAST_DISTRIBUTION_TIME: Item = Item::new("last_distribution_time"); +pub const LAST_GRAB_TIME: Item = Item::new("last_grab_time"); +pub const LAST_BALANCE: Item = Item::new("last_balance"); +pub const DISTRIBUTION_BALANCE: Item = Item::new("distribution_balance"); +pub const PENDING_DISTRIBUTION: Map<&[u8], Uint128> = Map::new("pending_distribution"); +pub const BANK_BALANCE: Item = Item::new("bank_balance"); + +pub const SHARES: Map<&[u8], Uint128> = Map::new("shares"); + +pub const CONFIG: Item = Item::new("config"); diff --git a/contracts/treasury/schema/execute_msg.json b/contracts/treasury/schema/execute_msg.json index 311dbc0c..e1fabffc 100644 --- a/contracts/treasury/schema/execute_msg.json +++ b/contracts/treasury/schema/execute_msg.json @@ -15,63 +15,14 @@ }, "additionalProperties": false }, - { - "type": "object", - "required": [ - "set_shares" - ], - "properties": { - "set_shares": { - "type": "object", - "required": [ - "shares" - ], - "properties": { - "shares": { - "type": "array", - "items": { - "type": "array", - "items": [ - { - "type": "string" - }, - { - "$ref": "#/definitions/Uint128" - } - ], - "maxItems": 2, - "minItems": 2 - } - } - } - } - }, - "additionalProperties": false - }, - { - "description": "Distribute funds to the and distribution accounts according to their shares", - "type": "object", - "required": [ - "distribute" - ], - "properties": { - "distribute": { - "type": "array", - "items": [], - "maxItems": 0, - "minItems": 0 - } - }, - "additionalProperties": false - }, { "description": "Distribute pending funds between Bank and Distribution accounts", "type": "object", "required": [ - "grab" + "collect" ], "properties": { - "grab": { + "collect": { "type": "array", "items": [], "maxItems": 0, diff --git a/contracts/treasury/schema/instantiate_msg.json b/contracts/treasury/schema/instantiate_msg.json index 371bf565..15240e54 100644 --- a/contracts/treasury/schema/instantiate_msg.json +++ b/contracts/treasury/schema/instantiate_msg.json @@ -5,8 +5,9 @@ "required": [ "dao", "denom", + "distribution_contract", "distribution_rate", - "min_time_elapsed_between_fundings", + "min_period", "owner" ], "properties": { @@ -16,12 +17,15 @@ "denom": { "type": "string" }, + "distribution_contract": { + "type": "string" + }, "distribution_rate": { "type": "integer", "format": "uint8", "minimum": 0.0 }, - "min_time_elapsed_between_fundings": { + "min_period": { "type": "integer", "format": "uint64", "minimum": 0.0 diff --git a/contracts/treasury/schema/query_msg.json b/contracts/treasury/schema/query_msg.json index 92e8cc80..967b89ed 100644 --- a/contracts/treasury/schema/query_msg.json +++ b/contracts/treasury/schema/query_msg.json @@ -26,18 +26,6 @@ } }, "additionalProperties": false - }, - { - "type": "object", - "required": [ - "shares" - ], - "properties": { - "shares": { - "type": "object" - } - }, - "additionalProperties": false } ] } diff --git a/contracts/treasury/src/contract.rs b/contracts/treasury/src/contract.rs index 016b3d3d..896cf704 100644 --- a/contracts/treasury/src/contract.rs +++ b/contracts/treasury/src/contract.rs @@ -1,15 +1,14 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - to_binary, Addr, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Order, - Response, StdError, StdResult, Storage, Uint128, + coins, to_binary, Addr, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, Env, MessageInfo, + Response, StdError, StdResult, Uint128, WasmMsg, }; -use cw_storage_plus::KeyDeserialize; -use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg, StatsResponse}; +use crate::msg::{DistributionMsg, ExecuteMsg, InstantiateMsg, QueryMsg, StatsResponse}; use crate::state::{ - Config, BANK_BALANCE, CONFIG, DISTRIBUTION_BALANCE, LAST_BALANCE, PENDING_DISTRIBUTION, SHARES, - TOTAL_BANK_SPENT, TOTAL_DISTRIBUTED, TOTAL_RECEIVED, + Config, BANK_BALANCE, CONFIG, LAST_BALANCE, LAST_GRAB_TIME, TOTAL_BANK_SPENT, + TOTAL_DISTRIBUTED, TOTAL_RECEIVED, }; // const CONTRACT_NAME: &str = "crates.io:neutron-treasury"; @@ -28,7 +27,8 @@ pub fn instantiate( ) -> StdResult { let config = Config { denom: msg.denom, - min_time_elapsed_between_fundings: msg.min_time_elapsed_between_fundings, + min_period: msg.min_period, + distribution_contract: deps.api.addr_validate(msg.distribution_contract.as_str())?, distribution_rate: msg.distribution_rate, owner: deps.api.addr_validate(&msg.owner)?, dao: deps.api.addr_validate(&msg.dao)?, @@ -37,8 +37,8 @@ pub fn instantiate( TOTAL_RECEIVED.save(deps.storage, &Uint128::zero())?; TOTAL_BANK_SPENT.save(deps.storage, &Uint128::zero())?; TOTAL_DISTRIBUTED.save(deps.storage, &Uint128::zero())?; + LAST_GRAB_TIME.save(deps.storage, &0)?; LAST_BALANCE.save(deps.storage, &Uint128::zero())?; - DISTRIBUTION_BALANCE.save(deps.storage, &Uint128::zero())?; BANK_BALANCE.save(deps.storage, &Uint128::zero())?; Ok(Response::new()) @@ -56,12 +56,8 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S ExecuteMsg::TransferOwnership(new_owner) => { exec_transfer_ownership(deps, info.sender, api.addr_validate(&new_owner)?) } - // permissioned - dao - ExecuteMsg::SetShares { shares } => exec_set_shares(deps, info, shares), - // permissionless - ExecuteMsg::Distribute {} => exec_distribute(deps, env), // permissionless - ExecuteMsg::Grab {} => exec_grab(deps, env), + ExecuteMsg::Collect {} => exec_collect(deps, env), // permissioned - dao ExecuteMsg::Payout { amount, recipient } => exec_payout(deps, info, env, amount, recipient), } @@ -89,74 +85,59 @@ pub fn exec_transfer_ownership( .add_attribute("new_owner", new_owner_addr)) } -pub fn exec_grab(deps: DepsMut, env: Env) -> StdResult { - let config = CONFIG.load(deps.storage)?; - if config.distribution_rate == 0 { - return Err(StdError::generic_err("distribution rate is zero")); - } +pub fn exec_collect(deps: DepsMut, env: Env) -> StdResult { + let config: Config = CONFIG.load(deps.storage)?; + let denom = config.denom; + let current_time = env.block.time.seconds(); + if current_time - LAST_GRAB_TIME.load(deps.storage)? < config.min_period { + return Err(StdError::generic_err("too soon to collect")); + } + LAST_GRAB_TIME.save(deps.storage, ¤t_time)?; + // TODO: do we need it? + // if config.distribution_rate == 0 { + // return Err(StdError::generic_err("distribution rate is zero")); + // } let last_balance = LAST_BALANCE.load(deps.storage)?; let current_balance = deps .querier - .query_balance(env.contract.address, config.denom)?; + .query_balance(env.contract.address, denom.clone())?; if current_balance.amount.eq(&last_balance) { return Err(StdError::generic_err("no new funds to grab")); } - let to_distribute = current_balance.amount.checked_sub(last_balance)?; - let mut to_bank = to_distribute + let balance_delta = current_balance.amount.checked_sub(last_balance)?; + let to_distribution = balance_delta .checked_mul(config.distribution_rate.into())? .checked_div(100u128.into())?; - let to_distribution = to_distribute.checked_sub(to_bank)?; - // update bank - let bank_balance = BANK_BALANCE.load(deps.storage)?; - BANK_BALANCE.save(deps.storage, &(bank_balance.checked_add(to_bank)?))?; + let to_bank = balance_delta.checked_sub(to_distribution)?; // update total received let total_received = TOTAL_RECEIVED.load(deps.storage)?; - TOTAL_RECEIVED.save(deps.storage, &(total_received.checked_add(to_distribute)?))?; - - // // distribute to shares - let shares = SHARES - .range(deps.storage, None, None, Order::Ascending) - .into_iter() - .collect::>>()?; - let sum_of_shares: Uint128 = shares.iter().fold(Uint128::zero(), |acc, (_, v)| acc + *v); - let mut distributed = Uint128::zero(); - for (addr, share) in shares { - let amount = to_distribution - .checked_mul(share)? - .checked_div(sum_of_shares)?; - let p = PENDING_DISTRIBUTION.load(deps.storage, &addr); - match p { - Ok(p) => { - PENDING_DISTRIBUTION.save(deps.storage, &addr, &(p.checked_add(amount)?))?; - } - Err(_) => { - PENDING_DISTRIBUTION.save(deps.storage, &addr, &amount)?; - } - } - distributed = distributed.checked_add(amount)?; - } - - if distributed != to_distribution { - to_bank = to_bank.checked_add(to_distribution.checked_sub(distributed)?)?; - } - + TOTAL_RECEIVED.save(deps.storage, &(total_received.checked_add(balance_delta)?))?; // update bank let bank_balance = BANK_BALANCE.load(deps.storage)?; BANK_BALANCE.save(deps.storage, &(bank_balance.checked_add(to_bank)?))?; - - // update distribution balance - let distribution_balance = DISTRIBUTION_BALANCE.load(deps.storage)?; - DISTRIBUTION_BALANCE.save( + let total_distributed = TOTAL_DISTRIBUTED.load(deps.storage)?; + TOTAL_DISTRIBUTED.save( deps.storage, - &(distribution_balance.checked_add(distributed)?), + &(total_distributed.checked_add(to_distribution)?), )?; - LAST_BALANCE.save(deps.storage, ¤t_balance.amount)?; + LAST_BALANCE.save( + deps.storage, + ¤t_balance.amount.checked_sub(to_distribution)?, + )?; + let msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: config.distribution_contract.to_string(), + funds: coins(to_distribution.u128(), denom), + msg: to_binary(&DistributionMsg::Distribute { + period: config.min_period, + })?, + }); Ok(Response::default() + .add_message(msg) .add_attribute("action", "neutron/treasury/grab") .add_attribute("bank_balance", bank_balance) - .add_attribute("distribution_balance", distribution_balance)) + .add_attribute("distributed", to_distribution)) } pub fn exec_payout( @@ -172,14 +153,13 @@ pub fn exec_payout( return Err(StdError::generic_err("only dao can payout")); } let bank_balance = BANK_BALANCE.load(deps.storage)?; - let distribute_balance = DISTRIBUTION_BALANCE.load(deps.storage)?; - if amount > bank_balance { + if amount.gt(&bank_balance) { return Err(StdError::generic_err("insufficient funds")); } let current_balance = deps .querier .query_balance(env.contract.address, denom.clone())?; - if bank_balance.checked_add(distribute_balance)? != current_balance.amount { + if bank_balance.gt(¤t_balance.amount) { return Err(StdError::generic_err("inconsistent state")); } BANK_BALANCE.save(deps.storage, &(bank_balance.checked_sub(amount)?))?; @@ -197,84 +177,6 @@ pub fn exec_payout( .add_attribute("recipient", recipient)) } -pub fn exec_distribute(deps: DepsMut, env: Env) -> StdResult { - let config: Config = CONFIG.load(deps.storage)?; - let denom = config.denom.as_str(); - let distribution_balance = DISTRIBUTION_BALANCE.load(deps.storage)?; - let bank_balance = BANK_BALANCE.load(deps.storage)?; - let current_balance = deps.querier.query_balance(env.contract.address, denom)?; - if bank_balance.checked_add(distribution_balance)? != current_balance.amount { - return Err(StdError::generic_err("inconsistent state")); - } - let pending_distribution = PENDING_DISTRIBUTION - .range(deps.storage, None, None, cosmwasm_std::Order::Ascending) - .into_iter() - .collect::>>()?; - let mut msgs = vec![]; - let mut spent = Uint128::zero(); - for one in pending_distribution { - let (addr, amount) = one; - spent = spent.checked_add(amount)?; - let msg = CosmosMsg::Bank(BankMsg::Send { - to_address: Addr::from_slice(&addr.clone())?.to_string(), - amount: vec![Coin { - denom: denom.to_string(), - amount, - }], - }); - msgs.push(msg); - PENDING_DISTRIBUTION.remove(deps.storage, &addr); - } - - LAST_BALANCE.save(deps.storage, ¤t_balance.amount.checked_sub(spent)?)?; - let total_distributed = TOTAL_DISTRIBUTED.load(deps.storage)?; - TOTAL_DISTRIBUTED.save(deps.storage, &(&total_distributed.checked_add(spent)?))?; - - Ok(Response::new() - .add_messages(msgs) - .add_attribute("action", "neutron/treasury/distribute")) -} - -pub fn exec_set_shares( - deps: DepsMut, - info: MessageInfo, - shares: Vec<(String, Uint128)>, -) -> StdResult { - let config = CONFIG.load(deps.storage)?; - if info.sender != config.dao { - return Err(StdError::generic_err("only dao can set shares")); - } - let mut new_shares = vec![]; - for (addr, share) in shares { - let addr = deps.api.addr_validate(&addr)?; - let addr_raw = addr.as_bytes(); - SHARES.save(deps.storage, addr_raw, &share)?; - new_shares.push((addr_raw.to_vec(), share)); - } - remove_all_shares(deps.storage)?; - for (addr, shares) in new_shares.clone() { - SHARES.save(deps.storage, &addr, &shares)?; - } - Ok(Response::new() - .add_attribute("action", "neutron/treasury/set_shares") - .add_attribute("shares", format!("{:?}", new_shares))) -} - -fn remove_all_shares(storage: &mut dyn Storage) -> StdResult<()> { - let keys = SHARES - .keys(storage, None, None, Order::Ascending) - .into_iter() - .fold(vec![], |mut acc, key| { - acc.push(key.unwrap()); - acc - }); - - for key in keys.iter() { - SHARES.remove(storage, &key); - } - Ok(()) -} - //-------------------------------------------------------------------------------------------------- // Queries //-------------------------------------------------------------------------------------------------- @@ -284,7 +186,6 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { match msg { QueryMsg::Config {} => to_binary(&query_config(deps)?), QueryMsg::Stats {} => to_binary(&query_stats(deps)?), - QueryMsg::Shares {} => to_binary(&query_shares(deps)?), } } @@ -298,7 +199,6 @@ pub fn query_stats(deps: Deps) -> StdResult { let total_bank_spent = TOTAL_BANK_SPENT.load(deps.storage)?; let total_distributed = TOTAL_DISTRIBUTED.load(deps.storage)?; let last_balance = LAST_BALANCE.load(deps.storage)?; - let distribution_balance = DISTRIBUTION_BALANCE.load(deps.storage)?; let bank_balance = BANK_BALANCE.load(deps.storage)?; Ok(StatsResponse { @@ -306,19 +206,6 @@ pub fn query_stats(deps: Deps) -> StdResult { total_bank_spent, total_distributed, last_balance, - distribution_balance, bank_balance, }) } - -pub fn query_shares(deps: Deps) -> StdResult> { - let shares = SHARES - .range(deps.storage, None, None, cosmwasm_std::Order::Ascending) - .into_iter() - .collect::>>()?; - let mut res: Vec<(String, Uint128)> = vec![]; - for (addr, shares) in shares.iter() { - res.push((Addr::from_slice(addr)?.to_string(), *shares)); - } - Ok(res) -} diff --git a/contracts/treasury/src/lib.rs b/contracts/treasury/src/lib.rs index 4934c19d..2a287a05 100644 --- a/contracts/treasury/src/lib.rs +++ b/contracts/treasury/src/lib.rs @@ -1,3 +1,5 @@ pub mod contract; pub mod msg; pub mod state; +#[cfg(test)] +mod testing; diff --git a/contracts/treasury/src/msg.rs b/contracts/treasury/src/msg.rs index ce749ab5..61b35ac7 100644 --- a/contracts/treasury/src/msg.rs +++ b/contracts/treasury/src/msg.rs @@ -9,7 +9,8 @@ pub struct InstantiateMsg { pub dao: String, pub denom: String, pub distribution_rate: u8, - pub min_time_elapsed_between_fundings: u64, + pub min_period: u64, + pub distribution_contract: String, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] @@ -18,15 +19,8 @@ pub enum ExecuteMsg { /// Transfer the contract's ownership to another account TransferOwnership(String), - SetShares { - shares: Vec<(String, Uint128)>, - }, - - /// Distribute funds to the and distribution accounts according to their shares - Distribute(), - /// Distribute pending funds between Bank and Distribution accounts - Grab(), + Collect(), // Payout funds at DAO decision Payout { @@ -41,7 +35,6 @@ pub enum QueryMsg { /// The contract's configurations; returns [`ConfigResponse`] Config {}, Stats {}, - Shares {}, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] @@ -51,7 +44,6 @@ pub struct StatsResponse { pub total_bank_spent: Uint128, pub total_distributed: Uint128, pub bank_balance: Uint128, - pub distribution_balance: Uint128, pub last_balance: Uint128, } @@ -61,3 +53,9 @@ pub struct ShareResponse { address: Addr, shares: Uint128, } + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum DistributionMsg { + Distribute { period: u64 }, +} diff --git a/contracts/treasury/src/state.rs b/contracts/treasury/src/state.rs index f207b19f..c9a78f03 100644 --- a/contracts/treasury/src/state.rs +++ b/contracts/treasury/src/state.rs @@ -1,12 +1,13 @@ use cosmwasm_std::{Addr, Uint128}; -use cw_storage_plus::{Item, Map}; +use cw_storage_plus::Item; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] pub struct Config { pub distribution_rate: u8, - pub min_time_elapsed_between_fundings: u64, + pub distribution_contract: Addr, + pub min_period: u64, pub denom: String, pub owner: Addr, pub dao: Addr, @@ -16,11 +17,8 @@ pub const TOTAL_RECEIVED: Item = Item::new("total_received"); pub const TOTAL_BANK_SPENT: Item = Item::new("total_bank_spent"); pub const TOTAL_DISTRIBUTED: Item = Item::new("total_distributed"); +pub const LAST_GRAB_TIME: Item = Item::new("last_grab_time"); pub const LAST_BALANCE: Item = Item::new("last_balance"); -pub const DISTRIBUTION_BALANCE: Item = Item::new("distribution_balance"); -pub const PENDING_DISTRIBUTION: Map<&[u8], Uint128> = Map::new("pending_distribution"); pub const BANK_BALANCE: Item = Item::new("bank_balance"); -pub const SHARES: Map<&[u8], Uint128> = Map::new("shares"); - pub const CONFIG: Item = Item::new("config"); diff --git a/contracts/treasury/src/testing/mock_querier.rs b/contracts/treasury/src/testing/mock_querier.rs new file mode 100644 index 00000000..df2eaa1f --- /dev/null +++ b/contracts/treasury/src/testing/mock_querier.rs @@ -0,0 +1,22 @@ +use std::marker::PhantomData; + +use cosmwasm_std::{ + testing::{MockApi, MockQuerier, MockStorage}, + Coin, OwnedDeps, +}; + +const MOCK_CONTRACT_ADDR: &str = "cosmos2contract"; + +pub fn mock_dependencies( + contract_balance: &[Coin], +) -> OwnedDeps { + let contract_addr = MOCK_CONTRACT_ADDR; + let custom_querier = MockQuerier::new(&[(contract_addr, contract_balance)]); + + OwnedDeps { + storage: MockStorage::default(), + api: MockApi::default(), + querier: custom_querier, + custom_query_type: PhantomData, + } +} diff --git a/contracts/treasury/src/testing/mod.rs b/contracts/treasury/src/testing/mod.rs new file mode 100644 index 00000000..a1e507b6 --- /dev/null +++ b/contracts/treasury/src/testing/mod.rs @@ -0,0 +1,2 @@ +mod mock_querier; +mod tests; diff --git a/contracts/treasury/src/testing/tests.rs b/contracts/treasury/src/testing/tests.rs new file mode 100644 index 00000000..e4feb621 --- /dev/null +++ b/contracts/treasury/src/testing/tests.rs @@ -0,0 +1,144 @@ +use cosmwasm_std::{ + coin, coins, + testing::{mock_env, mock_info}, + to_binary, BankMsg, Coin, CosmosMsg, DepsMut, Empty, Uint128, WasmMsg, +}; + +use crate::{ + contract::{execute, instantiate}, + msg::{DistributionMsg, ExecuteMsg, InstantiateMsg}, + state::{BANK_BALANCE, CONFIG, LAST_BALANCE, TOTAL_BANK_SPENT, TOTAL_RECEIVED}, + testing::mock_querier::mock_dependencies, +}; + +const DENOM: &str = "denom"; + +pub fn init_base_contract(deps: DepsMut) { + let msg = InstantiateMsg { + denom: DENOM.to_string(), + min_period: 1000, + distribution_contract: "distribution_contract".to_string(), + distribution_rate: 23, + owner: "owner".to_string(), + dao: "dao".to_string(), + }; + let info = mock_info("creator", &coins(2, DENOM)); + instantiate(deps, mock_env(), info, msg).unwrap(); +} + +#[test] +fn test_transfer_ownership() { + let mut deps = mock_dependencies(&[]); + init_base_contract(deps.as_mut()); + let msg = ExecuteMsg::TransferOwnership("new_owner".to_string()); + let res = execute(deps.as_mut(), mock_env(), mock_info("owner", &[]), msg); + assert!(res.is_ok()); + let config = CONFIG.load(deps.as_ref().storage).unwrap(); + assert_eq!(config.owner.to_string(), "new_owner".to_string()); +} + +#[test] +fn test_collect_with_no_money() { + let mut deps = mock_dependencies(&[]); + init_base_contract(deps.as_mut()); + let msg = ExecuteMsg::Collect {}; + let res = execute(deps.as_mut(), mock_env(), mock_info("anyone", &[]), msg); + assert!(res.is_err()); + assert_eq!( + res.unwrap_err().to_string(), + "Generic error: no new funds to grab" + ); +} + +#[test] +fn test_collect_with() { + let mut deps = mock_dependencies(&[coin(1000000, "denom")]); + init_base_contract(deps.as_mut()); + let msg = ExecuteMsg::Collect {}; + let res = execute(deps.as_mut(), mock_env(), mock_info("anyone", &[]), msg); + assert!(res.is_ok()); + let messages = res.unwrap().clone().messages; + assert_eq!(messages.len(), 1); + assert_eq!( + messages[0].msg, + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "distribution_contract".to_string(), + funds: vec![Coin { + denom: "denom".to_string(), + amount: Uint128::from(230000u128) + }], + msg: to_binary(&DistributionMsg::Distribute { period: 1000 }).unwrap(), + }) + ); + let bank_balance = BANK_BALANCE.load(deps.as_ref().storage).unwrap(); + assert_eq!(bank_balance, Uint128::from(770000u128)); + let last_balance = LAST_BALANCE.load(deps.as_ref().storage).unwrap(); + assert_eq!(last_balance, Uint128::from(770000u128)); + let total_received = TOTAL_RECEIVED.load(deps.as_ref().storage).unwrap(); + assert_eq!(total_received, Uint128::from(1000000u128)); +} + +#[test] +fn test_payout_no_money() { + let mut deps = mock_dependencies(&[]); + init_base_contract(deps.as_mut()); + let msg = ExecuteMsg::Payout { + amount: Uint128::from(500000u128), + recipient: "some".to_string(), + }; + let res = execute(deps.as_mut(), mock_env(), mock_info("dao", &[]), msg); + assert!(res.is_err()); + assert_eq!( + res.unwrap_err().to_string(), + "Generic error: insufficient funds" + ); +} + +#[test] +fn test_payout_not_dao() { + let mut deps = mock_dependencies(&[]); + init_base_contract(deps.as_mut()); + let msg = ExecuteMsg::Payout { + amount: Uint128::from(500000u128), + recipient: "some".to_string(), + }; + let res = execute(deps.as_mut(), mock_env(), mock_info("not_dao", &[]), msg); + assert!(res.is_err()); + assert_eq!( + res.unwrap_err().to_string(), + "Generic error: only dao can payout" + ); +} + +#[test] +fn test_payout_success() { + let mut deps = mock_dependencies(&[coin(1000000, "denom")]); + init_base_contract(deps.as_mut()); + BANK_BALANCE + .save(deps.as_mut().storage, &Uint128::from(1000000u128)) + .unwrap(); + let msg = ExecuteMsg::Payout { + amount: Uint128::from(400000u128), + recipient: "some".to_string(), + }; + let res = execute(deps.as_mut(), mock_env(), mock_info("dao", &[]), msg); + assert!(res.is_ok()); + let messages = res.unwrap().clone().messages; + assert_eq!(messages.len(), 1); + assert_eq!( + messages[0].msg, + CosmosMsg::Bank(BankMsg::Send { + to_address: "some".to_string(), + amount: vec![Coin { + denom: "denom".to_string(), + amount: Uint128::from(400000u128) + }], + }) + ); + let bank_balance = BANK_BALANCE.load(deps.as_ref().storage).unwrap(); + assert_eq!(bank_balance, Uint128::from(600000u128)); + let total_payout = TOTAL_BANK_SPENT.load(deps.as_ref().storage).unwrap(); + assert_eq!(total_payout, Uint128::from(400000u128)); + let last_balance = LAST_BALANCE.load(deps.as_ref().storage).unwrap(); + assert_eq!(last_balance, Uint128::from(600000u128)); +} From f3f0a7aaa7d292eddeb3332a0ba630a942aa4855 Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Mon, 5 Dec 2022 19:34:41 +0100 Subject: [PATCH 03/36] feat: treasury + dsc --- .../distribution/schema/execute_msg.json | 34 +- .../distribution/schema/instantiate_msg.json | 18 -- contracts/distribution/schema/query_msg.json | 4 +- contracts/distribution/src/contract.rs | 293 +++++------------- contracts/distribution/src/lib.rs | 2 + contracts/distribution/src/msg.rs | 20 +- contracts/distribution/src/state.rs | 12 - .../distribution/src/testing/mock_querier.rs | 22 ++ contracts/distribution/src/testing/mod.rs | 2 + contracts/distribution/src/testing/tests.rs | 203 ++++++++++++ contracts/treasury/schema/execute_msg.json | 4 +- contracts/treasury/src/contract.rs | 8 +- contracts/treasury/src/msg.rs | 13 +- contracts/treasury/src/testing/tests.rs | 18 +- 14 files changed, 337 insertions(+), 316 deletions(-) create mode 100644 contracts/distribution/src/testing/mock_querier.rs create mode 100644 contracts/distribution/src/testing/mod.rs create mode 100644 contracts/distribution/src/testing/tests.rs diff --git a/contracts/distribution/schema/execute_msg.json b/contracts/distribution/schema/execute_msg.json index 311dbc0c..e8820f52 100644 --- a/contracts/distribution/schema/execute_msg.json +++ b/contracts/distribution/schema/execute_msg.json @@ -52,10 +52,10 @@ "description": "Distribute funds to the and distribution accounts according to their shares", "type": "object", "required": [ - "distribute" + "fund" ], "properties": { - "distribute": { + "fund": { "type": "array", "items": [], "maxItems": 0, @@ -65,13 +65,13 @@ "additionalProperties": false }, { - "description": "Distribute pending funds between Bank and Distribution accounts", + "description": "Claim the funds that have been distributed to the contract's account", "type": "object", "required": [ - "grab" + "claim" ], "properties": { - "grab": { + "claim": { "type": "array", "items": [], "maxItems": 0, @@ -79,30 +79,6 @@ } }, "additionalProperties": false - }, - { - "type": "object", - "required": [ - "payout" - ], - "properties": { - "payout": { - "type": "object", - "required": [ - "amount", - "recipient" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "recipient": { - "type": "string" - } - } - } - }, - "additionalProperties": false } ], "definitions": { diff --git a/contracts/distribution/schema/instantiate_msg.json b/contracts/distribution/schema/instantiate_msg.json index 6c4562f1..e79c3b78 100644 --- a/contracts/distribution/schema/instantiate_msg.json +++ b/contracts/distribution/schema/instantiate_msg.json @@ -5,9 +5,6 @@ "required": [ "dao", "denom", - "distribution_rate", - "min_time_elapsed_between_distributions", - "min_time_elapsed_between_grabs", "owner" ], "properties": { @@ -17,21 +14,6 @@ "denom": { "type": "string" }, - "distribution_rate": { - "type": "integer", - "format": "uint8", - "minimum": 0.0 - }, - "min_time_elapsed_between_distributions": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "min_time_elapsed_between_grabs": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, "owner": { "description": "The contract's owner", "type": "string" diff --git a/contracts/distribution/schema/query_msg.json b/contracts/distribution/schema/query_msg.json index 92e8cc80..ded8cb2f 100644 --- a/contracts/distribution/schema/query_msg.json +++ b/contracts/distribution/schema/query_msg.json @@ -18,10 +18,10 @@ { "type": "object", "required": [ - "stats" + "pending" ], "properties": { - "stats": { + "pending": { "type": "object" } }, diff --git a/contracts/distribution/src/contract.rs b/contracts/distribution/src/contract.rs index 9ef18bbf..55662a66 100644 --- a/contracts/distribution/src/contract.rs +++ b/contracts/distribution/src/contract.rs @@ -1,5 +1,3 @@ -use std::sync::Arc; - #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ @@ -8,12 +6,8 @@ use cosmwasm_std::{ }; use cw_storage_plus::KeyDeserialize; -use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg, StatsResponse}; -use crate::state::{ - Config, BANK_BALANCE, CONFIG, DISTRIBUTION_BALANCE, LAST_BALANCE, LAST_DISTRIBUTION_TIME, - LAST_GRAB_TIME, PENDING_DISTRIBUTION, SHARES, TOTAL_BANK_SPENT, TOTAL_DISTRIBUTED, - TOTAL_RECEIVED, -}; +use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use crate::state::{Config, CONFIG, PENDING_DISTRIBUTION, SHARES}; // const CONTRACT_NAME: &str = "crates.io:neutron-treasury"; // const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -31,20 +25,10 @@ pub fn instantiate( ) -> StdResult { let config = Config { denom: msg.denom, - min_time_elapsed_between_distributions: msg.min_time_elapsed_between_distributions, - min_time_elapsed_between_grabs: msg.min_time_elapsed_between_grabs, - distribution_rate: msg.distribution_rate, owner: deps.api.addr_validate(&msg.owner)?, dao: deps.api.addr_validate(&msg.dao)?, }; CONFIG.save(deps.storage, &config)?; - TOTAL_RECEIVED.save(deps.storage, &Uint128::zero())?; - TOTAL_BANK_SPENT.save(deps.storage, &Uint128::zero())?; - TOTAL_DISTRIBUTED.save(deps.storage, &Uint128::zero())?; - LAST_BALANCE.save(deps.storage, &Uint128::zero())?; - DISTRIBUTION_BALANCE.save(deps.storage, &Uint128::zero())?; - BANK_BALANCE.save(deps.storage, &Uint128::zero())?; - Ok(Response::new()) } @@ -53,21 +37,27 @@ pub fn instantiate( //-------------------------------------------------------------------------------------------------- #[cfg_attr(not(feature = "library"), entry_point)] -pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> StdResult { +pub fn execute( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> StdResult { let api = deps.api; match msg { // permissioned - owner ExecuteMsg::TransferOwnership(new_owner) => { exec_transfer_ownership(deps, info.sender, api.addr_validate(&new_owner)?) } + // permissioned - dao ExecuteMsg::SetShares { shares } => exec_set_shares(deps, info, shares), + // permissionless - ExecuteMsg::Distribute {} => exec_distribute(deps, env), - // permissionless - ExecuteMsg::Grab {} => exec_grab(deps, env), - // permissioned - dao - ExecuteMsg::Payout { amount, recipient } => exec_payout(deps, info, env, amount, recipient), + ExecuteMsg::Fund {} => exec_fund(deps, info), + + // permissioned - owner + ExecuteMsg::Claim {} => exec_claim(deps, info), } } @@ -88,179 +78,46 @@ pub fn exec_transfer_ownership( })?; Ok(Response::new() - .add_attribute("action", "neutron/treasury/transfer_ownership") + .add_attribute("action", "neutron/distribution/transfer_ownership") .add_attribute("previous_owner", old_owner) .add_attribute("new_owner", new_owner_addr)) } -pub fn exec_grab(deps: DepsMut, env: Env) -> StdResult { - let config: Config = CONFIG.load(deps.storage)?; - let current_time = env.block.time.seconds(); - if current_time - LAST_GRAB_TIME.load(deps.storage)? < config.min_time_elapsed_between_grabs { - return Err(StdError::generic_err("too soon to grab")); - } - Arc::new(LAST_GRAB_TIME).save(deps.storage, ¤t_time)?; - if config.distribution_rate == 0 { - return Err(StdError::generic_err("distribution rate is zero")); - } - let last_balance = LAST_BALANCE.load(deps.storage)?; - let current_balance = deps - .querier - .query_balance(env.contract.address, config.denom)?; - if current_balance.amount.eq(&last_balance) { - return Err(StdError::generic_err("no new funds to grab")); - } - let to_distribute = current_balance.amount.checked_sub(last_balance)?; - let mut to_bank = to_distribute - .checked_mul(config.distribution_rate.into())? - .checked_div(100u128.into())?; - let to_distribution = to_distribute.checked_sub(to_bank)?; - // update bank - let bank_balance = BANK_BALANCE.load(deps.storage)?; - BANK_BALANCE.save(deps.storage, &(bank_balance.checked_add(to_bank)?))?; - // update total received - let total_received = TOTAL_RECEIVED.load(deps.storage)?; - TOTAL_RECEIVED.save(deps.storage, &(total_received.checked_add(to_distribute)?))?; - - // // distribute to shares - let shares = SHARES - .range(deps.storage, None, None, Order::Ascending) +fn get_denom_amount(coins: Vec, denom: String) -> Option { + coins .into_iter() - .collect::>>()?; - let sum_of_shares: Uint128 = shares.iter().fold(Uint128::zero(), |acc, (_, v)| acc + *v); - let mut distributed = Uint128::zero(); - for (addr, share) in shares { - let amount = to_distribution - .checked_mul(share)? - .checked_div(sum_of_shares)?; - let p = PENDING_DISTRIBUTION.load(deps.storage, &addr); - match p { - Ok(p) => { - PENDING_DISTRIBUTION.save(deps.storage, &addr, &(p.checked_add(amount)?))?; - } - Err(_) => { - PENDING_DISTRIBUTION.save(deps.storage, &addr, &amount)?; - } - } - distributed = distributed.checked_add(amount)?; - } - - if distributed != to_distribution { - to_bank = to_bank.checked_add(to_distribution.checked_sub(distributed)?)?; - } - - // update bank - let bank_balance = BANK_BALANCE.load(deps.storage)?; - BANK_BALANCE.save(deps.storage, &(bank_balance.checked_add(to_bank)?))?; - - // update distribution balance - let distribution_balance = DISTRIBUTION_BALANCE.load(deps.storage)?; - DISTRIBUTION_BALANCE.save( - deps.storage, - &(distribution_balance.checked_add(distributed)?), - )?; - - LAST_BALANCE.save(deps.storage, ¤t_balance.amount)?; - - Ok(Response::default() - .add_attribute("action", "neutron/treasury/grab") - .add_attribute("bank_balance", bank_balance) - .add_attribute("distribution_balance", distribution_balance)) + .find(|c| c.denom == denom) + .map(|c| c.amount) } -pub fn exec_payout( - deps: DepsMut, - info: MessageInfo, - env: Env, - amount: Uint128, - recipient: String, -) -> StdResult { +pub fn exec_fund(deps: DepsMut, info: MessageInfo) -> StdResult { let config: Config = CONFIG.load(deps.storage)?; let denom = config.denom; - if info.sender != config.dao { - return Err(StdError::generic_err("only dao can payout")); - } - let bank_balance = BANK_BALANCE.load(deps.storage)?; - let distribute_balance = DISTRIBUTION_BALANCE.load(deps.storage)?; - if amount > bank_balance { - return Err(StdError::generic_err("insufficient funds")); - } - let current_balance = deps - .querier - .query_balance(env.contract.address, denom.clone())?; - if bank_balance - .checked_add(distribute_balance)? - .gt(¤t_balance.amount) - { - return Err(StdError::generic_err("inconsistent state")); + let funds = get_denom_amount(info.funds, denom).unwrap_or(Uint128::zero()); + if funds.is_zero() { + return Err(StdError::generic_err("no funds sent")); } - BANK_BALANCE.save(deps.storage, &(bank_balance.checked_sub(amount)?))?; - let total_bank_spent = TOTAL_BANK_SPENT.load(deps.storage)?; - TOTAL_BANK_SPENT.save(deps.storage, &(total_bank_spent.checked_add(amount)?))?; - LAST_BALANCE.save(deps.storage, ¤t_balance.amount.checked_sub(amount)?)?; - - Ok(Response::new() - .add_message(CosmosMsg::Bank(BankMsg::Send { - to_address: recipient.clone(), - amount: vec![Coin { denom, amount }], - })) - .add_attribute("action", "neutron/treasury/payout") - .add_attribute("amount", amount) - .add_attribute("recipient", recipient)) -} - -pub fn exec_distribute(deps: DepsMut, env: Env) -> StdResult { - //get current time - let current_time = env.block.time.seconds(); - let config: Config = CONFIG.load(deps.storage)?; - if LAST_DISTRIBUTION_TIME.load(deps.storage)? - current_time - < config.min_time_elapsed_between_distributions - { - return Err(StdError::generic_err( - "can't distribute yet. min time not elapsed", - )); - } - LAST_DISTRIBUTION_TIME.save(deps.storage, ¤t_time)?; - let distribution_balance = DISTRIBUTION_BALANCE.load(deps.storage)?; - if distribution_balance.is_zero() { - return Err(StdError::generic_err("no funds to distribute")); - } - let denom = config.denom.as_str(); - let bank_balance = BANK_BALANCE.load(deps.storage)?; - let current_balance = deps.querier.query_balance(env.contract.address, denom)?; - if bank_balance - .checked_add(distribution_balance)? - .gt(¤t_balance.amount) - { - return Err(StdError::generic_err("inconsistent state")); - } - let pending_distribution = PENDING_DISTRIBUTION - .range(deps.storage, None, None, cosmwasm_std::Order::Ascending) + let shares = SHARES + .range(deps.storage, None, None, Order::Ascending) .into_iter() .collect::>>()?; - let mut msgs = vec![]; - let mut spent = Uint128::zero(); - for one in pending_distribution { - let (addr, amount) = one; - spent = spent.checked_add(amount)?; - let msg = CosmosMsg::Bank(BankMsg::Send { - to_address: Addr::from_slice(&addr.clone())?.to_string(), - amount: vec![Coin { - denom: denom.to_string(), - amount, - }], - }); - msgs.push(msg); - PENDING_DISTRIBUTION.remove(deps.storage, &addr); + if shares.is_empty() { + return Err(StdError::generic_err("no shares set")); } - DISTRIBUTION_BALANCE.save(deps.storage, &Uint128::zero())?; - LAST_BALANCE.save(deps.storage, ¤t_balance.amount.checked_sub(spent)?)?; - let total_distributed = TOTAL_DISTRIBUTED.load(deps.storage)?; - TOTAL_DISTRIBUTED.save(deps.storage, &(&total_distributed.checked_add(spent)?))?; - - Ok(Response::new() - .add_messages(msgs) - .add_attribute("action", "neutron/treasury/distribute")) + let mut spent = Uint128::zero(); + let total_shares = shares + .clone() + .into_iter() + .fold(Uint128::zero(), |acc, (_, s)| acc + s); + for (addr, share) in shares { + let amount = funds.checked_mul(share)?.checked_div(total_shares)?; + spent += amount; + let pending = PENDING_DISTRIBUTION + .may_load(deps.storage, &addr)? + .unwrap_or(Uint128::zero()); + PENDING_DISTRIBUTION.save(deps.storage, &addr, &(pending.checked_add(amount)?))?; + } + Ok(Response::new().add_attribute("action", "neutron/distribution/fund")) } pub fn exec_set_shares( @@ -288,21 +145,37 @@ pub fn exec_set_shares( .add_attribute("shares", format!("{:?}", new_shares))) } -fn remove_all_shares(storage: &mut dyn Storage) -> StdResult<()> { - let keys = SHARES - .keys(storage, None, None, Order::Ascending) +pub fn remove_all_shares(storage: &mut dyn Storage) -> StdResult<()> { + let shares = SHARES + .range(storage, None, None, Order::Ascending) .into_iter() - .fold(vec![], |mut acc, key| { - acc.push(key.unwrap()); - acc - }); - - for key in keys.iter() { - SHARES.remove(storage, &key); + .collect::>>()?; + for (addr, _) in shares { + SHARES.remove(storage, &addr); } Ok(()) } +pub fn exec_claim(deps: DepsMut, info: MessageInfo) -> StdResult { + let config = CONFIG.load(deps.storage)?; + let denom = config.denom; + let sender = info.sender.as_bytes(); + let pending = PENDING_DISTRIBUTION + .may_load(deps.storage, sender)? + .unwrap_or(Uint128::zero()); + if pending.is_zero() { + return Err(StdError::generic_err("no pending distribution")); + } + PENDING_DISTRIBUTION.remove(deps.storage, sender); + Ok(Response::new().add_message(CosmosMsg::Bank(BankMsg::Send { + to_address: info.sender.to_string(), + amount: vec![Coin { + denom, + amount: pending, + }], + }))) +} + //-------------------------------------------------------------------------------------------------- // Queries //-------------------------------------------------------------------------------------------------- @@ -311,7 +184,7 @@ fn remove_all_shares(storage: &mut dyn Storage) -> StdResult<()> { pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { match msg { QueryMsg::Config {} => to_binary(&query_config(deps)?), - QueryMsg::Stats {} => to_binary(&query_stats(deps)?), + QueryMsg::Pending {} => to_binary(&query_pending(deps)?), QueryMsg::Shares {} => to_binary(&query_shares(deps)?), } } @@ -321,24 +194,6 @@ pub fn query_config(deps: Deps) -> StdResult { Ok(config) } -pub fn query_stats(deps: Deps) -> StdResult { - let total_received = TOTAL_RECEIVED.load(deps.storage)?; - let total_bank_spent = TOTAL_BANK_SPENT.load(deps.storage)?; - let total_distributed = TOTAL_DISTRIBUTED.load(deps.storage)?; - let last_balance = LAST_BALANCE.load(deps.storage)?; - let distribution_balance = DISTRIBUTION_BALANCE.load(deps.storage)?; - let bank_balance = BANK_BALANCE.load(deps.storage)?; - - Ok(StatsResponse { - total_received, - total_bank_spent, - total_distributed, - last_balance, - distribution_balance, - bank_balance, - }) -} - pub fn query_shares(deps: Deps) -> StdResult> { let shares = SHARES .range(deps.storage, None, None, cosmwasm_std::Order::Ascending) @@ -350,3 +205,15 @@ pub fn query_shares(deps: Deps) -> StdResult> { } Ok(res) } + +pub fn query_pending(deps: Deps) -> StdResult> { + let pending = PENDING_DISTRIBUTION + .range(deps.storage, None, None, cosmwasm_std::Order::Ascending) + .into_iter() + .collect::>>()?; + let mut res: Vec<(String, Uint128)> = vec![]; + for (addr, pending) in pending.iter() { + res.push((Addr::from_slice(addr)?.to_string(), *pending)); + } + Ok(res) +} diff --git a/contracts/distribution/src/lib.rs b/contracts/distribution/src/lib.rs index 4934c19d..3b33d5fd 100644 --- a/contracts/distribution/src/lib.rs +++ b/contracts/distribution/src/lib.rs @@ -1,3 +1,5 @@ pub mod contract; pub mod msg; pub mod state; +#[cfg(test)] +pub mod testing; diff --git a/contracts/distribution/src/msg.rs b/contracts/distribution/src/msg.rs index 394a06e9..aca4f775 100644 --- a/contracts/distribution/src/msg.rs +++ b/contracts/distribution/src/msg.rs @@ -8,9 +8,6 @@ pub struct InstantiateMsg { pub owner: String, pub dao: String, pub denom: String, - pub distribution_rate: u8, - pub min_time_elapsed_between_distributions: u64, - pub min_time_elapsed_between_grabs: u64, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] @@ -24,16 +21,10 @@ pub enum ExecuteMsg { }, /// Distribute funds to the and distribution accounts according to their shares - Distribute(), + Fund(), - /// Distribute pending funds between Bank and Distribution accounts - Grab(), - - // Payout funds at DAO decision - Payout { - amount: Uint128, - recipient: String, - }, + /// Claim the funds that have been distributed to the contract's account + Claim(), } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] @@ -41,7 +32,7 @@ pub enum ExecuteMsg { pub enum QueryMsg { /// The contract's configurations; returns [`ConfigResponse`] Config {}, - Stats {}, + Pending {}, Shares {}, } @@ -49,10 +40,7 @@ pub enum QueryMsg { #[serde(rename_all = "snake_case")] pub struct StatsResponse { pub total_received: Uint128, - pub total_bank_spent: Uint128, pub total_distributed: Uint128, - pub bank_balance: Uint128, - pub distribution_balance: Uint128, pub last_balance: Uint128, } diff --git a/contracts/distribution/src/state.rs b/contracts/distribution/src/state.rs index 23621005..b5d19d3f 100644 --- a/contracts/distribution/src/state.rs +++ b/contracts/distribution/src/state.rs @@ -5,24 +5,12 @@ use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] pub struct Config { - pub distribution_rate: u8, - pub min_time_elapsed_between_distributions: u64, - pub min_time_elapsed_between_grabs: u64, pub denom: String, pub owner: Addr, pub dao: Addr, } -pub const TOTAL_RECEIVED: Item = Item::new("total_received"); -pub const TOTAL_BANK_SPENT: Item = Item::new("total_bank_spent"); -pub const TOTAL_DISTRIBUTED: Item = Item::new("total_distributed"); - -pub const LAST_DISTRIBUTION_TIME: Item = Item::new("last_distribution_time"); -pub const LAST_GRAB_TIME: Item = Item::new("last_grab_time"); -pub const LAST_BALANCE: Item = Item::new("last_balance"); -pub const DISTRIBUTION_BALANCE: Item = Item::new("distribution_balance"); pub const PENDING_DISTRIBUTION: Map<&[u8], Uint128> = Map::new("pending_distribution"); -pub const BANK_BALANCE: Item = Item::new("bank_balance"); pub const SHARES: Map<&[u8], Uint128> = Map::new("shares"); diff --git a/contracts/distribution/src/testing/mock_querier.rs b/contracts/distribution/src/testing/mock_querier.rs new file mode 100644 index 00000000..df2eaa1f --- /dev/null +++ b/contracts/distribution/src/testing/mock_querier.rs @@ -0,0 +1,22 @@ +use std::marker::PhantomData; + +use cosmwasm_std::{ + testing::{MockApi, MockQuerier, MockStorage}, + Coin, OwnedDeps, +}; + +const MOCK_CONTRACT_ADDR: &str = "cosmos2contract"; + +pub fn mock_dependencies( + contract_balance: &[Coin], +) -> OwnedDeps { + let contract_addr = MOCK_CONTRACT_ADDR; + let custom_querier = MockQuerier::new(&[(contract_addr, contract_balance)]); + + OwnedDeps { + storage: MockStorage::default(), + api: MockApi::default(), + querier: custom_querier, + custom_query_type: PhantomData, + } +} diff --git a/contracts/distribution/src/testing/mod.rs b/contracts/distribution/src/testing/mod.rs new file mode 100644 index 00000000..a1e507b6 --- /dev/null +++ b/contracts/distribution/src/testing/mod.rs @@ -0,0 +1,2 @@ +mod mock_querier; +mod tests; diff --git a/contracts/distribution/src/testing/tests.rs b/contracts/distribution/src/testing/tests.rs new file mode 100644 index 00000000..fdb1b19e --- /dev/null +++ b/contracts/distribution/src/testing/tests.rs @@ -0,0 +1,203 @@ +use cosmwasm_std::{ + coin, coins, + testing::{mock_env, mock_info}, + DepsMut, Empty, Uint128, +}; + +use crate::{ + contract::{execute, instantiate}, + msg::{ExecuteMsg, InstantiateMsg}, + state::{CONFIG, PENDING_DISTRIBUTION, SHARES}, + testing::mock_querier::mock_dependencies, +}; + +const DENOM: &str = "denom"; + +pub fn init_base_contract(deps: DepsMut) { + let msg = InstantiateMsg { + denom: DENOM.to_string(), + owner: "owner".to_string(), + dao: "dao".to_string(), + }; + let info = mock_info("creator", &coins(2, DENOM)); + instantiate(deps, mock_env(), info, msg).unwrap(); +} + +#[test] +fn test_transfer_ownership() { + let mut deps = mock_dependencies(&[]); + init_base_contract(deps.as_mut()); + let msg = ExecuteMsg::TransferOwnership("new_owner".to_string()); + let res = execute(deps.as_mut(), mock_env(), mock_info("owner", &[]), msg); + assert!(res.is_ok()); + let config = CONFIG.load(deps.as_ref().storage).unwrap(); + assert_eq!(config.owner.to_string(), "new_owner".to_string()); +} + +#[test] +fn test_fund_no_funds() { + let mut deps = mock_dependencies(&[]); + init_base_contract(deps.as_mut()); + let msg = ExecuteMsg::Fund {}; + let res = execute(deps.as_mut(), mock_env(), mock_info("someone", &[]), msg); + assert!(res.is_err()); + assert_eq!(res.unwrap_err().to_string(), "Generic error: no funds sent"); +} + +#[test] +fn test_fund_no_shares() { + let mut deps = mock_dependencies(&[]); + init_base_contract(deps.as_mut()); + let msg = ExecuteMsg::Fund {}; + let res = execute( + deps.as_mut(), + mock_env(), + mock_info("someone", &[coin(10000u128, DENOM)]), + msg, + ); + assert!(res.is_err()); + assert_eq!(res.unwrap_err().to_string(), "Generic error: no shares set"); +} + +#[test] +fn test_fund_success() { + let mut deps = mock_dependencies(&[]); + init_base_contract(deps.as_mut()); + SHARES + .save( + deps.as_mut().storage, + "addr1".as_bytes(), + &Uint128::from(1u128), + ) + .unwrap(); + SHARES + .save( + deps.as_mut().storage, + "addr2".as_bytes(), + &Uint128::from(3u128), + ) + .unwrap(); + let msg = ExecuteMsg::Fund {}; + let res = execute( + deps.as_mut(), + mock_env(), + mock_info("someone", &[coin(10000u128, DENOM)]), + msg, + ); + assert!(res.is_ok()); + assert_eq!( + PENDING_DISTRIBUTION + .load(deps.as_ref().storage, "addr1".as_bytes()) + .unwrap(), + Uint128::from(2500u128) + ); + assert_eq!( + PENDING_DISTRIBUTION + .load(deps.as_ref().storage, "addr2".as_bytes()) + .unwrap(), + Uint128::from(7500u128) + ); +} + +#[test] +fn test_withdraw_no_pending() { + let mut deps = mock_dependencies(&[]); + init_base_contract(deps.as_mut()); + let msg = ExecuteMsg::Claim {}; + let res = execute(deps.as_mut(), mock_env(), mock_info("someone", &[]), msg); + assert!(res.is_err()); + assert_eq!( + res.unwrap_err().to_string(), + "Generic error: no pending distribution" + ); +} + +#[test] +fn test_withdraw_success() { + let mut deps = mock_dependencies(&[]); + init_base_contract(deps.as_mut()); + PENDING_DISTRIBUTION + .save( + deps.as_mut().storage, + "addr1".as_bytes(), + &Uint128::from(1000u128), + ) + .unwrap(); + let msg = ExecuteMsg::Claim {}; + let res = execute(deps.as_mut(), mock_env(), mock_info("addr1", &[]), msg); + assert!(res.is_ok()); + assert_eq!( + PENDING_DISTRIBUTION + .may_load(deps.as_ref().storage, "addr1".as_bytes()) + .unwrap(), + None + ); +} + +#[test] +fn test_set_shares_unauthorized() { + let mut deps = mock_dependencies(&[]); + init_base_contract(deps.as_mut()); + let msg = ExecuteMsg::SetShares { + shares: vec![("addr1".to_string(), Uint128::from(1u128))], + }; + let res = execute(deps.as_mut(), mock_env(), mock_info("someone", &[]), msg); + assert!(res.is_err()); + assert_eq!( + res.unwrap_err().to_string(), + "Generic error: only dao can set shares" + ); +} + +#[test] +fn test_set_shares() { + let mut deps = mock_dependencies(&[]); + init_base_contract(deps.as_mut()); + SHARES + .save( + deps.as_mut().storage, + "addr1".as_bytes(), + &Uint128::from(1u128), + ) + .unwrap(); + SHARES + .save( + deps.as_mut().storage, + "addr2".as_bytes(), + &Uint128::from(3u128), + ) + .unwrap(); + SHARES + .save( + deps.as_mut().storage, + "addr3".as_bytes(), + &Uint128::from(3u128), + ) + .unwrap(); + let msg = ExecuteMsg::SetShares { + shares: vec![ + ("addr1".to_string(), Uint128::from(1u128)), + ("addr2".to_string(), Uint128::from(2u128)), + ], + }; + let res = execute(deps.as_mut(), mock_env(), mock_info("dao", &[]), msg); + assert!(res.is_ok()); + assert_eq!( + SHARES + .load(deps.as_ref().storage, "addr1".as_bytes()) + .unwrap(), + Uint128::from(1u128) + ); + assert_eq!( + SHARES + .load(deps.as_ref().storage, "addr2".as_bytes()) + .unwrap(), + Uint128::from(2u128) + ); + assert_eq!( + SHARES + .may_load(deps.as_ref().storage, "addr3".as_bytes()) + .unwrap(), + None + ); +} diff --git a/contracts/treasury/schema/execute_msg.json b/contracts/treasury/schema/execute_msg.json index e1fabffc..8f3ed744 100644 --- a/contracts/treasury/schema/execute_msg.json +++ b/contracts/treasury/schema/execute_msg.json @@ -19,10 +19,10 @@ "description": "Distribute pending funds between Bank and Distribution accounts", "type": "object", "required": [ - "collect" + "distribute" ], "properties": { - "collect": { + "distribute": { "type": "array", "items": [], "maxItems": 0, diff --git a/contracts/treasury/src/contract.rs b/contracts/treasury/src/contract.rs index 896cf704..3600d16d 100644 --- a/contracts/treasury/src/contract.rs +++ b/contracts/treasury/src/contract.rs @@ -57,7 +57,7 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S exec_transfer_ownership(deps, info.sender, api.addr_validate(&new_owner)?) } // permissionless - ExecuteMsg::Collect {} => exec_collect(deps, env), + ExecuteMsg::Distribute {} => exec_distribute(deps, env), // permissioned - dao ExecuteMsg::Payout { amount, recipient } => exec_payout(deps, info, env, amount, recipient), } @@ -85,7 +85,7 @@ pub fn exec_transfer_ownership( .add_attribute("new_owner", new_owner_addr)) } -pub fn exec_collect(deps: DepsMut, env: Env) -> StdResult { +pub fn exec_distribute(deps: DepsMut, env: Env) -> StdResult { let config: Config = CONFIG.load(deps.storage)?; let denom = config.denom; let current_time = env.block.time.seconds(); @@ -128,9 +128,7 @@ pub fn exec_collect(deps: DepsMut, env: Env) -> StdResult { let msg = CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: config.distribution_contract.to_string(), funds: coins(to_distribution.u128(), denom), - msg: to_binary(&DistributionMsg::Distribute { - period: config.min_period, - })?, + msg: to_binary(&DistributionMsg::Fund {})?, }); Ok(Response::default() diff --git a/contracts/treasury/src/msg.rs b/contracts/treasury/src/msg.rs index 61b35ac7..f679bdc5 100644 --- a/contracts/treasury/src/msg.rs +++ b/contracts/treasury/src/msg.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{Addr, Uint128}; +use cosmwasm_std::Uint128; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -20,7 +20,7 @@ pub enum ExecuteMsg { TransferOwnership(String), /// Distribute pending funds between Bank and Distribution accounts - Collect(), + Distribute(), // Payout funds at DAO decision Payout { @@ -47,15 +47,8 @@ pub struct StatsResponse { pub last_balance: Uint128, } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub struct ShareResponse { - address: Addr, - shares: Uint128, -} - #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum DistributionMsg { - Distribute { period: u64 }, + Fund {}, } diff --git a/contracts/treasury/src/testing/tests.rs b/contracts/treasury/src/testing/tests.rs index e4feb621..71211755 100644 --- a/contracts/treasury/src/testing/tests.rs +++ b/contracts/treasury/src/testing/tests.rs @@ -41,7 +41,7 @@ fn test_transfer_ownership() { fn test_collect_with_no_money() { let mut deps = mock_dependencies(&[]); init_base_contract(deps.as_mut()); - let msg = ExecuteMsg::Collect {}; + let msg = ExecuteMsg::Distribute {}; let res = execute(deps.as_mut(), mock_env(), mock_info("anyone", &[]), msg); assert!(res.is_err()); assert_eq!( @@ -52,22 +52,22 @@ fn test_collect_with_no_money() { #[test] fn test_collect_with() { - let mut deps = mock_dependencies(&[coin(1000000, "denom")]); + let mut deps = mock_dependencies(&[coin(1000000, DENOM)]); init_base_contract(deps.as_mut()); - let msg = ExecuteMsg::Collect {}; + let msg = ExecuteMsg::Distribute {}; let res = execute(deps.as_mut(), mock_env(), mock_info("anyone", &[]), msg); assert!(res.is_ok()); - let messages = res.unwrap().clone().messages; + let messages = res.unwrap().messages; assert_eq!(messages.len(), 1); assert_eq!( messages[0].msg, CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: "distribution_contract".to_string(), funds: vec![Coin { - denom: "denom".to_string(), + denom: DENOM.to_string(), amount: Uint128::from(230000u128) }], - msg: to_binary(&DistributionMsg::Distribute { period: 1000 }).unwrap(), + msg: to_binary(&DistributionMsg::Fund {}).unwrap(), }) ); let bank_balance = BANK_BALANCE.load(deps.as_ref().storage).unwrap(); @@ -112,7 +112,7 @@ fn test_payout_not_dao() { #[test] fn test_payout_success() { - let mut deps = mock_dependencies(&[coin(1000000, "denom")]); + let mut deps = mock_dependencies(&[coin(1000000, DENOM)]); init_base_contract(deps.as_mut()); BANK_BALANCE .save(deps.as_mut().storage, &Uint128::from(1000000u128)) @@ -123,14 +123,14 @@ fn test_payout_success() { }; let res = execute(deps.as_mut(), mock_env(), mock_info("dao", &[]), msg); assert!(res.is_ok()); - let messages = res.unwrap().clone().messages; + let messages = res.unwrap().messages; assert_eq!(messages.len(), 1); assert_eq!( messages[0].msg, CosmosMsg::Bank(BankMsg::Send { to_address: "some".to_string(), amount: vec![Coin { - denom: "denom".to_string(), + denom: DENOM.to_string(), amount: Uint128::from(400000u128) }], }) From 4e1892be474c21bc3a9136f959321fe9d25a75f6 Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Tue, 6 Dec 2022 09:58:01 +0100 Subject: [PATCH 04/36] chore: doc --- contracts/distribution/src/contract.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/distribution/src/contract.rs b/contracts/distribution/src/contract.rs index 55662a66..50d8bd6f 100644 --- a/contracts/distribution/src/contract.rs +++ b/contracts/distribution/src/contract.rs @@ -56,7 +56,7 @@ pub fn execute( // permissionless ExecuteMsg::Fund {} => exec_fund(deps, info), - // permissioned - owner + // permissioned - owner of the share ExecuteMsg::Claim {} => exec_claim(deps, info), } } From bc3c5f7e9e830626769684b6db193ee4c64f581f Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Tue, 6 Dec 2022 09:59:01 +0100 Subject: [PATCH 05/36] chore: trash --- contracts/distribution/src/contract.rs | 3 --- contracts/treasury/src/contract.rs | 3 --- 2 files changed, 6 deletions(-) diff --git a/contracts/distribution/src/contract.rs b/contracts/distribution/src/contract.rs index 50d8bd6f..a7d8bc43 100644 --- a/contracts/distribution/src/contract.rs +++ b/contracts/distribution/src/contract.rs @@ -9,9 +9,6 @@ use cw_storage_plus::KeyDeserialize; use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; use crate::state::{Config, CONFIG, PENDING_DISTRIBUTION, SHARES}; -// const CONTRACT_NAME: &str = "crates.io:neutron-treasury"; -// const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); - //-------------------------------------------------------------------------------------------------- // Instantiation //-------------------------------------------------------------------------------------------------- diff --git a/contracts/treasury/src/contract.rs b/contracts/treasury/src/contract.rs index 3600d16d..bd01ac01 100644 --- a/contracts/treasury/src/contract.rs +++ b/contracts/treasury/src/contract.rs @@ -11,9 +11,6 @@ use crate::state::{ TOTAL_DISTRIBUTED, TOTAL_RECEIVED, }; -// const CONTRACT_NAME: &str = "crates.io:neutron-treasury"; -// const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); - //-------------------------------------------------------------------------------------------------- // Instantiation //-------------------------------------------------------------------------------------------------- From abd69b783011221079993d7379eddaaed641f3f2 Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Tue, 6 Dec 2022 22:46:42 +0100 Subject: [PATCH 06/36] fix: integration tests afterfixes --- .../distribution/schema/execute_msg.json | 30 +++++-- contracts/distribution/src/contract.rs | 15 +++- contracts/distribution/src/msg.rs | 9 ++- contracts/distribution/src/testing/tests.rs | 30 +++++++ contracts/treasury/schema/execute_msg.json | 47 ++++++++++- contracts/treasury/src/contract.rs | 80 +++++++++++++++---- contracts/treasury/src/msg.rs | 11 ++- contracts/treasury/src/testing/tests.rs | 41 +++++++++- 8 files changed, 230 insertions(+), 33 deletions(-) diff --git a/contracts/distribution/schema/execute_msg.json b/contracts/distribution/schema/execute_msg.json index e8820f52..0cafbe29 100644 --- a/contracts/distribution/schema/execute_msg.json +++ b/contracts/distribution/schema/execute_msg.json @@ -56,10 +56,7 @@ ], "properties": { "fund": { - "type": "array", - "items": [], - "maxItems": 0, - "minItems": 0 + "type": "object" } }, "additionalProperties": false @@ -72,10 +69,27 @@ ], "properties": { "claim": { - "type": "array", - "items": [], - "maxItems": 0, - "minItems": 0 + "type": "object" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "update_config" + ], + "properties": { + "update_config": { + "type": "object", + "required": [ + "dao" + ], + "properties": { + "dao": { + "type": "string" + } + } } }, "additionalProperties": false diff --git a/contracts/distribution/src/contract.rs b/contracts/distribution/src/contract.rs index a7d8bc43..04e72b95 100644 --- a/contracts/distribution/src/contract.rs +++ b/contracts/distribution/src/contract.rs @@ -55,9 +55,22 @@ pub fn execute( // permissioned - owner of the share ExecuteMsg::Claim {} => exec_claim(deps, info), + + // permissioned - dao + ExecuteMsg::UpdateConfig { dao } => exec_update_config(deps, info, dao), } } +pub fn exec_update_config(deps: DepsMut, info: MessageInfo, dao: String) -> StdResult { + let mut config: Config = CONFIG.load(deps.storage)?; + if info.sender != config.dao { + return Err(StdError::generic_err("only dao can update config")); + } + config.dao = deps.api.addr_validate(&dao)?; + CONFIG.save(deps.storage, &config)?; + Ok(Response::new().add_attribute("action", "neutron/distribution/update_config")) +} + pub fn exec_transfer_ownership( deps: DepsMut, sender_addr: Addr, @@ -138,7 +151,7 @@ pub fn exec_set_shares( SHARES.save(deps.storage, &addr, &shares)?; } Ok(Response::new() - .add_attribute("action", "neutron/treasury/set_shares") + .add_attribute("action", "neutron/distribution/set_shares") .add_attribute("shares", format!("{:?}", new_shares))) } diff --git a/contracts/distribution/src/msg.rs b/contracts/distribution/src/msg.rs index aca4f775..b2372707 100644 --- a/contracts/distribution/src/msg.rs +++ b/contracts/distribution/src/msg.rs @@ -21,10 +21,15 @@ pub enum ExecuteMsg { }, /// Distribute funds to the and distribution accounts according to their shares - Fund(), + Fund {}, /// Claim the funds that have been distributed to the contract's account - Claim(), + Claim {}, + + //obviously Update config + UpdateConfig { + dao: String, + }, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] diff --git a/contracts/distribution/src/testing/tests.rs b/contracts/distribution/src/testing/tests.rs index fdb1b19e..26cbb622 100644 --- a/contracts/distribution/src/testing/tests.rs +++ b/contracts/distribution/src/testing/tests.rs @@ -201,3 +201,33 @@ fn test_set_shares() { None ); } + +#[test] +fn test_update_config_unauthorized() { + let mut deps = mock_dependencies(&[]); + init_base_contract(deps.as_mut()); + let msg = ExecuteMsg::UpdateConfig { + dao: "new_dao".to_string(), + }; + let res = execute(deps.as_mut(), mock_env(), mock_info("someone", &[]), msg); + assert!(res.is_err()); + assert_eq!( + res.unwrap_err().to_string(), + "Generic error: only dao can update config" + ); +} + +#[test] +fn test_update_config_success() { + let mut deps = mock_dependencies(&[]); + init_base_contract(deps.as_mut()); + let msg = ExecuteMsg::UpdateConfig { + dao: "new_dao".to_string(), + }; + let res = execute(deps.as_mut(), mock_env(), mock_info("dao", &[]), msg); + assert!(res.is_ok()); + assert_eq!( + CONFIG.load(deps.as_ref().storage).unwrap().dao, + "new_dao".to_string() + ); +} diff --git a/contracts/treasury/schema/execute_msg.json b/contracts/treasury/schema/execute_msg.json index 8f3ed744..03a6d6b7 100644 --- a/contracts/treasury/schema/execute_msg.json +++ b/contracts/treasury/schema/execute_msg.json @@ -23,10 +23,7 @@ ], "properties": { "distribute": { - "type": "array", - "items": [], - "maxItems": 0, - "minItems": 0 + "type": "object" } }, "additionalProperties": false @@ -54,6 +51,48 @@ } }, "additionalProperties": false + }, + { + "type": "object", + "required": [ + "update_config" + ], + "properties": { + "update_config": { + "type": "object", + "properties": { + "dao": { + "type": [ + "string", + "null" + ] + }, + "distribution_contract": { + "type": [ + "string", + "null" + ] + }, + "distribution_rate": { + "type": [ + "integer", + "null" + ], + "format": "uint8", + "minimum": 0.0 + }, + "min_period": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + } + } + }, + "additionalProperties": false } ], "definitions": { diff --git a/contracts/treasury/src/contract.rs b/contracts/treasury/src/contract.rs index bd01ac01..77ed2daa 100644 --- a/contracts/treasury/src/contract.rs +++ b/contracts/treasury/src/contract.rs @@ -5,7 +5,7 @@ use cosmwasm_std::{ Response, StdError, StdResult, Uint128, WasmMsg, }; -use crate::msg::{DistributionMsg, ExecuteMsg, InstantiateMsg, QueryMsg, StatsResponse}; +use crate::msg::{DistributeMsg, ExecuteMsg, InstantiateMsg, QueryMsg, StatsResponse}; use crate::state::{ Config, BANK_BALANCE, CONFIG, LAST_BALANCE, LAST_GRAB_TIME, TOTAL_BANK_SPENT, TOTAL_DISTRIBUTED, TOTAL_RECEIVED, @@ -57,6 +57,20 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S ExecuteMsg::Distribute {} => exec_distribute(deps, env), // permissioned - dao ExecuteMsg::Payout { amount, recipient } => exec_payout(deps, info, env, amount, recipient), + // permissioned - dao + ExecuteMsg::UpdateConfig { + distribution_rate, + min_period, + dao, + distribution_contract, + } => exec_update_config( + deps, + info, + distribution_rate, + min_period, + dao, + distribution_contract, + ), } } @@ -82,6 +96,44 @@ pub fn exec_transfer_ownership( .add_attribute("new_owner", new_owner_addr)) } +pub fn exec_update_config( + deps: DepsMut, + info: MessageInfo, + distribution_rate: Option, + min_period: Option, + dao: Option, + distribution_contract: Option, +) -> StdResult { + let mut config: Config = CONFIG.load(deps.storage)?; + if info.sender != config.dao { + return Err(StdError::generic_err("only dao can update config")); + } + + if let Some(min_period) = min_period { + config.min_period = min_period; + } + if let Some(distribution_contract) = distribution_contract { + config.distribution_contract = deps.api.addr_validate(distribution_contract.as_str())?; + } + if let Some(distribution_rate) = distribution_rate { + config.distribution_rate = distribution_rate; + } + if let Some(dao) = dao { + config.dao = deps.api.addr_validate(dao.as_str())?; + } + + CONFIG.save(deps.storage, &config)?; + + Ok(Response::new() + .add_attribute("action", "neutron/treasury/update_config") + .add_attribute("denom", config.denom) + .add_attribute("min_period", config.min_period.to_string()) + .add_attribute("distribution_contract", config.distribution_contract) + .add_attribute("distribution_rate", config.distribution_rate.to_string()) + .add_attribute("owner", config.owner) + .add_attribute("dao", config.dao)) +} + pub fn exec_distribute(deps: DepsMut, env: Env) -> StdResult { let config: Config = CONFIG.load(deps.storage)?; let denom = config.denom; @@ -90,10 +142,6 @@ pub fn exec_distribute(deps: DepsMut, env: Env) -> StdResult { return Err(StdError::generic_err("too soon to collect")); } LAST_GRAB_TIME.save(deps.storage, ¤t_time)?; - // TODO: do we need it? - // if config.distribution_rate == 0 { - // return Err(StdError::generic_err("distribution rate is zero")); - // } let last_balance = LAST_BALANCE.load(deps.storage)?; let current_balance = deps .querier @@ -122,15 +170,19 @@ pub fn exec_distribute(deps: DepsMut, env: Env) -> StdResult { deps.storage, ¤t_balance.amount.checked_sub(to_distribution)?, )?; - let msg = CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: config.distribution_contract.to_string(), - funds: coins(to_distribution.u128(), denom), - msg: to_binary(&DistributionMsg::Fund {})?, - }); - - Ok(Response::default() - .add_message(msg) - .add_attribute("action", "neutron/treasury/grab") + let mut resp = Response::default(); + if !to_distribution.is_zero() { + deps.api.debug("WASMDEBUG: zero"); + let msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: config.distribution_contract.to_string(), + funds: coins(to_distribution.u128(), denom), + msg: to_binary(&DistributeMsg::Fund {})?, + }); + deps.api.debug(format!("WASMDEBUG: {:?}", msg).as_str()); + resp = resp.add_message(msg) + } + Ok(resp + .add_attribute("action", "neutron/treasury/distribute") .add_attribute("bank_balance", bank_balance) .add_attribute("distributed", to_distribution)) } diff --git a/contracts/treasury/src/msg.rs b/contracts/treasury/src/msg.rs index f679bdc5..fe7b9a93 100644 --- a/contracts/treasury/src/msg.rs +++ b/contracts/treasury/src/msg.rs @@ -20,13 +20,20 @@ pub enum ExecuteMsg { TransferOwnership(String), /// Distribute pending funds between Bank and Distribution accounts - Distribute(), + Distribute {}, // Payout funds at DAO decision Payout { amount: Uint128, recipient: String, }, + // //Update config + UpdateConfig { + distribution_rate: Option, + min_period: Option, + dao: Option, + distribution_contract: Option, + }, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] @@ -49,6 +56,6 @@ pub struct StatsResponse { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] -pub enum DistributionMsg { +pub enum DistributeMsg { Fund {}, } diff --git a/contracts/treasury/src/testing/tests.rs b/contracts/treasury/src/testing/tests.rs index 71211755..121832c1 100644 --- a/contracts/treasury/src/testing/tests.rs +++ b/contracts/treasury/src/testing/tests.rs @@ -6,7 +6,7 @@ use cosmwasm_std::{ use crate::{ contract::{execute, instantiate}, - msg::{DistributionMsg, ExecuteMsg, InstantiateMsg}, + msg::{DistributeMsg, ExecuteMsg, InstantiateMsg}, state::{BANK_BALANCE, CONFIG, LAST_BALANCE, TOTAL_BANK_SPENT, TOTAL_RECEIVED}, testing::mock_querier::mock_dependencies, }; @@ -67,7 +67,7 @@ fn test_collect_with() { denom: DENOM.to_string(), amount: Uint128::from(230000u128) }], - msg: to_binary(&DistributionMsg::Fund {}).unwrap(), + msg: to_binary(&DistributeMsg::Fund {}).unwrap(), }) ); let bank_balance = BANK_BALANCE.load(deps.as_ref().storage).unwrap(); @@ -142,3 +142,40 @@ fn test_payout_success() { let last_balance = LAST_BALANCE.load(deps.as_ref().storage).unwrap(); assert_eq!(last_balance, Uint128::from(600000u128)); } + +#[test] +fn test_update_config_unauthorized() { + let mut deps = mock_dependencies(&[]); + init_base_contract(deps.as_mut()); + let msg = ExecuteMsg::UpdateConfig { + distribution_contract: None, + distribution_rate: None, + min_period: None, + dao: Some("dao1".to_string()), + }; + let res = execute(deps.as_mut(), mock_env(), mock_info("not_owner", &[]), msg); + assert!(res.is_err()); + assert_eq!( + res.unwrap_err().to_string(), + "Generic error: only dao can update config" + ); +} + +#[test] +fn test_update_config_success() { + let mut deps = mock_dependencies(&[]); + init_base_contract(deps.as_mut()); + let msg = ExecuteMsg::UpdateConfig { + distribution_contract: Some("new_contract".to_string()), + distribution_rate: Some(11), + min_period: Some(3000), + dao: Some("dao1".to_string()), + }; + let res = execute(deps.as_mut(), mock_env(), mock_info("dao", &[]), msg); + assert!(res.is_ok()); + let config = CONFIG.load(deps.as_ref().storage).unwrap(); + assert_eq!(config.distribution_contract, "new_contract"); + assert_eq!(config.distribution_rate, 11); + assert_eq!(config.min_period, 3000); + assert_eq!(config.dao, "dao1"); +} From bcd1b6c6b9450970ab4317b4a9916e51266b1a59 Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Wed, 7 Dec 2022 11:12:50 +0100 Subject: [PATCH 07/36] fix: remove dao --- .../distribution/schema/execute_msg.json | 20 ---------- .../distribution/schema/instantiate_msg.json | 4 -- contracts/distribution/src/contract.rs | 24 +++--------- contracts/distribution/src/msg.rs | 6 --- contracts/distribution/src/state.rs | 1 - contracts/distribution/src/testing/tests.rs | 38 +------------------ contracts/treasury/schema/execute_msg.json | 6 --- .../treasury/schema/instantiate_msg.json | 5 --- contracts/treasury/src/contract.rs | 24 ++++-------- contracts/treasury/src/msg.rs | 3 -- contracts/treasury/src/state.rs | 1 - contracts/treasury/src/testing/tests.rs | 24 ++++-------- 12 files changed, 22 insertions(+), 134 deletions(-) diff --git a/contracts/distribution/schema/execute_msg.json b/contracts/distribution/schema/execute_msg.json index 0cafbe29..6bae8a28 100644 --- a/contracts/distribution/schema/execute_msg.json +++ b/contracts/distribution/schema/execute_msg.json @@ -73,26 +73,6 @@ } }, "additionalProperties": false - }, - { - "type": "object", - "required": [ - "update_config" - ], - "properties": { - "update_config": { - "type": "object", - "required": [ - "dao" - ], - "properties": { - "dao": { - "type": "string" - } - } - } - }, - "additionalProperties": false } ], "definitions": { diff --git a/contracts/distribution/schema/instantiate_msg.json b/contracts/distribution/schema/instantiate_msg.json index e79c3b78..92d4425c 100644 --- a/contracts/distribution/schema/instantiate_msg.json +++ b/contracts/distribution/schema/instantiate_msg.json @@ -3,14 +3,10 @@ "title": "InstantiateMsg", "type": "object", "required": [ - "dao", "denom", "owner" ], "properties": { - "dao": { - "type": "string" - }, "denom": { "type": "string" }, diff --git a/contracts/distribution/src/contract.rs b/contracts/distribution/src/contract.rs index 04e72b95..b504f7d3 100644 --- a/contracts/distribution/src/contract.rs +++ b/contracts/distribution/src/contract.rs @@ -23,7 +23,6 @@ pub fn instantiate( let config = Config { denom: msg.denom, owner: deps.api.addr_validate(&msg.owner)?, - dao: deps.api.addr_validate(&msg.dao)?, }; CONFIG.save(deps.storage, &config)?; Ok(Response::new()) @@ -47,7 +46,7 @@ pub fn execute( exec_transfer_ownership(deps, info.sender, api.addr_validate(&new_owner)?) } - // permissioned - dao + // permissioned - owner ExecuteMsg::SetShares { shares } => exec_set_shares(deps, info, shares), // permissionless @@ -55,20 +54,7 @@ pub fn execute( // permissioned - owner of the share ExecuteMsg::Claim {} => exec_claim(deps, info), - - // permissioned - dao - ExecuteMsg::UpdateConfig { dao } => exec_update_config(deps, info, dao), - } -} - -pub fn exec_update_config(deps: DepsMut, info: MessageInfo, dao: String) -> StdResult { - let mut config: Config = CONFIG.load(deps.storage)?; - if info.sender != config.dao { - return Err(StdError::generic_err("only dao can update config")); } - config.dao = deps.api.addr_validate(&dao)?; - CONFIG.save(deps.storage, &config)?; - Ok(Response::new().add_attribute("action", "neutron/distribution/update_config")) } pub fn exec_transfer_ownership( @@ -79,7 +65,7 @@ pub fn exec_transfer_ownership( let config = CONFIG.load(deps.storage)?; let old_owner = config.owner; if sender_addr != old_owner { - return Err(StdError::generic_err("only owner can transfer ownership")); + return Err(StdError::generic_err("unauthorized")); } CONFIG.update(deps.storage, |mut config| -> StdResult<_> { @@ -135,9 +121,9 @@ pub fn exec_set_shares( info: MessageInfo, shares: Vec<(String, Uint128)>, ) -> StdResult { - let config = CONFIG.load(deps.storage)?; - if info.sender != config.dao { - return Err(StdError::generic_err("only dao can set shares")); + let config: Config = CONFIG.load(deps.storage)?; + if info.sender != config.owner { + return Err(StdError::generic_err("unauthorized")); } let mut new_shares = vec![]; for (addr, share) in shares { diff --git a/contracts/distribution/src/msg.rs b/contracts/distribution/src/msg.rs index b2372707..a914cddd 100644 --- a/contracts/distribution/src/msg.rs +++ b/contracts/distribution/src/msg.rs @@ -6,7 +6,6 @@ use serde::{Deserialize, Serialize}; pub struct InstantiateMsg { /// The contract's owner pub owner: String, - pub dao: String, pub denom: String, } @@ -25,11 +24,6 @@ pub enum ExecuteMsg { /// Claim the funds that have been distributed to the contract's account Claim {}, - - //obviously Update config - UpdateConfig { - dao: String, - }, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] diff --git a/contracts/distribution/src/state.rs b/contracts/distribution/src/state.rs index b5d19d3f..3450812b 100644 --- a/contracts/distribution/src/state.rs +++ b/contracts/distribution/src/state.rs @@ -7,7 +7,6 @@ use serde::{Deserialize, Serialize}; pub struct Config { pub denom: String, pub owner: Addr, - pub dao: Addr, } pub const PENDING_DISTRIBUTION: Map<&[u8], Uint128> = Map::new("pending_distribution"); diff --git a/contracts/distribution/src/testing/tests.rs b/contracts/distribution/src/testing/tests.rs index 26cbb622..9e6236d7 100644 --- a/contracts/distribution/src/testing/tests.rs +++ b/contracts/distribution/src/testing/tests.rs @@ -17,7 +17,6 @@ pub fn init_base_contract(deps: DepsMut) { let msg = InstantiateMsg { denom: DENOM.to_string(), owner: "owner".to_string(), - dao: "dao".to_string(), }; let info = mock_info("creator", &coins(2, DENOM)); instantiate(deps, mock_env(), info, msg).unwrap(); @@ -143,10 +142,7 @@ fn test_set_shares_unauthorized() { }; let res = execute(deps.as_mut(), mock_env(), mock_info("someone", &[]), msg); assert!(res.is_err()); - assert_eq!( - res.unwrap_err().to_string(), - "Generic error: only dao can set shares" - ); + assert_eq!(res.unwrap_err().to_string(), "Generic error: unauthorized"); } #[test] @@ -180,7 +176,7 @@ fn test_set_shares() { ("addr2".to_string(), Uint128::from(2u128)), ], }; - let res = execute(deps.as_mut(), mock_env(), mock_info("dao", &[]), msg); + let res = execute(deps.as_mut(), mock_env(), mock_info("owner", &[]), msg); assert!(res.is_ok()); assert_eq!( SHARES @@ -201,33 +197,3 @@ fn test_set_shares() { None ); } - -#[test] -fn test_update_config_unauthorized() { - let mut deps = mock_dependencies(&[]); - init_base_contract(deps.as_mut()); - let msg = ExecuteMsg::UpdateConfig { - dao: "new_dao".to_string(), - }; - let res = execute(deps.as_mut(), mock_env(), mock_info("someone", &[]), msg); - assert!(res.is_err()); - assert_eq!( - res.unwrap_err().to_string(), - "Generic error: only dao can update config" - ); -} - -#[test] -fn test_update_config_success() { - let mut deps = mock_dependencies(&[]); - init_base_contract(deps.as_mut()); - let msg = ExecuteMsg::UpdateConfig { - dao: "new_dao".to_string(), - }; - let res = execute(deps.as_mut(), mock_env(), mock_info("dao", &[]), msg); - assert!(res.is_ok()); - assert_eq!( - CONFIG.load(deps.as_ref().storage).unwrap().dao, - "new_dao".to_string() - ); -} diff --git a/contracts/treasury/schema/execute_msg.json b/contracts/treasury/schema/execute_msg.json index 03a6d6b7..f2c25df3 100644 --- a/contracts/treasury/schema/execute_msg.json +++ b/contracts/treasury/schema/execute_msg.json @@ -61,12 +61,6 @@ "update_config": { "type": "object", "properties": { - "dao": { - "type": [ - "string", - "null" - ] - }, "distribution_contract": { "type": [ "string", diff --git a/contracts/treasury/schema/instantiate_msg.json b/contracts/treasury/schema/instantiate_msg.json index 15240e54..28bce8c6 100644 --- a/contracts/treasury/schema/instantiate_msg.json +++ b/contracts/treasury/schema/instantiate_msg.json @@ -3,7 +3,6 @@ "title": "InstantiateMsg", "type": "object", "required": [ - "dao", "denom", "distribution_contract", "distribution_rate", @@ -11,9 +10,6 @@ "owner" ], "properties": { - "dao": { - "type": "string" - }, "denom": { "type": "string" }, @@ -31,7 +27,6 @@ "minimum": 0.0 }, "owner": { - "description": "The contract's owner", "type": "string" } } diff --git a/contracts/treasury/src/contract.rs b/contracts/treasury/src/contract.rs index 77ed2daa..0f11d9f0 100644 --- a/contracts/treasury/src/contract.rs +++ b/contracts/treasury/src/contract.rs @@ -28,7 +28,6 @@ pub fn instantiate( distribution_contract: deps.api.addr_validate(msg.distribution_contract.as_str())?, distribution_rate: msg.distribution_rate, owner: deps.api.addr_validate(&msg.owner)?, - dao: deps.api.addr_validate(&msg.dao)?, }; CONFIG.save(deps.storage, &config)?; TOTAL_RECEIVED.save(deps.storage, &Uint128::zero())?; @@ -55,20 +54,18 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S } // permissionless ExecuteMsg::Distribute {} => exec_distribute(deps, env), - // permissioned - dao + // permissioned - owner ExecuteMsg::Payout { amount, recipient } => exec_payout(deps, info, env, amount, recipient), - // permissioned - dao + // permissioned - owner ExecuteMsg::UpdateConfig { distribution_rate, min_period, - dao, distribution_contract, } => exec_update_config( deps, info, distribution_rate, min_period, - dao, distribution_contract, ), } @@ -82,7 +79,7 @@ pub fn exec_transfer_ownership( let config = CONFIG.load(deps.storage)?; let old_owner = config.owner; if sender_addr != old_owner { - return Err(StdError::generic_err("only owner can transfer ownership")); + return Err(StdError::generic_err("unathorized")); } CONFIG.update(deps.storage, |mut config| -> StdResult<_> { @@ -101,12 +98,11 @@ pub fn exec_update_config( info: MessageInfo, distribution_rate: Option, min_period: Option, - dao: Option, distribution_contract: Option, ) -> StdResult { let mut config: Config = CONFIG.load(deps.storage)?; - if info.sender != config.dao { - return Err(StdError::generic_err("only dao can update config")); + if info.sender != config.owner { + return Err(StdError::generic_err("unauthorized")); } if let Some(min_period) = min_period { @@ -118,9 +114,6 @@ pub fn exec_update_config( if let Some(distribution_rate) = distribution_rate { config.distribution_rate = distribution_rate; } - if let Some(dao) = dao { - config.dao = deps.api.addr_validate(dao.as_str())?; - } CONFIG.save(deps.storage, &config)?; @@ -130,8 +123,7 @@ pub fn exec_update_config( .add_attribute("min_period", config.min_period.to_string()) .add_attribute("distribution_contract", config.distribution_contract) .add_attribute("distribution_rate", config.distribution_rate.to_string()) - .add_attribute("owner", config.owner) - .add_attribute("dao", config.dao)) + .add_attribute("owner", config.owner)) } pub fn exec_distribute(deps: DepsMut, env: Env) -> StdResult { @@ -196,8 +188,8 @@ pub fn exec_payout( ) -> StdResult { let config: Config = CONFIG.load(deps.storage)?; let denom = config.denom; - if info.sender != config.dao { - return Err(StdError::generic_err("only dao can payout")); + if info.sender != config.owner { + return Err(StdError::generic_err("unauthorized")); } let bank_balance = BANK_BALANCE.load(deps.storage)?; if amount.gt(&bank_balance) { diff --git a/contracts/treasury/src/msg.rs b/contracts/treasury/src/msg.rs index fe7b9a93..373decfe 100644 --- a/contracts/treasury/src/msg.rs +++ b/contracts/treasury/src/msg.rs @@ -4,9 +4,7 @@ use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] pub struct InstantiateMsg { - /// The contract's owner pub owner: String, - pub dao: String, pub denom: String, pub distribution_rate: u8, pub min_period: u64, @@ -31,7 +29,6 @@ pub enum ExecuteMsg { UpdateConfig { distribution_rate: Option, min_period: Option, - dao: Option, distribution_contract: Option, }, } diff --git a/contracts/treasury/src/state.rs b/contracts/treasury/src/state.rs index c9a78f03..49944c82 100644 --- a/contracts/treasury/src/state.rs +++ b/contracts/treasury/src/state.rs @@ -10,7 +10,6 @@ pub struct Config { pub min_period: u64, pub denom: String, pub owner: Addr, - pub dao: Addr, } pub const TOTAL_RECEIVED: Item = Item::new("total_received"); diff --git a/contracts/treasury/src/testing/tests.rs b/contracts/treasury/src/testing/tests.rs index 121832c1..b47e8d40 100644 --- a/contracts/treasury/src/testing/tests.rs +++ b/contracts/treasury/src/testing/tests.rs @@ -20,7 +20,6 @@ pub fn init_base_contract(deps: DepsMut) { distribution_contract: "distribution_contract".to_string(), distribution_rate: 23, owner: "owner".to_string(), - dao: "dao".to_string(), }; let info = mock_info("creator", &coins(2, DENOM)); instantiate(deps, mock_env(), info, msg).unwrap(); @@ -86,7 +85,7 @@ fn test_payout_no_money() { amount: Uint128::from(500000u128), recipient: "some".to_string(), }; - let res = execute(deps.as_mut(), mock_env(), mock_info("dao", &[]), msg); + let res = execute(deps.as_mut(), mock_env(), mock_info("owner", &[]), msg); assert!(res.is_err()); assert_eq!( res.unwrap_err().to_string(), @@ -95,19 +94,16 @@ fn test_payout_no_money() { } #[test] -fn test_payout_not_dao() { +fn test_payout_not_owner() { let mut deps = mock_dependencies(&[]); init_base_contract(deps.as_mut()); let msg = ExecuteMsg::Payout { amount: Uint128::from(500000u128), recipient: "some".to_string(), }; - let res = execute(deps.as_mut(), mock_env(), mock_info("not_dao", &[]), msg); + let res = execute(deps.as_mut(), mock_env(), mock_info("not_owner", &[]), msg); assert!(res.is_err()); - assert_eq!( - res.unwrap_err().to_string(), - "Generic error: only dao can payout" - ); + assert_eq!(res.unwrap_err().to_string(), "Generic error: unauthorized"); } #[test] @@ -121,7 +117,7 @@ fn test_payout_success() { amount: Uint128::from(400000u128), recipient: "some".to_string(), }; - let res = execute(deps.as_mut(), mock_env(), mock_info("dao", &[]), msg); + let res = execute(deps.as_mut(), mock_env(), mock_info("owner", &[]), msg); assert!(res.is_ok()); let messages = res.unwrap().messages; assert_eq!(messages.len(), 1); @@ -151,14 +147,10 @@ fn test_update_config_unauthorized() { distribution_contract: None, distribution_rate: None, min_period: None, - dao: Some("dao1".to_string()), }; let res = execute(deps.as_mut(), mock_env(), mock_info("not_owner", &[]), msg); assert!(res.is_err()); - assert_eq!( - res.unwrap_err().to_string(), - "Generic error: only dao can update config" - ); + assert_eq!(res.unwrap_err().to_string(), "Generic error: unauthorized"); } #[test] @@ -169,13 +161,11 @@ fn test_update_config_success() { distribution_contract: Some("new_contract".to_string()), distribution_rate: Some(11), min_period: Some(3000), - dao: Some("dao1".to_string()), }; - let res = execute(deps.as_mut(), mock_env(), mock_info("dao", &[]), msg); + let res = execute(deps.as_mut(), mock_env(), mock_info("owner", &[]), msg); assert!(res.is_ok()); let config = CONFIG.load(deps.as_ref().storage).unwrap(); assert_eq!(config.distribution_contract, "new_contract"); assert_eq!(config.distribution_rate, 11); assert_eq!(config.min_period, 3000); - assert_eq!(config.dao, "dao1"); } From 469edfba8313feec323c4c00d266d12c48db6867 Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Mon, 12 Dec 2022 15:32:25 +0100 Subject: [PATCH 08/36] chore: update versions --- contracts/distribution/Cargo.toml | 2 +- contracts/treasury/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/distribution/Cargo.toml b/contracts/distribution/Cargo.toml index 439d10cd..97211b08 100644 --- a/contracts/distribution/Cargo.toml +++ b/contracts/distribution/Cargo.toml @@ -2,7 +2,7 @@ name = "neutron-distribution" version = "0.1.0" authors = ["Sergey Ratiashvili "] -edition = "2018" +edition = "2021" license = "Apache-2.0" repository = "https://github.com/neutron/neutron-dao" diff --git a/contracts/treasury/Cargo.toml b/contracts/treasury/Cargo.toml index d6a3e41c..3726c790 100644 --- a/contracts/treasury/Cargo.toml +++ b/contracts/treasury/Cargo.toml @@ -2,7 +2,7 @@ name = "neutron-treasury" version = "0.1.0" authors = ["Sergey Ratiashvili "] -edition = "2018" +edition = "2021" license = "Apache-2.0" repository = "https://github.com/neutron/neutron-dao" From bfed2678563b9e985d769519f298b52bed0a5992 Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Mon, 12 Dec 2022 15:33:26 +0100 Subject: [PATCH 09/36] fix: typo --- contracts/treasury/src/contract.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/treasury/src/contract.rs b/contracts/treasury/src/contract.rs index 0f11d9f0..919f97b6 100644 --- a/contracts/treasury/src/contract.rs +++ b/contracts/treasury/src/contract.rs @@ -79,7 +79,7 @@ pub fn exec_transfer_ownership( let config = CONFIG.load(deps.storage)?; let old_owner = config.owner; if sender_addr != old_owner { - return Err(StdError::generic_err("unathorized")); + return Err(StdError::generic_err("unauthorized")); } CONFIG.update(deps.storage, |mut config| -> StdResult<_> { From a3e511983227427fd54a15d82b530f9f730cbe73 Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Mon, 12 Dec 2022 15:34:36 +0100 Subject: [PATCH 10/36] chore: rename fns --- contracts/distribution/src/contract.rs | 16 ++++++++-------- contracts/treasury/src/contract.rs | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/contracts/distribution/src/contract.rs b/contracts/distribution/src/contract.rs index b504f7d3..d9287071 100644 --- a/contracts/distribution/src/contract.rs +++ b/contracts/distribution/src/contract.rs @@ -43,21 +43,21 @@ pub fn execute( match msg { // permissioned - owner ExecuteMsg::TransferOwnership(new_owner) => { - exec_transfer_ownership(deps, info.sender, api.addr_validate(&new_owner)?) + execute_transfer_ownership(deps, info.sender, api.addr_validate(&new_owner)?) } // permissioned - owner - ExecuteMsg::SetShares { shares } => exec_set_shares(deps, info, shares), + ExecuteMsg::SetShares { shares } => execute_set_shares(deps, info, shares), // permissionless - ExecuteMsg::Fund {} => exec_fund(deps, info), + ExecuteMsg::Fund {} => execute_fund(deps, info), // permissioned - owner of the share - ExecuteMsg::Claim {} => exec_claim(deps, info), + ExecuteMsg::Claim {} => execute_claim(deps, info), } } -pub fn exec_transfer_ownership( +pub fn execute_transfer_ownership( deps: DepsMut, sender_addr: Addr, new_owner_addr: Addr, @@ -86,7 +86,7 @@ fn get_denom_amount(coins: Vec, denom: String) -> Option { .map(|c| c.amount) } -pub fn exec_fund(deps: DepsMut, info: MessageInfo) -> StdResult { +pub fn execute_fund(deps: DepsMut, info: MessageInfo) -> StdResult { let config: Config = CONFIG.load(deps.storage)?; let denom = config.denom; let funds = get_denom_amount(info.funds, denom).unwrap_or(Uint128::zero()); @@ -116,7 +116,7 @@ pub fn exec_fund(deps: DepsMut, info: MessageInfo) -> StdResult { Ok(Response::new().add_attribute("action", "neutron/distribution/fund")) } -pub fn exec_set_shares( +pub fn execute_set_shares( deps: DepsMut, info: MessageInfo, shares: Vec<(String, Uint128)>, @@ -152,7 +152,7 @@ pub fn remove_all_shares(storage: &mut dyn Storage) -> StdResult<()> { Ok(()) } -pub fn exec_claim(deps: DepsMut, info: MessageInfo) -> StdResult { +pub fn execute_claim(deps: DepsMut, info: MessageInfo) -> StdResult { let config = CONFIG.load(deps.storage)?; let denom = config.denom; let sender = info.sender.as_bytes(); diff --git a/contracts/treasury/src/contract.rs b/contracts/treasury/src/contract.rs index 919f97b6..2af6d710 100644 --- a/contracts/treasury/src/contract.rs +++ b/contracts/treasury/src/contract.rs @@ -50,18 +50,18 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S match msg { // permissioned - owner ExecuteMsg::TransferOwnership(new_owner) => { - exec_transfer_ownership(deps, info.sender, api.addr_validate(&new_owner)?) + execute_transfer_ownership(deps, info.sender, api.addr_validate(&new_owner)?) } // permissionless - ExecuteMsg::Distribute {} => exec_distribute(deps, env), + ExecuteMsg::Distribute {} => execute_distribute(deps, env), // permissioned - owner - ExecuteMsg::Payout { amount, recipient } => exec_payout(deps, info, env, amount, recipient), + ExecuteMsg::Payout { amount, recipient } => execute_payout(deps, info, env, amount, recipient), // permissioned - owner ExecuteMsg::UpdateConfig { distribution_rate, min_period, distribution_contract, - } => exec_update_config( + } => execute_update_config( deps, info, distribution_rate, @@ -71,7 +71,7 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S } } -pub fn exec_transfer_ownership( +pub fn execute_transfer_ownership( deps: DepsMut, sender_addr: Addr, new_owner_addr: Addr, @@ -93,7 +93,7 @@ pub fn exec_transfer_ownership( .add_attribute("new_owner", new_owner_addr)) } -pub fn exec_update_config( +pub fn execute_update_config( deps: DepsMut, info: MessageInfo, distribution_rate: Option, @@ -126,7 +126,7 @@ pub fn exec_update_config( .add_attribute("owner", config.owner)) } -pub fn exec_distribute(deps: DepsMut, env: Env) -> StdResult { +pub fn execute_distribute(deps: DepsMut, env: Env) -> StdResult { let config: Config = CONFIG.load(deps.storage)?; let denom = config.denom; let current_time = env.block.time.seconds(); @@ -179,7 +179,7 @@ pub fn exec_distribute(deps: DepsMut, env: Env) -> StdResult { .add_attribute("distributed", to_distribution)) } -pub fn exec_payout( +pub fn execute_payout( deps: DepsMut, info: MessageInfo, env: Env, From a2ac095ed487f7a7b28c84af460b17122804c000 Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Mon, 12 Dec 2022 15:35:40 +0100 Subject: [PATCH 11/36] chore: var rename --- contracts/treasury/src/contract.rs | 8 ++++---- contracts/treasury/src/state.rs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/treasury/src/contract.rs b/contracts/treasury/src/contract.rs index 2af6d710..1215e591 100644 --- a/contracts/treasury/src/contract.rs +++ b/contracts/treasury/src/contract.rs @@ -7,7 +7,7 @@ use cosmwasm_std::{ use crate::msg::{DistributeMsg, ExecuteMsg, InstantiateMsg, QueryMsg, StatsResponse}; use crate::state::{ - Config, BANK_BALANCE, CONFIG, LAST_BALANCE, LAST_GRAB_TIME, TOTAL_BANK_SPENT, + Config, BANK_BALANCE, CONFIG, LAST_BALANCE, LAST_DISTRIBUTION_TIME, TOTAL_BANK_SPENT, TOTAL_DISTRIBUTED, TOTAL_RECEIVED, }; @@ -33,7 +33,7 @@ pub fn instantiate( TOTAL_RECEIVED.save(deps.storage, &Uint128::zero())?; TOTAL_BANK_SPENT.save(deps.storage, &Uint128::zero())?; TOTAL_DISTRIBUTED.save(deps.storage, &Uint128::zero())?; - LAST_GRAB_TIME.save(deps.storage, &0)?; + LAST_DISTRIBUTION_TIME.save(deps.storage, &0)?; LAST_BALANCE.save(deps.storage, &Uint128::zero())?; BANK_BALANCE.save(deps.storage, &Uint128::zero())?; @@ -130,10 +130,10 @@ pub fn execute_distribute(deps: DepsMut, env: Env) -> StdResult { let config: Config = CONFIG.load(deps.storage)?; let denom = config.denom; let current_time = env.block.time.seconds(); - if current_time - LAST_GRAB_TIME.load(deps.storage)? < config.min_period { + if current_time - LAST_DISTRIBUTION_TIME.load(deps.storage)? < config.min_period { return Err(StdError::generic_err("too soon to collect")); } - LAST_GRAB_TIME.save(deps.storage, ¤t_time)?; + LAST_DISTRIBUTION_TIME.save(deps.storage, ¤t_time)?; let last_balance = LAST_BALANCE.load(deps.storage)?; let current_balance = deps .querier diff --git a/contracts/treasury/src/state.rs b/contracts/treasury/src/state.rs index 49944c82..cc109f93 100644 --- a/contracts/treasury/src/state.rs +++ b/contracts/treasury/src/state.rs @@ -16,7 +16,7 @@ pub const TOTAL_RECEIVED: Item = Item::new("total_received"); pub const TOTAL_BANK_SPENT: Item = Item::new("total_bank_spent"); pub const TOTAL_DISTRIBUTED: Item = Item::new("total_distributed"); -pub const LAST_GRAB_TIME: Item = Item::new("last_grab_time"); +pub const LAST_DISTRIBUTION_TIME: Item = Item::new("last_grab_time"); pub const LAST_BALANCE: Item = Item::new("last_balance"); pub const BANK_BALANCE: Item = Item::new("bank_balance"); From 56b13ee3ee96c50079a0b9f2f0432d8674b8bb1b Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Mon, 12 Dec 2022 15:36:44 +0100 Subject: [PATCH 12/36] chore: wording --- contracts/treasury/src/contract.rs | 2 +- contracts/treasury/src/testing/tests.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/treasury/src/contract.rs b/contracts/treasury/src/contract.rs index 1215e591..62d759a0 100644 --- a/contracts/treasury/src/contract.rs +++ b/contracts/treasury/src/contract.rs @@ -139,7 +139,7 @@ pub fn execute_distribute(deps: DepsMut, env: Env) -> StdResult { .querier .query_balance(env.contract.address, denom.clone())?; if current_balance.amount.eq(&last_balance) { - return Err(StdError::generic_err("no new funds to grab")); + return Err(StdError::generic_err("no new funds to distribute")); } let balance_delta = current_balance.amount.checked_sub(last_balance)?; let to_distribution = balance_delta diff --git a/contracts/treasury/src/testing/tests.rs b/contracts/treasury/src/testing/tests.rs index b47e8d40..72d03de5 100644 --- a/contracts/treasury/src/testing/tests.rs +++ b/contracts/treasury/src/testing/tests.rs @@ -45,7 +45,7 @@ fn test_collect_with_no_money() { assert!(res.is_err()); assert_eq!( res.unwrap_err().to_string(), - "Generic error: no new funds to grab" + "Generic error: no new funds to distribute" ); } From b91e4652815a1a74e5b645c948fad43571cce389 Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Mon, 12 Dec 2022 15:37:17 +0100 Subject: [PATCH 13/36] chore: -debug --- contracts/treasury/src/contract.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/treasury/src/contract.rs b/contracts/treasury/src/contract.rs index 62d759a0..901fddb6 100644 --- a/contracts/treasury/src/contract.rs +++ b/contracts/treasury/src/contract.rs @@ -164,7 +164,6 @@ pub fn execute_distribute(deps: DepsMut, env: Env) -> StdResult { )?; let mut resp = Response::default(); if !to_distribution.is_zero() { - deps.api.debug("WASMDEBUG: zero"); let msg = CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: config.distribution_contract.to_string(), funds: coins(to_distribution.u128(), denom), From 51ed2c54956b43ea4c3c95a46cad182117a9d136 Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Mon, 12 Dec 2022 15:37:59 +0100 Subject: [PATCH 14/36] chore: rename var --- contracts/treasury/src/contract.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/contracts/treasury/src/contract.rs b/contracts/treasury/src/contract.rs index 901fddb6..e3387794 100644 --- a/contracts/treasury/src/contract.rs +++ b/contracts/treasury/src/contract.rs @@ -142,10 +142,10 @@ pub fn execute_distribute(deps: DepsMut, env: Env) -> StdResult { return Err(StdError::generic_err("no new funds to distribute")); } let balance_delta = current_balance.amount.checked_sub(last_balance)?; - let to_distribution = balance_delta + let to_distribute = balance_delta .checked_mul(config.distribution_rate.into())? .checked_div(100u128.into())?; - let to_bank = balance_delta.checked_sub(to_distribution)?; + let to_bank = balance_delta.checked_sub(to_distribute)?; // update total received let total_received = TOTAL_RECEIVED.load(deps.storage)?; TOTAL_RECEIVED.save(deps.storage, &(total_received.checked_add(balance_delta)?))?; @@ -155,18 +155,18 @@ pub fn execute_distribute(deps: DepsMut, env: Env) -> StdResult { let total_distributed = TOTAL_DISTRIBUTED.load(deps.storage)?; TOTAL_DISTRIBUTED.save( deps.storage, - &(total_distributed.checked_add(to_distribution)?), + &(total_distributed.checked_add(to_distribute)?), )?; LAST_BALANCE.save( deps.storage, - ¤t_balance.amount.checked_sub(to_distribution)?, + ¤t_balance.amount.checked_sub(to_distribute)?, )?; let mut resp = Response::default(); - if !to_distribution.is_zero() { + if !to_distribute.is_zero() { let msg = CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: config.distribution_contract.to_string(), - funds: coins(to_distribution.u128(), denom), + funds: coins(to_distribute.u128(), denom), msg: to_binary(&DistributeMsg::Fund {})?, }); deps.api.debug(format!("WASMDEBUG: {:?}", msg).as_str()); @@ -175,7 +175,7 @@ pub fn execute_distribute(deps: DepsMut, env: Env) -> StdResult { Ok(resp .add_attribute("action", "neutron/treasury/distribute") .add_attribute("bank_balance", bank_balance) - .add_attribute("distributed", to_distribution)) + .add_attribute("distributed", to_distribute)) } pub fn execute_payout( From 1a2908d39e1b5496b766c6feef8010c34b7ace24 Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Mon, 12 Dec 2022 15:38:22 +0100 Subject: [PATCH 15/36] chore: wording --- contracts/treasury/src/contract.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/treasury/src/contract.rs b/contracts/treasury/src/contract.rs index e3387794..e113b754 100644 --- a/contracts/treasury/src/contract.rs +++ b/contracts/treasury/src/contract.rs @@ -131,7 +131,7 @@ pub fn execute_distribute(deps: DepsMut, env: Env) -> StdResult { let denom = config.denom; let current_time = env.block.time.seconds(); if current_time - LAST_DISTRIBUTION_TIME.load(deps.storage)? < config.min_period { - return Err(StdError::generic_err("too soon to collect")); + return Err(StdError::generic_err("too soon to distribute")); } LAST_DISTRIBUTION_TIME.save(deps.storage, ¤t_time)?; let last_balance = LAST_BALANCE.load(deps.storage)?; From e17736e63f1ad8e950e6f92735367248d5242aac Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Mon, 12 Dec 2022 15:40:04 +0100 Subject: [PATCH 16/36] chore: pass info instead of sender --- contracts/treasury/src/contract.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/treasury/src/contract.rs b/contracts/treasury/src/contract.rs index e113b754..77dd4974 100644 --- a/contracts/treasury/src/contract.rs +++ b/contracts/treasury/src/contract.rs @@ -50,7 +50,7 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S match msg { // permissioned - owner ExecuteMsg::TransferOwnership(new_owner) => { - execute_transfer_ownership(deps, info.sender, api.addr_validate(&new_owner)?) + execute_transfer_ownership(deps, info, api.addr_validate(&new_owner)?) } // permissionless ExecuteMsg::Distribute {} => execute_distribute(deps, env), @@ -73,10 +73,11 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S pub fn execute_transfer_ownership( deps: DepsMut, - sender_addr: Addr, + info: MessageInfo, new_owner_addr: Addr, ) -> StdResult { let config = CONFIG.load(deps.storage)?; + let sender_addr = info.sender; let old_owner = config.owner; if sender_addr != old_owner { return Err(StdError::generic_err("unauthorized")); From 9f10170dc3a8a801331092606898dba3db996fdf Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Mon, 12 Dec 2022 15:48:52 +0100 Subject: [PATCH 17/36] fix: no need to clone --- contracts/distribution/src/contract.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/contracts/distribution/src/contract.rs b/contracts/distribution/src/contract.rs index d9287071..4ef35195 100644 --- a/contracts/distribution/src/contract.rs +++ b/contracts/distribution/src/contract.rs @@ -101,9 +101,7 @@ pub fn execute_fund(deps: DepsMut, info: MessageInfo) -> StdResult { return Err(StdError::generic_err("no shares set")); } let mut spent = Uint128::zero(); - let total_shares = shares - .clone() - .into_iter() + let total_shares = shares.iter() .fold(Uint128::zero(), |acc, (_, s)| acc + s); for (addr, share) in shares { let amount = funds.checked_mul(share)?.checked_div(total_shares)?; From 359e3776152c84b785a074fa240df96f0b9bc147 Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Mon, 12 Dec 2022 15:49:34 +0100 Subject: [PATCH 18/36] fix: unused var --- contracts/distribution/src/contract.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/contracts/distribution/src/contract.rs b/contracts/distribution/src/contract.rs index 4ef35195..bccae079 100644 --- a/contracts/distribution/src/contract.rs +++ b/contracts/distribution/src/contract.rs @@ -100,12 +100,10 @@ pub fn execute_fund(deps: DepsMut, info: MessageInfo) -> StdResult { if shares.is_empty() { return Err(StdError::generic_err("no shares set")); } - let mut spent = Uint128::zero(); let total_shares = shares.iter() .fold(Uint128::zero(), |acc, (_, s)| acc + s); for (addr, share) in shares { - let amount = funds.checked_mul(share)?.checked_div(total_shares)?; - spent += amount; + let amount = funds.checked_mul(share)?.checked_div(total_shares)?; let pending = PENDING_DISTRIBUTION .may_load(deps.storage, &addr)? .unwrap_or(Uint128::zero()); From c60eed4ed1b9d1e360c826bcbc71b6c6b49cdedd Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Mon, 12 Dec 2022 15:52:44 +0100 Subject: [PATCH 19/36] fix: -c/p --- contracts/distribution/src/contract.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/distribution/src/contract.rs b/contracts/distribution/src/contract.rs index bccae079..10394434 100644 --- a/contracts/distribution/src/contract.rs +++ b/contracts/distribution/src/contract.rs @@ -125,7 +125,6 @@ pub fn execute_set_shares( for (addr, share) in shares { let addr = deps.api.addr_validate(&addr)?; let addr_raw = addr.as_bytes(); - SHARES.save(deps.storage, addr_raw, &share)?; new_shares.push((addr_raw.to_vec(), share)); } remove_all_shares(deps.storage)?; From d00aba0db73ec56741606cbd62c34825b664760c Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Mon, 12 Dec 2022 15:55:58 +0100 Subject: [PATCH 20/36] fix: no need to get data --- contracts/distribution/src/contract.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/contracts/distribution/src/contract.rs b/contracts/distribution/src/contract.rs index 10394434..867739d8 100644 --- a/contracts/distribution/src/contract.rs +++ b/contracts/distribution/src/contract.rs @@ -138,10 +138,9 @@ pub fn execute_set_shares( pub fn remove_all_shares(storage: &mut dyn Storage) -> StdResult<()> { let shares = SHARES - .range(storage, None, None, Order::Ascending) - .into_iter() + .keys(storage, None, None, Order::Ascending) .collect::>>()?; - for (addr, _) in shares { + for addr in shares { SHARES.remove(storage, &addr); } Ok(()) From d4a5c6754a9d78f73e425adc8e34d31fae391c1e Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Mon, 12 Dec 2022 15:56:48 +0100 Subject: [PATCH 21/36] chore: clone > iter --- contracts/distribution/src/contract.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/distribution/src/contract.rs b/contracts/distribution/src/contract.rs index 867739d8..901a349b 100644 --- a/contracts/distribution/src/contract.rs +++ b/contracts/distribution/src/contract.rs @@ -128,7 +128,7 @@ pub fn execute_set_shares( new_shares.push((addr_raw.to_vec(), share)); } remove_all_shares(deps.storage)?; - for (addr, shares) in new_shares.clone() { + for (addr, shares) in new_shares.iter() { SHARES.save(deps.storage, &addr, &shares)?; } Ok(Response::new() From 759870adf1fd8692b745cf3754598b28b7a0d429 Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Mon, 12 Dec 2022 16:00:27 +0100 Subject: [PATCH 22/36] fix: review fixes --- contracts/distribution/src/contract.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/contracts/distribution/src/contract.rs b/contracts/distribution/src/contract.rs index 901a349b..de295e29 100644 --- a/contracts/distribution/src/contract.rs +++ b/contracts/distribution/src/contract.rs @@ -186,20 +186,18 @@ pub fn query_config(deps: Deps) -> StdResult { pub fn query_shares(deps: Deps) -> StdResult> { let shares = SHARES - .range(deps.storage, None, None, cosmwasm_std::Order::Ascending) - .into_iter() + .range(deps.storage, None, None, Order::Ascending) .collect::>>()?; let mut res: Vec<(String, Uint128)> = vec![]; - for (addr, shares) in shares.iter() { - res.push((Addr::from_slice(addr)?.to_string(), *shares)); + for (addr, shares) in shares { + res.push((Addr::from_slice(&addr)?.to_string(), shares)); } Ok(res) } pub fn query_pending(deps: Deps) -> StdResult> { let pending = PENDING_DISTRIBUTION - .range(deps.storage, None, None, cosmwasm_std::Order::Ascending) - .into_iter() + .range(deps.storage, None, None, Order::Ascending) .collect::>>()?; let mut res: Vec<(String, Uint128)> = vec![]; for (addr, pending) in pending.iter() { From d9954189aa6d5a3f0ab88819284576bdb9cc94d2 Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Mon, 12 Dec 2022 16:01:57 +0100 Subject: [PATCH 23/36] fix: review fixes --- contracts/distribution/src/contract.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/distribution/src/contract.rs b/contracts/distribution/src/contract.rs index de295e29..3ad51f6a 100644 --- a/contracts/distribution/src/contract.rs +++ b/contracts/distribution/src/contract.rs @@ -200,8 +200,8 @@ pub fn query_pending(deps: Deps) -> StdResult> { .range(deps.storage, None, None, Order::Ascending) .collect::>>()?; let mut res: Vec<(String, Uint128)> = vec![]; - for (addr, pending) in pending.iter() { - res.push((Addr::from_slice(addr)?.to_string(), *pending)); + for (addr, pending) in pending { + res.push((Addr::from_slice(&addr)?.to_string(), pending)); } Ok(res) } From 60c415073d60de1474d4b565fe8593e823e2c251 Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Mon, 12 Dec 2022 16:03:32 +0100 Subject: [PATCH 24/36] chore: fix wording --- contracts/distribution/src/msg.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/distribution/src/msg.rs b/contracts/distribution/src/msg.rs index a914cddd..350aec01 100644 --- a/contracts/distribution/src/msg.rs +++ b/contracts/distribution/src/msg.rs @@ -19,7 +19,7 @@ pub enum ExecuteMsg { shares: Vec<(String, Uint128)>, }, - /// Distribute funds to the and distribution accounts according to their shares + /// Distribute funds between share holders Fund {}, /// Claim the funds that have been distributed to the contract's account From 6ecfcb2004db948a9f148fec3582b1344ce94cd9 Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Mon, 12 Dec 2022 16:27:16 +0100 Subject: [PATCH 25/36] fix: add check for messsage --- contracts/distribution/src/testing/tests.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/contracts/distribution/src/testing/tests.rs b/contracts/distribution/src/testing/tests.rs index 9e6236d7..6630541a 100644 --- a/contracts/distribution/src/testing/tests.rs +++ b/contracts/distribution/src/testing/tests.rs @@ -1,7 +1,7 @@ use cosmwasm_std::{ coin, coins, testing::{mock_env, mock_info}, - DepsMut, Empty, Uint128, + DepsMut, Empty, Uint128, BankMsg, CosmosMsg, }; use crate::{ @@ -125,6 +125,16 @@ fn test_withdraw_success() { let msg = ExecuteMsg::Claim {}; let res = execute(deps.as_mut(), mock_env(), mock_info("addr1", &[]), msg); assert!(res.is_ok()); + // check message + let messages = res.unwrap().messages; + assert_eq!(messages.len(), 1); + assert_eq!( + messages[0].msg, + CosmosMsg::Bank(BankMsg::Send { + to_address: "addr1".to_string(), + amount: vec![coin(1000u128, DENOM)], + }) + ); assert_eq!( PENDING_DISTRIBUTION .may_load(deps.as_ref().storage, "addr1".as_bytes()) From bb012c5f856d887f5bfbeaa78eef238f2c2aaf8f Mon Sep 17 00:00:00 2001 From: Murad Karammaev Date: Tue, 13 Dec 2022 07:30:47 +0200 Subject: [PATCH 26/36] chore: fix clippy warnings, run fmt and schema --- contracts/distribution/schema/execute_msg.json | 2 +- contracts/distribution/src/contract.rs | 7 +++---- contracts/distribution/src/testing/tests.rs | 2 +- contracts/treasury/src/contract.rs | 4 +++- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/contracts/distribution/schema/execute_msg.json b/contracts/distribution/schema/execute_msg.json index 6bae8a28..a391eba7 100644 --- a/contracts/distribution/schema/execute_msg.json +++ b/contracts/distribution/schema/execute_msg.json @@ -49,7 +49,7 @@ "additionalProperties": false }, { - "description": "Distribute funds to the and distribution accounts according to their shares", + "description": "Distribute funds between share holders", "type": "object", "required": [ "fund" diff --git a/contracts/distribution/src/contract.rs b/contracts/distribution/src/contract.rs index 3ad51f6a..67566a1b 100644 --- a/contracts/distribution/src/contract.rs +++ b/contracts/distribution/src/contract.rs @@ -100,10 +100,9 @@ pub fn execute_fund(deps: DepsMut, info: MessageInfo) -> StdResult { if shares.is_empty() { return Err(StdError::generic_err("no shares set")); } - let total_shares = shares.iter() - .fold(Uint128::zero(), |acc, (_, s)| acc + s); + let total_shares = shares.iter().fold(Uint128::zero(), |acc, (_, s)| acc + s); for (addr, share) in shares { - let amount = funds.checked_mul(share)?.checked_div(total_shares)?; + let amount = funds.checked_mul(share)?.checked_div(total_shares)?; let pending = PENDING_DISTRIBUTION .may_load(deps.storage, &addr)? .unwrap_or(Uint128::zero()); @@ -129,7 +128,7 @@ pub fn execute_set_shares( } remove_all_shares(deps.storage)?; for (addr, shares) in new_shares.iter() { - SHARES.save(deps.storage, &addr, &shares)?; + SHARES.save(deps.storage, addr, shares)?; } Ok(Response::new() .add_attribute("action", "neutron/distribution/set_shares") diff --git a/contracts/distribution/src/testing/tests.rs b/contracts/distribution/src/testing/tests.rs index 6630541a..6e86919a 100644 --- a/contracts/distribution/src/testing/tests.rs +++ b/contracts/distribution/src/testing/tests.rs @@ -1,7 +1,7 @@ use cosmwasm_std::{ coin, coins, testing::{mock_env, mock_info}, - DepsMut, Empty, Uint128, BankMsg, CosmosMsg, + BankMsg, CosmosMsg, DepsMut, Empty, Uint128, }; use crate::{ diff --git a/contracts/treasury/src/contract.rs b/contracts/treasury/src/contract.rs index 77dd4974..9af059d7 100644 --- a/contracts/treasury/src/contract.rs +++ b/contracts/treasury/src/contract.rs @@ -55,7 +55,9 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S // permissionless ExecuteMsg::Distribute {} => execute_distribute(deps, env), // permissioned - owner - ExecuteMsg::Payout { amount, recipient } => execute_payout(deps, info, env, amount, recipient), + ExecuteMsg::Payout { amount, recipient } => { + execute_payout(deps, info, env, amount, recipient) + } // permissioned - owner ExecuteMsg::UpdateConfig { distribution_rate, From 6d877f18992dc1692e9cff27bc85269dcacb2609 Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Tue, 13 Dec 2022 08:05:13 +0100 Subject: [PATCH 27/36] chore: doc --- contracts/treasury/src/msg.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contracts/treasury/src/msg.rs b/contracts/treasury/src/msg.rs index 373decfe..3f33517f 100644 --- a/contracts/treasury/src/msg.rs +++ b/contracts/treasury/src/msg.rs @@ -6,8 +6,11 @@ use serde::{Deserialize, Serialize}; pub struct InstantiateMsg { pub owner: String, pub denom: String, + /// Distribution rate in percent (0-100) which goes to distribution contract pub distribution_rate: u8, + /// Minimum period between distribution calls pub min_period: u64, + /// Address of distribution contract pub distribution_contract: String, } From efeba0050a4c349c38d0b7224b6f7fe615a3332c Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Tue, 13 Dec 2022 13:44:53 +0100 Subject: [PATCH 28/36] feat: eliminate dust --- contracts/distribution/src/contract.rs | 33 ++++++++++--- contracts/distribution/src/state.rs | 2 + contracts/distribution/src/testing/tests.rs | 47 ++++++++++++++++++- .../treasury/schema/instantiate_msg.json | 3 ++ 4 files changed, 78 insertions(+), 7 deletions(-) diff --git a/contracts/distribution/src/contract.rs b/contracts/distribution/src/contract.rs index 67566a1b..d074d98c 100644 --- a/contracts/distribution/src/contract.rs +++ b/contracts/distribution/src/contract.rs @@ -7,7 +7,7 @@ use cosmwasm_std::{ use cw_storage_plus::KeyDeserialize; use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; -use crate::state::{Config, CONFIG, PENDING_DISTRIBUTION, SHARES}; +use crate::state::{Config, CONFIG, FUND_COUNTER, PENDING_DISTRIBUTION, SHARES}; //-------------------------------------------------------------------------------------------------- // Instantiation @@ -89,6 +89,7 @@ fn get_denom_amount(coins: Vec, denom: String) -> Option { pub fn execute_fund(deps: DepsMut, info: MessageInfo) -> StdResult { let config: Config = CONFIG.load(deps.storage)?; let denom = config.denom; + let fund_counter = FUND_COUNTER.may_load(deps.storage)?.unwrap_or(0); let funds = get_denom_amount(info.funds, denom).unwrap_or(Uint128::zero()); if funds.is_zero() { return Err(StdError::generic_err("no funds sent")); @@ -101,14 +102,34 @@ pub fn execute_fund(deps: DepsMut, info: MessageInfo) -> StdResult { return Err(StdError::generic_err("no shares set")); } let total_shares = shares.iter().fold(Uint128::zero(), |acc, (_, s)| acc + s); - for (addr, share) in shares { - let amount = funds.checked_mul(share)?.checked_div(total_shares)?; + let mut spent = Uint128::zero(); + let mut resp = Response::new().add_attribute("action", "neutron/distribution/fund"); + + for (addr, share) in shares.iter() { + let amount = funds.checked_mul(*share)?.checked_div(total_shares)?; + let pending = PENDING_DISTRIBUTION + .may_load(deps.storage, addr)? + .unwrap_or(Uint128::zero()); + PENDING_DISTRIBUTION.save(deps.storage, addr, &(pending.checked_add(amount)?))?; + spent = spent.checked_add(amount)?; + resp = resp + .add_attribute("address", Addr::from_slice(addr)?) + .add_attribute("amount", amount); + } + let remaining = funds.checked_sub(spent)?; + if !remaining.is_zero() { + let index = fund_counter % shares.len() as u64; + let key = &shares.get(index as usize).unwrap().0; let pending = PENDING_DISTRIBUTION - .may_load(deps.storage, &addr)? + .may_load(deps.storage, key)? .unwrap_or(Uint128::zero()); - PENDING_DISTRIBUTION.save(deps.storage, &addr, &(pending.checked_add(amount)?))?; + PENDING_DISTRIBUTION.save(deps.storage, key, &(pending.checked_add(remaining)?))?; + resp = resp + .add_attribute("remainder_address", Addr::from_slice(key)?) + .add_attribute("remainder_amount", remaining); } - Ok(Response::new().add_attribute("action", "neutron/distribution/fund")) + FUND_COUNTER.save(deps.storage, &(fund_counter + 1))?; + Ok(resp) } pub fn execute_set_shares( diff --git a/contracts/distribution/src/state.rs b/contracts/distribution/src/state.rs index 3450812b..cbce5d21 100644 --- a/contracts/distribution/src/state.rs +++ b/contracts/distribution/src/state.rs @@ -14,3 +14,5 @@ pub const PENDING_DISTRIBUTION: Map<&[u8], Uint128> = Map::new("pending_distribu pub const SHARES: Map<&[u8], Uint128> = Map::new("shares"); pub const CONFIG: Item = Item::new("config"); + +pub const FUND_COUNTER: Item = Item::new("fund_counter"); diff --git a/contracts/distribution/src/testing/tests.rs b/contracts/distribution/src/testing/tests.rs index 6e86919a..2cd91c52 100644 --- a/contracts/distribution/src/testing/tests.rs +++ b/contracts/distribution/src/testing/tests.rs @@ -7,7 +7,7 @@ use cosmwasm_std::{ use crate::{ contract::{execute, instantiate}, msg::{ExecuteMsg, InstantiateMsg}, - state::{CONFIG, PENDING_DISTRIBUTION, SHARES}, + state::{CONFIG, FUND_COUNTER, PENDING_DISTRIBUTION, SHARES}, testing::mock_querier::mock_dependencies, }; @@ -96,6 +96,51 @@ fn test_fund_success() { .unwrap(), Uint128::from(7500u128) ); + let fund_counter = FUND_COUNTER.load(deps.as_ref().storage).unwrap(); + assert_eq!(fund_counter, 1u64); +} + +#[test] +fn test_fund_success_with_dust() { + let mut deps = mock_dependencies(&[]); + init_base_contract(deps.as_mut()); + SHARES + .save( + deps.as_mut().storage, + "addr1".as_bytes(), + &Uint128::from(1u128), + ) + .unwrap(); + SHARES + .save( + deps.as_mut().storage, + "addr2".as_bytes(), + &Uint128::from(3u128), + ) + .unwrap(); + let msg = ExecuteMsg::Fund {}; + let res = execute( + deps.as_mut(), + mock_env(), + mock_info("someone", &[coin(10001u128, DENOM)]), + msg, + ); + assert!(res.is_ok()); + println!("{:?}", res.unwrap().attributes); + assert_eq!( + PENDING_DISTRIBUTION + .load(deps.as_ref().storage, "addr1".as_bytes()) + .unwrap(), + Uint128::from(2501u128) + ); + assert_eq!( + PENDING_DISTRIBUTION + .load(deps.as_ref().storage, "addr2".as_bytes()) + .unwrap(), + Uint128::from(7500u128) + ); + let fund_counter = FUND_COUNTER.load(deps.as_ref().storage).unwrap(); + assert_eq!(fund_counter, 1u64); } #[test] diff --git a/contracts/treasury/schema/instantiate_msg.json b/contracts/treasury/schema/instantiate_msg.json index 28bce8c6..a073a83a 100644 --- a/contracts/treasury/schema/instantiate_msg.json +++ b/contracts/treasury/schema/instantiate_msg.json @@ -14,14 +14,17 @@ "type": "string" }, "distribution_contract": { + "description": "Address of distribution contract", "type": "string" }, "distribution_rate": { + "description": "Distribution rate in percent (0-100) which goes to distribution contract", "type": "integer", "format": "uint8", "minimum": 0.0 }, "min_period": { + "description": "Minimum period between distribution calls", "type": "integer", "format": "uint64", "minimum": 0.0 From 0d798bb2d91b9565472a28522429b4bbf63e5d6e Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Tue, 13 Dec 2022 14:08:18 +0100 Subject: [PATCH 29/36] fix: change sender to messageInfo --- contracts/distribution/src/contract.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/distribution/src/contract.rs b/contracts/distribution/src/contract.rs index d074d98c..b12d3926 100644 --- a/contracts/distribution/src/contract.rs +++ b/contracts/distribution/src/contract.rs @@ -43,7 +43,7 @@ pub fn execute( match msg { // permissioned - owner ExecuteMsg::TransferOwnership(new_owner) => { - execute_transfer_ownership(deps, info.sender, api.addr_validate(&new_owner)?) + execute_transfer_ownership(deps, info, api.addr_validate(&new_owner)?) } // permissioned - owner @@ -59,11 +59,12 @@ pub fn execute( pub fn execute_transfer_ownership( deps: DepsMut, - sender_addr: Addr, + info: MessageInfo, new_owner_addr: Addr, ) -> StdResult { let config = CONFIG.load(deps.storage)?; let old_owner = config.owner; + let sender_addr = info.sender; if sender_addr != old_owner { return Err(StdError::generic_err("unauthorized")); } From efcc461dcfea5518d21292b255899ecc365d1637 Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Tue, 13 Dec 2022 15:44:45 +0100 Subject: [PATCH 30/36] feat: &[u8] > Addr --- contracts/distribution/src/contract.rs | 36 +++++++++++---------- contracts/distribution/src/state.rs | 4 +-- contracts/distribution/src/testing/tests.rs | 34 +++++++++---------- 3 files changed, 38 insertions(+), 36 deletions(-) diff --git a/contracts/distribution/src/contract.rs b/contracts/distribution/src/contract.rs index b12d3926..93ef55df 100644 --- a/contracts/distribution/src/contract.rs +++ b/contracts/distribution/src/contract.rs @@ -4,7 +4,6 @@ use cosmwasm_std::{ to_binary, Addr, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Order, Response, StdError, StdResult, Storage, Uint128, }; -use cw_storage_plus::KeyDeserialize; use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; use crate::state::{Config, CONFIG, FUND_COUNTER, PENDING_DISTRIBUTION, SHARES}; @@ -109,12 +108,12 @@ pub fn execute_fund(deps: DepsMut, info: MessageInfo) -> StdResult { for (addr, share) in shares.iter() { let amount = funds.checked_mul(*share)?.checked_div(total_shares)?; let pending = PENDING_DISTRIBUTION - .may_load(deps.storage, addr)? + .may_load(deps.storage, addr.clone())? .unwrap_or(Uint128::zero()); - PENDING_DISTRIBUTION.save(deps.storage, addr, &(pending.checked_add(amount)?))?; + PENDING_DISTRIBUTION.save(deps.storage, addr.clone(), &(pending.checked_add(amount)?))?; spent = spent.checked_add(amount)?; resp = resp - .add_attribute("address", Addr::from_slice(addr)?) + .add_attribute("address", addr) .add_attribute("amount", amount); } let remaining = funds.checked_sub(spent)?; @@ -122,11 +121,15 @@ pub fn execute_fund(deps: DepsMut, info: MessageInfo) -> StdResult { let index = fund_counter % shares.len() as u64; let key = &shares.get(index as usize).unwrap().0; let pending = PENDING_DISTRIBUTION - .may_load(deps.storage, key)? + .may_load(deps.storage, key.clone())? .unwrap_or(Uint128::zero()); - PENDING_DISTRIBUTION.save(deps.storage, key, &(pending.checked_add(remaining)?))?; + PENDING_DISTRIBUTION.save( + deps.storage, + key.clone(), + &(pending.checked_add(remaining)?), + )?; resp = resp - .add_attribute("remainder_address", Addr::from_slice(key)?) + .add_attribute("remainder_address", key) .add_attribute("remainder_amount", remaining); } FUND_COUNTER.save(deps.storage, &(fund_counter + 1))?; @@ -145,12 +148,11 @@ pub fn execute_set_shares( let mut new_shares = vec![]; for (addr, share) in shares { let addr = deps.api.addr_validate(&addr)?; - let addr_raw = addr.as_bytes(); - new_shares.push((addr_raw.to_vec(), share)); + new_shares.push((addr, share)); } remove_all_shares(deps.storage)?; for (addr, shares) in new_shares.iter() { - SHARES.save(deps.storage, addr, shares)?; + SHARES.save(deps.storage, addr.clone(), shares)?; } Ok(Response::new() .add_attribute("action", "neutron/distribution/set_shares") @@ -162,7 +164,7 @@ pub fn remove_all_shares(storage: &mut dyn Storage) -> StdResult<()> { .keys(storage, None, None, Order::Ascending) .collect::>>()?; for addr in shares { - SHARES.remove(storage, &addr); + SHARES.remove(storage, addr); } Ok(()) } @@ -170,16 +172,16 @@ pub fn remove_all_shares(storage: &mut dyn Storage) -> StdResult<()> { pub fn execute_claim(deps: DepsMut, info: MessageInfo) -> StdResult { let config = CONFIG.load(deps.storage)?; let denom = config.denom; - let sender = info.sender.as_bytes(); + let sender = info.sender; let pending = PENDING_DISTRIBUTION - .may_load(deps.storage, sender)? + .may_load(deps.storage, sender.clone())? .unwrap_or(Uint128::zero()); if pending.is_zero() { return Err(StdError::generic_err("no pending distribution")); } - PENDING_DISTRIBUTION.remove(deps.storage, sender); + PENDING_DISTRIBUTION.remove(deps.storage, sender.clone()); Ok(Response::new().add_message(CosmosMsg::Bank(BankMsg::Send { - to_address: info.sender.to_string(), + to_address: sender.to_string(), amount: vec![Coin { denom, amount: pending, @@ -211,7 +213,7 @@ pub fn query_shares(deps: Deps) -> StdResult> { .collect::>>()?; let mut res: Vec<(String, Uint128)> = vec![]; for (addr, shares) in shares { - res.push((Addr::from_slice(&addr)?.to_string(), shares)); + res.push((addr.to_string(), shares)); } Ok(res) } @@ -222,7 +224,7 @@ pub fn query_pending(deps: Deps) -> StdResult> { .collect::>>()?; let mut res: Vec<(String, Uint128)> = vec![]; for (addr, pending) in pending { - res.push((Addr::from_slice(&addr)?.to_string(), pending)); + res.push((addr.to_string(), pending)); } Ok(res) } diff --git a/contracts/distribution/src/state.rs b/contracts/distribution/src/state.rs index cbce5d21..0d38512f 100644 --- a/contracts/distribution/src/state.rs +++ b/contracts/distribution/src/state.rs @@ -9,9 +9,9 @@ pub struct Config { pub owner: Addr, } -pub const PENDING_DISTRIBUTION: Map<&[u8], Uint128> = Map::new("pending_distribution"); +pub const PENDING_DISTRIBUTION: Map = Map::new("pending_distribution"); -pub const SHARES: Map<&[u8], Uint128> = Map::new("shares"); +pub const SHARES: Map = Map::new("shares"); pub const CONFIG: Item = Item::new("config"); diff --git a/contracts/distribution/src/testing/tests.rs b/contracts/distribution/src/testing/tests.rs index 2cd91c52..966a73d5 100644 --- a/contracts/distribution/src/testing/tests.rs +++ b/contracts/distribution/src/testing/tests.rs @@ -1,7 +1,7 @@ use cosmwasm_std::{ coin, coins, testing::{mock_env, mock_info}, - BankMsg, CosmosMsg, DepsMut, Empty, Uint128, + Addr, BankMsg, CosmosMsg, DepsMut, Empty, Uint128, }; use crate::{ @@ -65,14 +65,14 @@ fn test_fund_success() { SHARES .save( deps.as_mut().storage, - "addr1".as_bytes(), + Addr::unchecked("addr1"), &Uint128::from(1u128), ) .unwrap(); SHARES .save( deps.as_mut().storage, - "addr2".as_bytes(), + Addr::unchecked("addr2"), &Uint128::from(3u128), ) .unwrap(); @@ -86,13 +86,13 @@ fn test_fund_success() { assert!(res.is_ok()); assert_eq!( PENDING_DISTRIBUTION - .load(deps.as_ref().storage, "addr1".as_bytes()) + .load(deps.as_ref().storage, Addr::unchecked("addr1")) .unwrap(), Uint128::from(2500u128) ); assert_eq!( PENDING_DISTRIBUTION - .load(deps.as_ref().storage, "addr2".as_bytes()) + .load(deps.as_ref().storage, Addr::unchecked("addr2")) .unwrap(), Uint128::from(7500u128) ); @@ -107,14 +107,14 @@ fn test_fund_success_with_dust() { SHARES .save( deps.as_mut().storage, - "addr1".as_bytes(), + Addr::unchecked("addr1"), &Uint128::from(1u128), ) .unwrap(); SHARES .save( deps.as_mut().storage, - "addr2".as_bytes(), + Addr::unchecked("addr2"), &Uint128::from(3u128), ) .unwrap(); @@ -129,13 +129,13 @@ fn test_fund_success_with_dust() { println!("{:?}", res.unwrap().attributes); assert_eq!( PENDING_DISTRIBUTION - .load(deps.as_ref().storage, "addr1".as_bytes()) + .load(deps.as_ref().storage, Addr::unchecked("addr1")) .unwrap(), Uint128::from(2501u128) ); assert_eq!( PENDING_DISTRIBUTION - .load(deps.as_ref().storage, "addr2".as_bytes()) + .load(deps.as_ref().storage, Addr::unchecked("addr2")) .unwrap(), Uint128::from(7500u128) ); @@ -163,7 +163,7 @@ fn test_withdraw_success() { PENDING_DISTRIBUTION .save( deps.as_mut().storage, - "addr1".as_bytes(), + Addr::unchecked("addr1"), &Uint128::from(1000u128), ) .unwrap(); @@ -182,7 +182,7 @@ fn test_withdraw_success() { ); assert_eq!( PENDING_DISTRIBUTION - .may_load(deps.as_ref().storage, "addr1".as_bytes()) + .may_load(deps.as_ref().storage, Addr::unchecked("addr1")) .unwrap(), None ); @@ -207,21 +207,21 @@ fn test_set_shares() { SHARES .save( deps.as_mut().storage, - "addr1".as_bytes(), + Addr::unchecked("addr1"), &Uint128::from(1u128), ) .unwrap(); SHARES .save( deps.as_mut().storage, - "addr2".as_bytes(), + Addr::unchecked("addr2"), &Uint128::from(3u128), ) .unwrap(); SHARES .save( deps.as_mut().storage, - "addr3".as_bytes(), + Addr::unchecked("addr3"), &Uint128::from(3u128), ) .unwrap(); @@ -235,19 +235,19 @@ fn test_set_shares() { assert!(res.is_ok()); assert_eq!( SHARES - .load(deps.as_ref().storage, "addr1".as_bytes()) + .load(deps.as_ref().storage, Addr::unchecked("addr1")) .unwrap(), Uint128::from(1u128) ); assert_eq!( SHARES - .load(deps.as_ref().storage, "addr2".as_bytes()) + .load(deps.as_ref().storage, Addr::unchecked("addr2")) .unwrap(), Uint128::from(2u128) ); assert_eq!( SHARES - .may_load(deps.as_ref().storage, "addr3".as_bytes()) + .may_load(deps.as_ref().storage, Addr::unchecked("addr3")) .unwrap(), None ); From 3b01b4ea8843fc4338cf3d233d9b8569fa053f1f Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Wed, 14 Dec 2022 15:06:07 +0100 Subject: [PATCH 31/36] feat: bank > reserve contract --- contracts/reserve/.cargo/config | 6 + contracts/reserve/Cargo.toml | 22 ++++ contracts/reserve/Makefile | 9 ++ contracts/reserve/README.md | 1 + contracts/reserve/examples/schema.rs | 31 +++++ contracts/reserve/schema/execute_msg.json | 49 ++++++++ contracts/reserve/schema/instantiate_msg.json | 17 +++ contracts/reserve/schema/query_msg.json | 19 +++ contracts/reserve/src/contract.rs | 118 ++++++++++++++++++ contracts/reserve/src/lib.rs | 5 + contracts/reserve/src/msg.rs | 28 +++++ contracts/reserve/src/state.rs | 12 ++ contracts/reserve/src/testing/mock_querier.rs | 22 ++++ contracts/reserve/src/testing/mod.rs | 2 + contracts/reserve/src/testing/tests.rs | 88 +++++++++++++ contracts/treasury/schema/execute_msg.json | 38 ++---- .../treasury/schema/instantiate_msg.json | 7 +- contracts/treasury/src/contract.rs | 113 ++++++----------- contracts/treasury/src/msg.rs | 12 +- contracts/treasury/src/state.rs | 5 +- contracts/treasury/src/testing/tests.rs | 82 +++--------- 21 files changed, 505 insertions(+), 181 deletions(-) create mode 100644 contracts/reserve/.cargo/config create mode 100644 contracts/reserve/Cargo.toml create mode 100644 contracts/reserve/Makefile create mode 100644 contracts/reserve/README.md create mode 100644 contracts/reserve/examples/schema.rs create mode 100644 contracts/reserve/schema/execute_msg.json create mode 100644 contracts/reserve/schema/instantiate_msg.json create mode 100644 contracts/reserve/schema/query_msg.json create mode 100644 contracts/reserve/src/contract.rs create mode 100644 contracts/reserve/src/lib.rs create mode 100644 contracts/reserve/src/msg.rs create mode 100644 contracts/reserve/src/state.rs create mode 100644 contracts/reserve/src/testing/mock_querier.rs create mode 100644 contracts/reserve/src/testing/mod.rs create mode 100644 contracts/reserve/src/testing/tests.rs diff --git a/contracts/reserve/.cargo/config b/contracts/reserve/.cargo/config new file mode 100644 index 00000000..7c115322 --- /dev/null +++ b/contracts/reserve/.cargo/config @@ -0,0 +1,6 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +wasm-debug = "build --target wasm32-unknown-unknown" +unit-test = "test --lib --features backtraces" +integration-test = "test --test integration" +schema = "run --example schema" diff --git a/contracts/reserve/Cargo.toml b/contracts/reserve/Cargo.toml new file mode 100644 index 00000000..32205c74 --- /dev/null +++ b/contracts/reserve/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "neutron-reserve" +version = "0.1.0" +authors = ["Sergey Ratiashvili "] +edition = "2021" +license = "Apache-2.0" +repository = "https://github.com/neutron/neutron-dao" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +backtraces = ["cosmwasm-std/backtraces"] + +[dependencies] +cosmwasm-std = { version = "1.0" } +cw-storage-plus = "0.13" +schemars = "0.8.1" +serde = { version = "1.0.103", default-features = false, features = ["derive"] } + +[dev-dependencies] +cosmwasm-schema = { version = "1.0.0", default-features = false } diff --git a/contracts/reserve/Makefile b/contracts/reserve/Makefile new file mode 100644 index 00000000..e2aa6a26 --- /dev/null +++ b/contracts/reserve/Makefile @@ -0,0 +1,9 @@ +CURRENT_DIR = $(shell pwd) +CURRENT_DIR_RELATIVE = $(notdir $(shell pwd)) + +clippy: + rustup component add clippy || true + cargo clippy --all-targets --all-features --workspace -- -D warnings + +test: clippy + cargo unit-test diff --git a/contracts/reserve/README.md b/contracts/reserve/README.md new file mode 100644 index 00000000..9db39578 --- /dev/null +++ b/contracts/reserve/README.md @@ -0,0 +1 @@ +# Neutron treasury diff --git a/contracts/reserve/examples/schema.rs b/contracts/reserve/examples/schema.rs new file mode 100644 index 00000000..9ebb4dec --- /dev/null +++ b/contracts/reserve/examples/schema.rs @@ -0,0 +1,31 @@ +// Copyright 2022 Neutron +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::env::current_dir; +use std::fs::create_dir_all; + +use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; + +use neutron_reserve::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + let mut out_dir = current_dir().unwrap(); + out_dir.push("schema"); + create_dir_all(&out_dir).unwrap(); + remove_schemas(&out_dir).unwrap(); + + export_schema(&schema_for!(InstantiateMsg), &out_dir); + export_schema(&schema_for!(ExecuteMsg), &out_dir); + export_schema(&schema_for!(QueryMsg), &out_dir); +} diff --git a/contracts/reserve/schema/execute_msg.json b/contracts/reserve/schema/execute_msg.json new file mode 100644 index 00000000..c0803a5b --- /dev/null +++ b/contracts/reserve/schema/execute_msg.json @@ -0,0 +1,49 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "description": "Transfer the contract's ownership to another account", + "type": "object", + "required": [ + "transfer_ownership" + ], + "properties": { + "transfer_ownership": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "payout" + ], + "properties": { + "payout": { + "type": "object", + "required": [ + "amount", + "recipient" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "recipient": { + "type": "string" + } + } + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/reserve/schema/instantiate_msg.json b/contracts/reserve/schema/instantiate_msg.json new file mode 100644 index 00000000..955cece9 --- /dev/null +++ b/contracts/reserve/schema/instantiate_msg.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "denom", + "owner" + ], + "properties": { + "denom": { + "type": "string" + }, + "owner": { + "type": "string" + } + } +} diff --git a/contracts/reserve/schema/query_msg.json b/contracts/reserve/schema/query_msg.json new file mode 100644 index 00000000..d3f74fb2 --- /dev/null +++ b/contracts/reserve/schema/query_msg.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "description": "The contract's configuration", + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "type": "object" + } + }, + "additionalProperties": false + } + ] +} diff --git a/contracts/reserve/src/contract.rs b/contracts/reserve/src/contract.rs new file mode 100644 index 00000000..51de1d8d --- /dev/null +++ b/contracts/reserve/src/contract.rs @@ -0,0 +1,118 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + coins, to_binary, Addr, BankMsg, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Response, + StdError, StdResult, Uint128, +}; + +use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use crate::state::{Config, CONFIG}; + +//-------------------------------------------------------------------------------------------------- +// Instantiation +//-------------------------------------------------------------------------------------------------- + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> StdResult { + let config = Config { + denom: msg.denom, + owner: deps.api.addr_validate(&msg.owner)?, + }; + CONFIG.save(deps.storage, &config)?; + + Ok(Response::new()) +} + +//-------------------------------------------------------------------------------------------------- +// Executions +//-------------------------------------------------------------------------------------------------- + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> StdResult { + let api = deps.api; + match msg { + // permissioned - owner + ExecuteMsg::TransferOwnership(new_owner) => { + execute_transfer_ownership(deps, info, api.addr_validate(&new_owner)?) + } + ExecuteMsg::Payout { amount, recipient } => { + execute_payout(deps, info, env, amount, recipient) + } + } +} + +pub fn execute_transfer_ownership( + deps: DepsMut, + info: MessageInfo, + new_owner_addr: Addr, +) -> StdResult { + let config = CONFIG.load(deps.storage)?; + let sender_addr = info.sender; + let old_owner = config.owner; + if sender_addr != old_owner { + return Err(StdError::generic_err("unauthorized")); + } + + CONFIG.update(deps.storage, |mut config| -> StdResult<_> { + config.owner = new_owner_addr.clone(); + Ok(config) + })?; + + Ok(Response::new() + .add_attribute("action", "neutron/treasury/transfer_ownership") + .add_attribute("previous_owner", old_owner) + .add_attribute("new_owner", new_owner_addr)) +} + +pub fn execute_payout( + deps: DepsMut, + info: MessageInfo, + env: Env, + amount: Uint128, + recipient: String, +) -> StdResult { + let config: Config = CONFIG.load(deps.storage)?; + let denom = config.denom; + if info.sender != config.owner { + return Err(StdError::generic_err("unauthorized")); + } + // verify that the contract has enough funds + let bank_balance = deps + .querier + .query_balance(env.contract.address, denom.clone())? + .amount; + + if amount.gt(&bank_balance) { + return Err(StdError::generic_err("insufficient funds")); + } + + Ok(Response::new() + .add_message(CosmosMsg::Bank(BankMsg::Send { + to_address: recipient.clone(), + amount: coins(amount.u128(), denom), + })) + .add_attribute("action", "neutron/treasury/payout") + .add_attribute("amount", amount) + .add_attribute("recipient", recipient)) +} + +//-------------------------------------------------------------------------------------------------- +// Queries +//-------------------------------------------------------------------------------------------------- + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Config {} => to_binary(&query_config(deps)?), + } +} + +pub fn query_config(deps: Deps) -> StdResult { + let config = CONFIG.load(deps.storage)?; + Ok(config) +} diff --git a/contracts/reserve/src/lib.rs b/contracts/reserve/src/lib.rs new file mode 100644 index 00000000..2a287a05 --- /dev/null +++ b/contracts/reserve/src/lib.rs @@ -0,0 +1,5 @@ +pub mod contract; +pub mod msg; +pub mod state; +#[cfg(test)] +mod testing; diff --git a/contracts/reserve/src/msg.rs b/contracts/reserve/src/msg.rs new file mode 100644 index 00000000..57e07a8e --- /dev/null +++ b/contracts/reserve/src/msg.rs @@ -0,0 +1,28 @@ +use cosmwasm_std::Uint128; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct InstantiateMsg { + pub owner: String, + pub denom: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ExecuteMsg { + /// Transfer the contract's ownership to another account + TransferOwnership(String), + // Payout funds at DAO decision + Payout { + amount: Uint128, + recipient: String, + }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum QueryMsg { + /// The contract's configuration + Config {}, +} diff --git a/contracts/reserve/src/state.rs b/contracts/reserve/src/state.rs new file mode 100644 index 00000000..438f9bb9 --- /dev/null +++ b/contracts/reserve/src/state.rs @@ -0,0 +1,12 @@ +use cosmwasm_std::Addr; +use cw_storage_plus::Item; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct Config { + pub denom: String, + pub owner: Addr, +} + +pub const CONFIG: Item = Item::new("config"); diff --git a/contracts/reserve/src/testing/mock_querier.rs b/contracts/reserve/src/testing/mock_querier.rs new file mode 100644 index 00000000..df2eaa1f --- /dev/null +++ b/contracts/reserve/src/testing/mock_querier.rs @@ -0,0 +1,22 @@ +use std::marker::PhantomData; + +use cosmwasm_std::{ + testing::{MockApi, MockQuerier, MockStorage}, + Coin, OwnedDeps, +}; + +const MOCK_CONTRACT_ADDR: &str = "cosmos2contract"; + +pub fn mock_dependencies( + contract_balance: &[Coin], +) -> OwnedDeps { + let contract_addr = MOCK_CONTRACT_ADDR; + let custom_querier = MockQuerier::new(&[(contract_addr, contract_balance)]); + + OwnedDeps { + storage: MockStorage::default(), + api: MockApi::default(), + querier: custom_querier, + custom_query_type: PhantomData, + } +} diff --git a/contracts/reserve/src/testing/mod.rs b/contracts/reserve/src/testing/mod.rs new file mode 100644 index 00000000..a1e507b6 --- /dev/null +++ b/contracts/reserve/src/testing/mod.rs @@ -0,0 +1,2 @@ +mod mock_querier; +mod tests; diff --git a/contracts/reserve/src/testing/tests.rs b/contracts/reserve/src/testing/tests.rs new file mode 100644 index 00000000..91cab54c --- /dev/null +++ b/contracts/reserve/src/testing/tests.rs @@ -0,0 +1,88 @@ +use cosmwasm_std::{ + coin, coins, + testing::{mock_env, mock_info}, + BankMsg, Coin, CosmosMsg, DepsMut, Empty, Uint128, +}; + +use crate::{ + contract::{execute, instantiate}, + msg::{ExecuteMsg, InstantiateMsg}, + state::CONFIG, + testing::mock_querier::mock_dependencies, +}; + +const DENOM: &str = "denom"; + +pub fn init_base_contract(deps: DepsMut) { + let msg = InstantiateMsg { + denom: DENOM.to_string(), + + owner: "owner".to_string(), + }; + let info = mock_info("creator", &coins(2, DENOM)); + instantiate(deps, mock_env(), info, msg).unwrap(); +} + +#[test] +fn test_transfer_ownership() { + let mut deps = mock_dependencies(&[]); + init_base_contract(deps.as_mut()); + let msg = ExecuteMsg::TransferOwnership("new_owner".to_string()); + let res = execute(deps.as_mut(), mock_env(), mock_info("owner", &[]), msg); + assert!(res.is_ok()); + let config = CONFIG.load(deps.as_ref().storage).unwrap(); + assert_eq!(config.owner.to_string(), "new_owner".to_string()); +} + +#[test] +fn test_payout_no_money() { + let mut deps = mock_dependencies(&[]); + init_base_contract(deps.as_mut()); + let msg = ExecuteMsg::Payout { + amount: Uint128::from(500000u128), + recipient: "some".to_string(), + }; + let res = execute(deps.as_mut(), mock_env(), mock_info("owner", &[]), msg); + assert!(res.is_err()); + assert_eq!( + res.unwrap_err().to_string(), + "Generic error: insufficient funds" + ); +} + +#[test] +fn test_payout_not_owner() { + let mut deps = mock_dependencies(&[]); + init_base_contract(deps.as_mut()); + let msg = ExecuteMsg::Payout { + amount: Uint128::from(500000u128), + recipient: "some".to_string(), + }; + let res = execute(deps.as_mut(), mock_env(), mock_info("not_owner", &[]), msg); + assert!(res.is_err()); + assert_eq!(res.unwrap_err().to_string(), "Generic error: unauthorized"); +} + +#[test] +fn test_payout_success() { + let mut deps = mock_dependencies(&[coin(1000000, DENOM)]); + init_base_contract(deps.as_mut()); + let msg = ExecuteMsg::Payout { + amount: Uint128::from(400000u128), + recipient: "some".to_string(), + }; + let res = execute(deps.as_mut(), mock_env(), mock_info("owner", &[]), msg); + assert!(res.is_ok()); + let messages = res.unwrap().messages; + assert_eq!(messages.len(), 1); + assert_eq!( + messages[0].msg, + CosmosMsg::Bank(BankMsg::Send { + to_address: "some".to_string(), + amount: vec![Coin { + denom: DENOM.to_string(), + amount: Uint128::from(400000u128) + }], + }) + ); +} diff --git a/contracts/treasury/schema/execute_msg.json b/contracts/treasury/schema/execute_msg.json index f2c25df3..1c2d7358 100644 --- a/contracts/treasury/schema/execute_msg.json +++ b/contracts/treasury/schema/execute_msg.json @@ -28,30 +28,6 @@ }, "additionalProperties": false }, - { - "type": "object", - "required": [ - "payout" - ], - "properties": { - "payout": { - "type": "object", - "required": [ - "amount", - "recipient" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "recipient": { - "type": "string" - } - } - } - }, - "additionalProperties": false - }, { "type": "object", "required": [ @@ -82,17 +58,17 @@ ], "format": "uint64", "minimum": 0.0 + }, + "reserve_contract": { + "type": [ + "string", + "null" + ] } } } }, "additionalProperties": false } - ], - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } + ] } diff --git a/contracts/treasury/schema/instantiate_msg.json b/contracts/treasury/schema/instantiate_msg.json index a073a83a..952389f0 100644 --- a/contracts/treasury/schema/instantiate_msg.json +++ b/contracts/treasury/schema/instantiate_msg.json @@ -7,7 +7,8 @@ "distribution_contract", "distribution_rate", "min_period", - "owner" + "owner", + "reserve_contract" ], "properties": { "denom": { @@ -31,6 +32,10 @@ }, "owner": { "type": "string" + }, + "reserve_contract": { + "description": "Address of reserve contract", + "type": "string" } } } diff --git a/contracts/treasury/src/contract.rs b/contracts/treasury/src/contract.rs index 9af059d7..5b061458 100644 --- a/contracts/treasury/src/contract.rs +++ b/contracts/treasury/src/contract.rs @@ -1,14 +1,13 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - coins, to_binary, Addr, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, Env, MessageInfo, - Response, StdError, StdResult, Uint128, WasmMsg, + coins, to_binary, Addr, BankMsg, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Response, + StdError, StdResult, Uint128, WasmMsg, }; use crate::msg::{DistributeMsg, ExecuteMsg, InstantiateMsg, QueryMsg, StatsResponse}; use crate::state::{ - Config, BANK_BALANCE, CONFIG, LAST_BALANCE, LAST_DISTRIBUTION_TIME, TOTAL_BANK_SPENT, - TOTAL_DISTRIBUTED, TOTAL_RECEIVED, + Config, CONFIG, LAST_DISTRIBUTION_TIME, TOTAL_DISTRIBUTED, TOTAL_RECEIVED, TOTAL_RESERVED, }; //-------------------------------------------------------------------------------------------------- @@ -26,16 +25,15 @@ pub fn instantiate( denom: msg.denom, min_period: msg.min_period, distribution_contract: deps.api.addr_validate(msg.distribution_contract.as_str())?, + reserve_contract: deps.api.addr_validate(msg.reserve_contract.as_str())?, distribution_rate: msg.distribution_rate, owner: deps.api.addr_validate(&msg.owner)?, }; CONFIG.save(deps.storage, &config)?; TOTAL_RECEIVED.save(deps.storage, &Uint128::zero())?; - TOTAL_BANK_SPENT.save(deps.storage, &Uint128::zero())?; TOTAL_DISTRIBUTED.save(deps.storage, &Uint128::zero())?; + TOTAL_RESERVED.save(deps.storage, &Uint128::zero())?; LAST_DISTRIBUTION_TIME.save(deps.storage, &0)?; - LAST_BALANCE.save(deps.storage, &Uint128::zero())?; - BANK_BALANCE.save(deps.storage, &Uint128::zero())?; Ok(Response::new()) } @@ -54,21 +52,20 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S } // permissionless ExecuteMsg::Distribute {} => execute_distribute(deps, env), - // permissioned - owner - ExecuteMsg::Payout { amount, recipient } => { - execute_payout(deps, info, env, amount, recipient) - } + // permissioned - owner ExecuteMsg::UpdateConfig { distribution_rate, min_period, distribution_contract, + reserve_contract, } => execute_update_config( deps, info, distribution_rate, min_period, distribution_contract, + reserve_contract, ), } } @@ -102,6 +99,7 @@ pub fn execute_update_config( distribution_rate: Option, min_period: Option, distribution_contract: Option, + reserve_contract: Option, ) -> StdResult { let mut config: Config = CONFIG.load(deps.storage)?; if info.sender != config.owner { @@ -114,6 +112,9 @@ pub fn execute_update_config( if let Some(distribution_contract) = distribution_contract { config.distribution_contract = deps.api.addr_validate(distribution_contract.as_str())?; } + if let Some(reserve_contract) = reserve_contract { + config.reserve_contract = deps.api.addr_validate(reserve_contract.as_str())?; + } if let Some(distribution_rate) = distribution_rate { config.distribution_rate = distribution_rate; } @@ -137,87 +138,57 @@ pub fn execute_distribute(deps: DepsMut, env: Env) -> StdResult { return Err(StdError::generic_err("too soon to distribute")); } LAST_DISTRIBUTION_TIME.save(deps.storage, ¤t_time)?; - let last_balance = LAST_BALANCE.load(deps.storage)?; let current_balance = deps .querier - .query_balance(env.contract.address, denom.clone())?; - if current_balance.amount.eq(&last_balance) { - return Err(StdError::generic_err("no new funds to distribute")); + .query_balance(env.contract.address, denom.clone())? + .amount; + + if current_balance.is_zero() { + return Err(StdError::GenericErr { + msg: "no new funds to distribute".to_string(), + }); } - let balance_delta = current_balance.amount.checked_sub(last_balance)?; - let to_distribute = balance_delta + + let to_distribute = current_balance .checked_mul(config.distribution_rate.into())? .checked_div(100u128.into())?; - let to_bank = balance_delta.checked_sub(to_distribute)?; - // update total received + let to_reserve = current_balance.checked_sub(to_distribute)?; + // update stats let total_received = TOTAL_RECEIVED.load(deps.storage)?; - TOTAL_RECEIVED.save(deps.storage, &(total_received.checked_add(balance_delta)?))?; - // update bank - let bank_balance = BANK_BALANCE.load(deps.storage)?; - BANK_BALANCE.save(deps.storage, &(bank_balance.checked_add(to_bank)?))?; + TOTAL_RECEIVED.save( + deps.storage, + &(total_received.checked_add(current_balance)?), + )?; let total_distributed = TOTAL_DISTRIBUTED.load(deps.storage)?; TOTAL_DISTRIBUTED.save( deps.storage, &(total_distributed.checked_add(to_distribute)?), )?; + let total_reserved = TOTAL_RESERVED.load(deps.storage)?; + TOTAL_RESERVED.save(deps.storage, &(total_reserved.checked_add(to_reserve)?))?; - LAST_BALANCE.save( - deps.storage, - ¤t_balance.amount.checked_sub(to_distribute)?, - )?; let mut resp = Response::default(); if !to_distribute.is_zero() { let msg = CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: config.distribution_contract.to_string(), - funds: coins(to_distribute.u128(), denom), + funds: coins(to_distribute.u128(), denom.clone()), msg: to_binary(&DistributeMsg::Fund {})?, }); - deps.api.debug(format!("WASMDEBUG: {:?}", msg).as_str()); resp = resp.add_message(msg) } + + let msg = CosmosMsg::Bank(BankMsg::Send { + to_address: config.reserve_contract.to_string(), + amount: coins(to_reserve.u128(), denom), + }); + resp = resp.add_message(msg); + Ok(resp .add_attribute("action", "neutron/treasury/distribute") - .add_attribute("bank_balance", bank_balance) + .add_attribute("reserved", to_reserve) .add_attribute("distributed", to_distribute)) } -pub fn execute_payout( - deps: DepsMut, - info: MessageInfo, - env: Env, - amount: Uint128, - recipient: String, -) -> StdResult { - let config: Config = CONFIG.load(deps.storage)?; - let denom = config.denom; - if info.sender != config.owner { - return Err(StdError::generic_err("unauthorized")); - } - let bank_balance = BANK_BALANCE.load(deps.storage)?; - if amount.gt(&bank_balance) { - return Err(StdError::generic_err("insufficient funds")); - } - let current_balance = deps - .querier - .query_balance(env.contract.address, denom.clone())?; - if bank_balance.gt(¤t_balance.amount) { - return Err(StdError::generic_err("inconsistent state")); - } - BANK_BALANCE.save(deps.storage, &(bank_balance.checked_sub(amount)?))?; - let total_bank_spent = TOTAL_BANK_SPENT.load(deps.storage)?; - TOTAL_BANK_SPENT.save(deps.storage, &(total_bank_spent.checked_add(amount)?))?; - LAST_BALANCE.save(deps.storage, ¤t_balance.amount.checked_sub(amount)?)?; - - Ok(Response::new() - .add_message(CosmosMsg::Bank(BankMsg::Send { - to_address: recipient.clone(), - amount: vec![Coin { denom, amount }], - })) - .add_attribute("action", "neutron/treasury/payout") - .add_attribute("amount", amount) - .add_attribute("recipient", recipient)) -} - //-------------------------------------------------------------------------------------------------- // Queries //-------------------------------------------------------------------------------------------------- @@ -237,16 +208,12 @@ pub fn query_config(deps: Deps) -> StdResult { pub fn query_stats(deps: Deps) -> StdResult { let total_received = TOTAL_RECEIVED.load(deps.storage)?; - let total_bank_spent = TOTAL_BANK_SPENT.load(deps.storage)?; let total_distributed = TOTAL_DISTRIBUTED.load(deps.storage)?; - let last_balance = LAST_BALANCE.load(deps.storage)?; - let bank_balance = BANK_BALANCE.load(deps.storage)?; + let total_reserved = TOTAL_RESERVED.load(deps.storage)?; Ok(StatsResponse { total_received, - total_bank_spent, total_distributed, - last_balance, - bank_balance, + total_reserved, }) } diff --git a/contracts/treasury/src/msg.rs b/contracts/treasury/src/msg.rs index 3f33517f..601dfb4e 100644 --- a/contracts/treasury/src/msg.rs +++ b/contracts/treasury/src/msg.rs @@ -12,6 +12,8 @@ pub struct InstantiateMsg { pub min_period: u64, /// Address of distribution contract pub distribution_contract: String, + /// Address of reserve contract + pub reserve_contract: String, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] @@ -23,16 +25,12 @@ pub enum ExecuteMsg { /// Distribute pending funds between Bank and Distribution accounts Distribute {}, - // Payout funds at DAO decision - Payout { - amount: Uint128, - recipient: String, - }, // //Update config UpdateConfig { distribution_rate: Option, min_period: Option, distribution_contract: Option, + reserve_contract: Option, }, } @@ -48,10 +46,8 @@ pub enum QueryMsg { #[serde(rename_all = "snake_case")] pub struct StatsResponse { pub total_received: Uint128, - pub total_bank_spent: Uint128, pub total_distributed: Uint128, - pub bank_balance: Uint128, - pub last_balance: Uint128, + pub total_reserved: Uint128, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] diff --git a/contracts/treasury/src/state.rs b/contracts/treasury/src/state.rs index cc109f93..9f5c953b 100644 --- a/contracts/treasury/src/state.rs +++ b/contracts/treasury/src/state.rs @@ -7,17 +7,16 @@ use serde::{Deserialize, Serialize}; pub struct Config { pub distribution_rate: u8, pub distribution_contract: Addr, + pub reserve_contract: Addr, pub min_period: u64, pub denom: String, pub owner: Addr, } pub const TOTAL_RECEIVED: Item = Item::new("total_received"); -pub const TOTAL_BANK_SPENT: Item = Item::new("total_bank_spent"); pub const TOTAL_DISTRIBUTED: Item = Item::new("total_distributed"); +pub const TOTAL_RESERVED: Item = Item::new("total_reserved"); pub const LAST_DISTRIBUTION_TIME: Item = Item::new("last_grab_time"); -pub const LAST_BALANCE: Item = Item::new("last_balance"); -pub const BANK_BALANCE: Item = Item::new("bank_balance"); pub const CONFIG: Item = Item::new("config"); diff --git a/contracts/treasury/src/testing/tests.rs b/contracts/treasury/src/testing/tests.rs index 72d03de5..5b6d9773 100644 --- a/contracts/treasury/src/testing/tests.rs +++ b/contracts/treasury/src/testing/tests.rs @@ -7,7 +7,7 @@ use cosmwasm_std::{ use crate::{ contract::{execute, instantiate}, msg::{DistributeMsg, ExecuteMsg, InstantiateMsg}, - state::{BANK_BALANCE, CONFIG, LAST_BALANCE, TOTAL_BANK_SPENT, TOTAL_RECEIVED}, + state::{CONFIG, TOTAL_DISTRIBUTED, TOTAL_RECEIVED, TOTAL_RESERVED}, testing::mock_querier::mock_dependencies, }; @@ -18,6 +18,7 @@ pub fn init_base_contract(deps: DepsMut) { denom: DENOM.to_string(), min_period: 1000, distribution_contract: "distribution_contract".to_string(), + reserve_contract: "reserve_contract".to_string(), distribution_rate: 23, owner: "owner".to_string(), }; @@ -50,14 +51,14 @@ fn test_collect_with_no_money() { } #[test] -fn test_collect_with() { +fn test_distribute_success() { let mut deps = mock_dependencies(&[coin(1000000, DENOM)]); init_base_contract(deps.as_mut()); let msg = ExecuteMsg::Distribute {}; let res = execute(deps.as_mut(), mock_env(), mock_info("anyone", &[]), msg); assert!(res.is_ok()); let messages = res.unwrap().messages; - assert_eq!(messages.len(), 1); + assert_eq!(messages.len(), 2); assert_eq!( messages[0].msg, CosmosMsg::Wasm(WasmMsg::Execute { @@ -69,74 +70,22 @@ fn test_collect_with() { msg: to_binary(&DistributeMsg::Fund {}).unwrap(), }) ); - let bank_balance = BANK_BALANCE.load(deps.as_ref().storage).unwrap(); - assert_eq!(bank_balance, Uint128::from(770000u128)); - let last_balance = LAST_BALANCE.load(deps.as_ref().storage).unwrap(); - assert_eq!(last_balance, Uint128::from(770000u128)); - let total_received = TOTAL_RECEIVED.load(deps.as_ref().storage).unwrap(); - assert_eq!(total_received, Uint128::from(1000000u128)); -} - -#[test] -fn test_payout_no_money() { - let mut deps = mock_dependencies(&[]); - init_base_contract(deps.as_mut()); - let msg = ExecuteMsg::Payout { - amount: Uint128::from(500000u128), - recipient: "some".to_string(), - }; - let res = execute(deps.as_mut(), mock_env(), mock_info("owner", &[]), msg); - assert!(res.is_err()); assert_eq!( - res.unwrap_err().to_string(), - "Generic error: insufficient funds" - ); -} - -#[test] -fn test_payout_not_owner() { - let mut deps = mock_dependencies(&[]); - init_base_contract(deps.as_mut()); - let msg = ExecuteMsg::Payout { - amount: Uint128::from(500000u128), - recipient: "some".to_string(), - }; - let res = execute(deps.as_mut(), mock_env(), mock_info("not_owner", &[]), msg); - assert!(res.is_err()); - assert_eq!(res.unwrap_err().to_string(), "Generic error: unauthorized"); -} - -#[test] -fn test_payout_success() { - let mut deps = mock_dependencies(&[coin(1000000, DENOM)]); - init_base_contract(deps.as_mut()); - BANK_BALANCE - .save(deps.as_mut().storage, &Uint128::from(1000000u128)) - .unwrap(); - let msg = ExecuteMsg::Payout { - amount: Uint128::from(400000u128), - recipient: "some".to_string(), - }; - let res = execute(deps.as_mut(), mock_env(), mock_info("owner", &[]), msg); - assert!(res.is_ok()); - let messages = res.unwrap().messages; - assert_eq!(messages.len(), 1); - assert_eq!( - messages[0].msg, + messages[1].msg, CosmosMsg::Bank(BankMsg::Send { - to_address: "some".to_string(), + to_address: "reserve_contract".to_string(), amount: vec![Coin { denom: DENOM.to_string(), - amount: Uint128::from(400000u128) - }], + amount: Uint128::from(770000u128) + }] }) ); - let bank_balance = BANK_BALANCE.load(deps.as_ref().storage).unwrap(); - assert_eq!(bank_balance, Uint128::from(600000u128)); - let total_payout = TOTAL_BANK_SPENT.load(deps.as_ref().storage).unwrap(); - assert_eq!(total_payout, Uint128::from(400000u128)); - let last_balance = LAST_BALANCE.load(deps.as_ref().storage).unwrap(); - assert_eq!(last_balance, Uint128::from(600000u128)); + let total_received = TOTAL_RECEIVED.load(deps.as_ref().storage).unwrap(); + assert_eq!(total_received, Uint128::from(1000000u128)); + let total_reserved = TOTAL_RESERVED.load(deps.as_ref().storage).unwrap(); + assert_eq!(total_reserved, Uint128::from(770000u128)); + let total_distributed = TOTAL_DISTRIBUTED.load(deps.as_ref().storage).unwrap(); + assert_eq!(total_distributed, Uint128::from(230000u128)); } #[test] @@ -145,6 +94,7 @@ fn test_update_config_unauthorized() { init_base_contract(deps.as_mut()); let msg = ExecuteMsg::UpdateConfig { distribution_contract: None, + reserve_contract: None, distribution_rate: None, min_period: None, }; @@ -159,6 +109,7 @@ fn test_update_config_success() { init_base_contract(deps.as_mut()); let msg = ExecuteMsg::UpdateConfig { distribution_contract: Some("new_contract".to_string()), + reserve_contract: Some("new_reserve_contract".to_string()), distribution_rate: Some(11), min_period: Some(3000), }; @@ -166,6 +117,7 @@ fn test_update_config_success() { assert!(res.is_ok()); let config = CONFIG.load(deps.as_ref().storage).unwrap(); assert_eq!(config.distribution_contract, "new_contract"); + assert_eq!(config.reserve_contract, "new_reserve_contract"); assert_eq!(config.distribution_rate, 11); assert_eq!(config.min_period, 3000); } From c647f54a727b4e1943096c4c048f2958a1608ee1 Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Wed, 14 Dec 2022 16:24:16 +0100 Subject: [PATCH 32/36] fix: moar review fixes --- contracts/distribution/src/contract.rs | 22 +++---- contracts/distribution/src/testing/tests.rs | 68 ++++++++++++++++++++- contracts/reserve/src/contract.rs | 2 +- contracts/treasury/src/contract.rs | 2 +- 4 files changed, 74 insertions(+), 20 deletions(-) diff --git a/contracts/distribution/src/contract.rs b/contracts/distribution/src/contract.rs index 93ef55df..ca70e588 100644 --- a/contracts/distribution/src/contract.rs +++ b/contracts/distribution/src/contract.rs @@ -145,7 +145,7 @@ pub fn execute_set_shares( if info.sender != config.owner { return Err(StdError::generic_err("unauthorized")); } - let mut new_shares = vec![]; + let mut new_shares = Vec::with_capacity(shares.len()); for (addr, share) in shares { let addr = deps.api.addr_validate(&addr)?; new_shares.push((addr, share)); @@ -207,24 +207,16 @@ pub fn query_config(deps: Deps) -> StdResult { Ok(config) } -pub fn query_shares(deps: Deps) -> StdResult> { +pub fn query_shares(deps: Deps) -> StdResult> { let shares = SHARES .range(deps.storage, None, None, Order::Ascending) - .collect::>>()?; - let mut res: Vec<(String, Uint128)> = vec![]; - for (addr, shares) in shares { - res.push((addr.to_string(), shares)); - } - Ok(res) + .collect::>>()?; + Ok(shares) } -pub fn query_pending(deps: Deps) -> StdResult> { +pub fn query_pending(deps: Deps) -> StdResult> { let pending = PENDING_DISTRIBUTION .range(deps.storage, None, None, Order::Ascending) - .collect::>>()?; - let mut res: Vec<(String, Uint128)> = vec![]; - for (addr, pending) in pending { - res.push((addr.to_string(), pending)); - } - Ok(res) + .collect::>>()?; + Ok(pending) } diff --git a/contracts/distribution/src/testing/tests.rs b/contracts/distribution/src/testing/tests.rs index 966a73d5..6458604e 100644 --- a/contracts/distribution/src/testing/tests.rs +++ b/contracts/distribution/src/testing/tests.rs @@ -1,12 +1,12 @@ use cosmwasm_std::{ - coin, coins, + coin, coins, from_binary, testing::{mock_env, mock_info}, Addr, BankMsg, CosmosMsg, DepsMut, Empty, Uint128, }; use crate::{ - contract::{execute, instantiate}, - msg::{ExecuteMsg, InstantiateMsg}, + contract::{execute, instantiate, query}, + msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, state::{CONFIG, FUND_COUNTER, PENDING_DISTRIBUTION, SHARES}, testing::mock_querier::mock_dependencies, }; @@ -252,3 +252,65 @@ fn test_set_shares() { None ); } + +#[test] +fn test_query_shares() { + let mut deps = mock_dependencies(&[]); + init_base_contract(deps.as_mut()); + SHARES + .save( + deps.as_mut().storage, + Addr::unchecked("addr1"), + &Uint128::from(1u128), + ) + .unwrap(); + SHARES + .save( + deps.as_mut().storage, + Addr::unchecked("addr2"), + &Uint128::from(3u128), + ) + .unwrap(); + let msg = QueryMsg::Shares {}; + let res = query(deps.as_ref(), mock_env(), msg); + assert!(res.is_ok()); + let value: Vec<(String, Uint128)> = from_binary(&res.unwrap()).unwrap(); + assert_eq!( + value, + vec![ + ("addr1".to_string(), Uint128::from(1u128)), + ("addr2".to_string(), Uint128::from(3u128)) + ] + ); +} + +#[test] +fn test_query_pending() { + let mut deps = mock_dependencies(&[]); + init_base_contract(deps.as_mut()); + PENDING_DISTRIBUTION + .save( + deps.as_mut().storage, + Addr::unchecked("addr1"), + &Uint128::from(1u128), + ) + .unwrap(); + PENDING_DISTRIBUTION + .save( + deps.as_mut().storage, + Addr::unchecked("addr2"), + &Uint128::from(3u128), + ) + .unwrap(); + let msg = QueryMsg::Pending {}; + let res = query(deps.as_ref(), mock_env(), msg); + assert!(res.is_ok()); + let value: Vec<(String, Uint128)> = from_binary(&res.unwrap()).unwrap(); + assert_eq!( + value, + vec![ + ("addr1".to_string(), Uint128::from(1u128)), + ("addr2".to_string(), Uint128::from(3u128)) + ] + ); +} diff --git a/contracts/reserve/src/contract.rs b/contracts/reserve/src/contract.rs index 51de1d8d..fb49c68e 100644 --- a/contracts/reserve/src/contract.rs +++ b/contracts/reserve/src/contract.rs @@ -84,7 +84,7 @@ pub fn execute_payout( // verify that the contract has enough funds let bank_balance = deps .querier - .query_balance(env.contract.address, denom.clone())? + .query_balance(env.contract.address, &denom)? .amount; if amount.gt(&bank_balance) { diff --git a/contracts/treasury/src/contract.rs b/contracts/treasury/src/contract.rs index 5b061458..f469c32a 100644 --- a/contracts/treasury/src/contract.rs +++ b/contracts/treasury/src/contract.rs @@ -140,7 +140,7 @@ pub fn execute_distribute(deps: DepsMut, env: Env) -> StdResult { LAST_DISTRIBUTION_TIME.save(deps.storage, ¤t_time)?; let current_balance = deps .querier - .query_balance(env.contract.address, denom.clone())? + .query_balance(env.contract.address, &denom)? .amount; if current_balance.is_zero() { From 68b366bde0b60df975858eda6912c10ce25aad01 Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Wed, 14 Dec 2022 23:02:02 +0100 Subject: [PATCH 33/36] chore: bump cw-storage-plus ver --- contracts/distribution/Cargo.toml | 2 +- contracts/reserve/Cargo.toml | 2 +- contracts/treasury/Cargo.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/distribution/Cargo.toml b/contracts/distribution/Cargo.toml index 97211b08..3684a775 100644 --- a/contracts/distribution/Cargo.toml +++ b/contracts/distribution/Cargo.toml @@ -14,7 +14,7 @@ backtraces = ["cosmwasm-std/backtraces"] [dependencies] cosmwasm-std = { version = "1.0" } -cw-storage-plus = "0.13" +cw-storage-plus = "1.0.1" schemars = "0.8.1" serde = { version = "1.0.103", default-features = false, features = ["derive"] } diff --git a/contracts/reserve/Cargo.toml b/contracts/reserve/Cargo.toml index 32205c74..00a738f2 100644 --- a/contracts/reserve/Cargo.toml +++ b/contracts/reserve/Cargo.toml @@ -14,7 +14,7 @@ backtraces = ["cosmwasm-std/backtraces"] [dependencies] cosmwasm-std = { version = "1.0" } -cw-storage-plus = "0.13" +cw-storage-plus = "1.0.1" schemars = "0.8.1" serde = { version = "1.0.103", default-features = false, features = ["derive"] } diff --git a/contracts/treasury/Cargo.toml b/contracts/treasury/Cargo.toml index 3726c790..38a9c06d 100644 --- a/contracts/treasury/Cargo.toml +++ b/contracts/treasury/Cargo.toml @@ -14,7 +14,7 @@ backtraces = ["cosmwasm-std/backtraces"] [dependencies] cosmwasm-std = { version = "1.0" } -cw-storage-plus = "0.13" +cw-storage-plus = "1.0.1" schemars = "0.8.1" serde = { version = "1.0.103", default-features = false, features = ["derive"] } From 9ddf10ef29a8f088a9e2ded8631fffd4a02fed39 Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Wed, 14 Dec 2022 23:07:40 +0100 Subject: [PATCH 34/36] doc: doc --- contracts/distribution/src/msg.rs | 3 ++- contracts/distribution/src/state.rs | 4 ++-- contracts/treasury/src/state.rs | 4 ++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/contracts/distribution/src/msg.rs b/contracts/distribution/src/msg.rs index 350aec01..12547595 100644 --- a/contracts/distribution/src/msg.rs +++ b/contracts/distribution/src/msg.rs @@ -19,7 +19,8 @@ pub enum ExecuteMsg { shares: Vec<(String, Uint128)>, }, - /// Distribute funds between share holders + /// Distribute funds between share holders. It is called from treasury contract only + /// when part of the fund is going to distribution betrween share holders. Fund {}, /// Claim the funds that have been distributed to the contract's account diff --git a/contracts/distribution/src/state.rs b/contracts/distribution/src/state.rs index 0d38512f..1b001040 100644 --- a/contracts/distribution/src/state.rs +++ b/contracts/distribution/src/state.rs @@ -8,9 +8,9 @@ pub struct Config { pub denom: String, pub owner: Addr, } - +/// Map to store the amount of funds that are pending distribution to a given address pub const PENDING_DISTRIBUTION: Map = Map::new("pending_distribution"); - +/// Map to store the amount of shares that a given address has pub const SHARES: Map = Map::new("shares"); pub const CONFIG: Item = Item::new("config"); diff --git a/contracts/treasury/src/state.rs b/contracts/treasury/src/state.rs index 9f5c953b..c7683b5f 100644 --- a/contracts/treasury/src/state.rs +++ b/contracts/treasury/src/state.rs @@ -5,9 +5,13 @@ use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] pub struct Config { + /// Distribution rate in percents (0-100) which goes to distribution contract pub distribution_rate: u8, + /// Address of distribution contract, which will receive funds defined but distribution_rate % pub distribution_contract: Addr, + /// Address of reserve contract, which will receive funds defined by 100-distribution_rate % pub reserve_contract: Addr, + /// Minimum period between distribution calls pub min_period: u64, pub denom: String, pub owner: Addr, From cfa8fda79458036eab439f8b5667c4f8ee1ebc41 Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Thu, 15 Dec 2022 00:39:27 +0100 Subject: [PATCH 35/36] fix: distribution rate > decimal --- .../distribution/schema/execute_msg.json | 2 +- contracts/treasury/schema/execute_msg.json | 22 +++++++++++++------ .../treasury/schema/instantiate_msg.json | 16 ++++++++++---- contracts/treasury/src/contract.rs | 10 ++++----- contracts/treasury/src/msg.rs | 8 +++---- contracts/treasury/src/state.rs | 6 ++--- contracts/treasury/src/testing/tests.rs | 10 +++++---- 7 files changed, 45 insertions(+), 29 deletions(-) diff --git a/contracts/distribution/schema/execute_msg.json b/contracts/distribution/schema/execute_msg.json index a391eba7..b2191627 100644 --- a/contracts/distribution/schema/execute_msg.json +++ b/contracts/distribution/schema/execute_msg.json @@ -49,7 +49,7 @@ "additionalProperties": false }, { - "description": "Distribute funds between share holders", + "description": "Distribute funds between share holders. It is called from treasury contract only when part of the fund is going to distribution betrween share holders.", "type": "object", "required": [ "fund" diff --git a/contracts/treasury/schema/execute_msg.json b/contracts/treasury/schema/execute_msg.json index 1c2d7358..991dcceb 100644 --- a/contracts/treasury/schema/execute_msg.json +++ b/contracts/treasury/schema/execute_msg.json @@ -44,12 +44,14 @@ ] }, "distribution_rate": { - "type": [ - "integer", - "null" - ], - "format": "uint8", - "minimum": 0.0 + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] }, "min_period": { "type": [ @@ -70,5 +72,11 @@ }, "additionalProperties": false } - ] + ], + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + } + } } diff --git a/contracts/treasury/schema/instantiate_msg.json b/contracts/treasury/schema/instantiate_msg.json index 952389f0..cdd51d4c 100644 --- a/contracts/treasury/schema/instantiate_msg.json +++ b/contracts/treasury/schema/instantiate_msg.json @@ -19,10 +19,12 @@ "type": "string" }, "distribution_rate": { - "description": "Distribution rate in percent (0-100) which goes to distribution contract", - "type": "integer", - "format": "uint8", - "minimum": 0.0 + "description": "Distribution rate (0-1) which goes to distribution contract", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] }, "min_period": { "description": "Minimum period between distribution calls", @@ -37,5 +39,11 @@ "description": "Address of reserve contract", "type": "string" } + }, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + } } } diff --git a/contracts/treasury/src/contract.rs b/contracts/treasury/src/contract.rs index f469c32a..2d963da9 100644 --- a/contracts/treasury/src/contract.rs +++ b/contracts/treasury/src/contract.rs @@ -1,8 +1,8 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - coins, to_binary, Addr, BankMsg, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Response, - StdError, StdResult, Uint128, WasmMsg, + coins, to_binary, Addr, BankMsg, Binary, CosmosMsg, Decimal, Deps, DepsMut, Env, MessageInfo, + Response, StdError, StdResult, Uint128, WasmMsg, }; use crate::msg::{DistributeMsg, ExecuteMsg, InstantiateMsg, QueryMsg, StatsResponse}; @@ -96,7 +96,7 @@ pub fn execute_transfer_ownership( pub fn execute_update_config( deps: DepsMut, info: MessageInfo, - distribution_rate: Option, + distribution_rate: Option, min_period: Option, distribution_contract: Option, reserve_contract: Option, @@ -149,9 +149,7 @@ pub fn execute_distribute(deps: DepsMut, env: Env) -> StdResult { }); } - let to_distribute = current_balance - .checked_mul(config.distribution_rate.into())? - .checked_div(100u128.into())?; + let to_distribute = current_balance * config.distribution_rate; let to_reserve = current_balance.checked_sub(to_distribute)?; // update stats let total_received = TOTAL_RECEIVED.load(deps.storage)?; diff --git a/contracts/treasury/src/msg.rs b/contracts/treasury/src/msg.rs index 601dfb4e..58db1887 100644 --- a/contracts/treasury/src/msg.rs +++ b/contracts/treasury/src/msg.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::Uint128; +use cosmwasm_std::{Decimal, Uint128}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -6,8 +6,8 @@ use serde::{Deserialize, Serialize}; pub struct InstantiateMsg { pub owner: String, pub denom: String, - /// Distribution rate in percent (0-100) which goes to distribution contract - pub distribution_rate: u8, + /// Distribution rate (0-1) which goes to distribution contract + pub distribution_rate: Decimal, /// Minimum period between distribution calls pub min_period: u64, /// Address of distribution contract @@ -27,7 +27,7 @@ pub enum ExecuteMsg { // //Update config UpdateConfig { - distribution_rate: Option, + distribution_rate: Option, min_period: Option, distribution_contract: Option, reserve_contract: Option, diff --git a/contracts/treasury/src/state.rs b/contracts/treasury/src/state.rs index c7683b5f..9c18d4bd 100644 --- a/contracts/treasury/src/state.rs +++ b/contracts/treasury/src/state.rs @@ -1,12 +1,12 @@ -use cosmwasm_std::{Addr, Uint128}; +use cosmwasm_std::{Addr, Decimal, Uint128}; use cw_storage_plus::Item; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] pub struct Config { - /// Distribution rate in percents (0-100) which goes to distribution contract - pub distribution_rate: u8, + /// Distribution rate (0-1) which goes to distribution contract + pub distribution_rate: Decimal, /// Address of distribution contract, which will receive funds defined but distribution_rate % pub distribution_contract: Addr, /// Address of reserve contract, which will receive funds defined by 100-distribution_rate % diff --git a/contracts/treasury/src/testing/tests.rs b/contracts/treasury/src/testing/tests.rs index 5b6d9773..cf1bb627 100644 --- a/contracts/treasury/src/testing/tests.rs +++ b/contracts/treasury/src/testing/tests.rs @@ -1,7 +1,9 @@ +use std::str::FromStr; + use cosmwasm_std::{ coin, coins, testing::{mock_env, mock_info}, - to_binary, BankMsg, Coin, CosmosMsg, DepsMut, Empty, Uint128, WasmMsg, + to_binary, BankMsg, Coin, CosmosMsg, Decimal, DepsMut, Empty, Uint128, WasmMsg, }; use crate::{ @@ -19,7 +21,7 @@ pub fn init_base_contract(deps: DepsMut) { min_period: 1000, distribution_contract: "distribution_contract".to_string(), reserve_contract: "reserve_contract".to_string(), - distribution_rate: 23, + distribution_rate: Decimal::from_str("0.23").unwrap(), owner: "owner".to_string(), }; let info = mock_info("creator", &coins(2, DENOM)); @@ -110,7 +112,7 @@ fn test_update_config_success() { let msg = ExecuteMsg::UpdateConfig { distribution_contract: Some("new_contract".to_string()), reserve_contract: Some("new_reserve_contract".to_string()), - distribution_rate: Some(11), + distribution_rate: Some(Decimal::from_str("0.11").unwrap()), min_period: Some(3000), }; let res = execute(deps.as_mut(), mock_env(), mock_info("owner", &[]), msg); @@ -118,6 +120,6 @@ fn test_update_config_success() { let config = CONFIG.load(deps.as_ref().storage).unwrap(); assert_eq!(config.distribution_contract, "new_contract"); assert_eq!(config.reserve_contract, "new_reserve_contract"); - assert_eq!(config.distribution_rate, 11); + assert_eq!(config.distribution_rate, Decimal::from_str("0.11").unwrap()); assert_eq!(config.min_period, 3000); } From b05f481ba3560b2f8075aa940fa7471b579c396e Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Thu, 15 Dec 2022 08:46:01 +0100 Subject: [PATCH 36/36] fix: limit distribution rate --- contracts/treasury/src/contract.rs | 17 +++-- contracts/treasury/src/testing/tests.rs | 84 ++++++++++++++++++++++--- 2 files changed, 89 insertions(+), 12 deletions(-) diff --git a/contracts/treasury/src/contract.rs b/contracts/treasury/src/contract.rs index 2d963da9..5491fdff 100644 --- a/contracts/treasury/src/contract.rs +++ b/contracts/treasury/src/contract.rs @@ -116,6 +116,11 @@ pub fn execute_update_config( config.reserve_contract = deps.api.addr_validate(reserve_contract.as_str())?; } if let Some(distribution_rate) = distribution_rate { + if (distribution_rate > Decimal::one()) || (distribution_rate < Decimal::zero()) { + return Err(StdError::generic_err( + "distribution_rate must be between 0 and 1", + )); + } config.distribution_rate = distribution_rate; } @@ -175,11 +180,13 @@ pub fn execute_distribute(deps: DepsMut, env: Env) -> StdResult { resp = resp.add_message(msg) } - let msg = CosmosMsg::Bank(BankMsg::Send { - to_address: config.reserve_contract.to_string(), - amount: coins(to_reserve.u128(), denom), - }); - resp = resp.add_message(msg); + if !to_reserve.is_zero() { + let msg = CosmosMsg::Bank(BankMsg::Send { + to_address: config.reserve_contract.to_string(), + amount: coins(to_reserve.u128(), denom), + }); + resp = resp.add_message(msg); + } Ok(resp .add_attribute("action", "neutron/treasury/distribute") diff --git a/contracts/treasury/src/testing/tests.rs b/contracts/treasury/src/testing/tests.rs index cf1bb627..22b7e6c0 100644 --- a/contracts/treasury/src/testing/tests.rs +++ b/contracts/treasury/src/testing/tests.rs @@ -15,13 +15,13 @@ use crate::{ const DENOM: &str = "denom"; -pub fn init_base_contract(deps: DepsMut) { +pub fn init_base_contract(deps: DepsMut, distribution_rate: &str) { let msg = InstantiateMsg { denom: DENOM.to_string(), min_period: 1000, distribution_contract: "distribution_contract".to_string(), reserve_contract: "reserve_contract".to_string(), - distribution_rate: Decimal::from_str("0.23").unwrap(), + distribution_rate: Decimal::from_str(distribution_rate).unwrap(), owner: "owner".to_string(), }; let info = mock_info("creator", &coins(2, DENOM)); @@ -31,7 +31,7 @@ pub fn init_base_contract(deps: DepsMut) { #[test] fn test_transfer_ownership() { let mut deps = mock_dependencies(&[]); - init_base_contract(deps.as_mut()); + init_base_contract(deps.as_mut(), "0.23"); let msg = ExecuteMsg::TransferOwnership("new_owner".to_string()); let res = execute(deps.as_mut(), mock_env(), mock_info("owner", &[]), msg); assert!(res.is_ok()); @@ -42,7 +42,7 @@ fn test_transfer_ownership() { #[test] fn test_collect_with_no_money() { let mut deps = mock_dependencies(&[]); - init_base_contract(deps.as_mut()); + init_base_contract(deps.as_mut(), "1"); let msg = ExecuteMsg::Distribute {}; let res = execute(deps.as_mut(), mock_env(), mock_info("anyone", &[]), msg); assert!(res.is_err()); @@ -55,7 +55,7 @@ fn test_collect_with_no_money() { #[test] fn test_distribute_success() { let mut deps = mock_dependencies(&[coin(1000000, DENOM)]); - init_base_contract(deps.as_mut()); + init_base_contract(deps.as_mut(), "0.23"); let msg = ExecuteMsg::Distribute {}; let res = execute(deps.as_mut(), mock_env(), mock_info("anyone", &[]), msg); assert!(res.is_ok()); @@ -90,10 +90,66 @@ fn test_distribute_success() { assert_eq!(total_distributed, Uint128::from(230000u128)); } +#[test] +fn test_distribute_zero_to_reserve() { + let mut deps = mock_dependencies(&[coin(1000000, DENOM)]); + init_base_contract(deps.as_mut(), "1"); + let msg = ExecuteMsg::Distribute {}; + let res = execute(deps.as_mut(), mock_env(), mock_info("anyone", &[]), msg); + assert!(res.is_ok()); + let messages = res.unwrap().messages; + assert_eq!(messages.len(), 1); + assert_eq!( + messages[0].msg, + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "distribution_contract".to_string(), + funds: vec![Coin { + denom: DENOM.to_string(), + amount: Uint128::from(1000000u128) + }], + msg: to_binary(&DistributeMsg::Fund {}).unwrap(), + }) + ); + + let total_received = TOTAL_RECEIVED.load(deps.as_ref().storage).unwrap(); + assert_eq!(total_received, Uint128::from(1000000u128)); + let total_reserved = TOTAL_RESERVED.load(deps.as_ref().storage).unwrap(); + assert_eq!(total_reserved, Uint128::from(0u128)); + let total_distributed = TOTAL_DISTRIBUTED.load(deps.as_ref().storage).unwrap(); + assert_eq!(total_distributed, Uint128::from(1000000u128)); +} + +#[test] +fn test_distribute_zero_to_distribution_contract() { + let mut deps = mock_dependencies(&[coin(1000000, DENOM)]); + init_base_contract(deps.as_mut(), "0"); + let msg = ExecuteMsg::Distribute {}; + let res = execute(deps.as_mut(), mock_env(), mock_info("anyone", &[]), msg); + assert!(res.is_ok()); + let messages = res.unwrap().messages; + assert_eq!(messages.len(), 1); + assert_eq!( + messages[0].msg, + CosmosMsg::Bank(BankMsg::Send { + to_address: "reserve_contract".to_string(), + amount: vec![Coin { + denom: DENOM.to_string(), + amount: Uint128::from(1000000u128) + }] + }) + ); + let total_received = TOTAL_RECEIVED.load(deps.as_ref().storage).unwrap(); + assert_eq!(total_received, Uint128::from(1000000u128)); + let total_reserved = TOTAL_RESERVED.load(deps.as_ref().storage).unwrap(); + assert_eq!(total_reserved, Uint128::from(1000000u128)); + let total_distributed = TOTAL_DISTRIBUTED.load(deps.as_ref().storage).unwrap(); + assert_eq!(total_distributed, Uint128::from(0u128)); +} + #[test] fn test_update_config_unauthorized() { let mut deps = mock_dependencies(&[]); - init_base_contract(deps.as_mut()); + init_base_contract(deps.as_mut(), "1"); let msg = ExecuteMsg::UpdateConfig { distribution_contract: None, reserve_contract: None, @@ -108,7 +164,7 @@ fn test_update_config_unauthorized() { #[test] fn test_update_config_success() { let mut deps = mock_dependencies(&[]); - init_base_contract(deps.as_mut()); + init_base_contract(deps.as_mut(), "1"); let msg = ExecuteMsg::UpdateConfig { distribution_contract: Some("new_contract".to_string()), reserve_contract: Some("new_reserve_contract".to_string()), @@ -123,3 +179,17 @@ fn test_update_config_success() { assert_eq!(config.distribution_rate, Decimal::from_str("0.11").unwrap()); assert_eq!(config.min_period, 3000); } + +#[test] +fn test_update_distribution_rate_below_the_limit() { + let mut deps = mock_dependencies(&[]); + init_base_contract(deps.as_mut(), "1"); + let msg = ExecuteMsg::UpdateConfig { + distribution_contract: None, + reserve_contract: None, + distribution_rate: Some(Decimal::from_str("2").unwrap()), + min_period: None, + }; + let res = execute(deps.as_mut(), mock_env(), mock_info("owner", &[]), msg); + assert!(res.is_err()); +}