diff --git a/.gitignore b/.gitignore index 29f22fa3..ef76b67c 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,3 @@ Cargo.lock **/*.rs.bk .idea - diff --git a/Cargo.lock b/Cargo.lock index d0671476..11054164 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,6 +37,12 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" + [[package]] name = "base64ct" version = "1.5.3" @@ -78,6 +84,9 @@ name = "bytes" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" +dependencies = [ + "serde", +] [[package]] name = "cfg-if" @@ -99,7 +108,18 @@ checksum = "20b42021d8488665b1a0d9748f1f81df7235362d194f44481e2e61bf376b77b4" dependencies = [ "prost 0.11.3", "prost-types", - "tendermint-proto", + "tendermint-proto 0.23.9", +] + +[[package]] +name = "cosmos-sdk-proto" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4776e787b24d9568dd61d3237eeb4eb321d622fb881b858c7b82806420e87d4" +dependencies = [ + "prost 0.11.3", + "prost-types", + "tendermint-proto 0.27.0", ] [[package]] @@ -154,7 +174,7 @@ version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b13d5a84d15cf7be17dc249a21588cdb0f7ef308907c50ce2723316a7d79c3dc" dependencies = [ - "base64", + "base64 0.13.1", "cosmwasm-crypto", "cosmwasm-derive", "derivative", @@ -543,7 +563,7 @@ dependencies = [ "cw721", "cwd-interface", "cwd-macros", - "neutron-sdk", + "neutron-sdk 0.1.0 (git+https://github.com/neutron-org/neutron-contracts.git)", "schemars", "serde", "thiserror", @@ -622,7 +642,7 @@ dependencies = [ "cwd-proposal-hooks", "cwd-proposal-single", "cwd-voting", - "neutron-sdk", + "neutron-sdk 0.1.0 (git+https://github.com/neutron-org/neutron-contracts.git)", "schemars", "serde", ] @@ -666,7 +686,7 @@ dependencies = [ "cwd-proposal-hooks", "cwd-vote-hooks", "cwd-voting", - "neutron-sdk", + "neutron-sdk 0.1.0 (git+https://github.com/neutron-org/neutron-contracts.git)", "schemars", "serde", "thiserror", @@ -690,7 +710,7 @@ dependencies = [ "cwd-interface", "cwd-macros", "exec-control", - "neutron-sdk", + "neutron-sdk 0.1.0 (git+https://github.com/neutron-org/neutron-contracts.git)", "schemars", "serde", "thiserror", @@ -716,7 +736,7 @@ dependencies = [ "cwd-subdao-core", "cwd-subdao-proposal-single", "cwd-voting", - "neutron-sdk", + "neutron-sdk 0.1.0 (git+https://github.com/neutron-org/neutron-contracts.git)", "neutron-timelock", "schemars", "serde", @@ -750,7 +770,7 @@ dependencies = [ "cwd-subdao-pre-propose-single", "cwd-vote-hooks", "cwd-voting", - "neutron-sdk", + "neutron-sdk 0.1.0 (git+https://github.com/neutron-org/neutron-contracts.git)", "schemars", "serde", "thiserror", @@ -773,7 +793,7 @@ dependencies = [ "cwd-interface", "cwd-macros", "cwd-pre-propose-base", - "neutron-sdk", + "neutron-sdk 0.1.0 (git+https://github.com/neutron-org/neutron-contracts.git)", "neutron-timelock", "schemars", "serde", @@ -803,7 +823,7 @@ dependencies = [ "cwd-core", "cwd-interface", "cwd-macros", - "neutron-sdk", + "neutron-sdk 0.1.0 (git+https://github.com/neutron-org/neutron-contracts.git)", "schemars", "serde", "thiserror", @@ -1078,9 +1098,9 @@ name = "neutron-sdk" version = "0.1.0" source = "git+https://github.com/neutron-org/neutron-contracts.git#3ad81939da78954ff032a26e54d792cc16a4d9f3" dependencies = [ - "base64", + "base64 0.13.1", "bech32", - "cosmos-sdk-proto", + "cosmos-sdk-proto 0.14.0", "cosmwasm-std", "cw-storage-plus 0.16.0", "prost 0.11.3", @@ -1092,12 +1112,32 @@ dependencies = [ "thiserror", ] +[[package]] +name = "neutron-sdk" +version = "0.1.0" +source = "git+https://github.com/neutron-org/neutron-sdk.git#c19b40c024eeaa8733af9ddee94a52798d78f469" +dependencies = [ + "base64 0.20.0", + "bech32", + "cosmos-sdk-proto 0.16.0", + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.0.1", + "prost 0.11.3", + "protobuf", + "schemars", + "serde", + "serde-json-wasm", + "serde_json", + "thiserror", +] + [[package]] name = "neutron-timelock" version = "0.1.0" dependencies = [ "cosmwasm-std", - "neutron-sdk", + "neutron-sdk 0.1.0 (git+https://github.com/neutron-org/neutron-contracts.git)", "schemars", "serde", ] @@ -1111,6 +1151,7 @@ dependencies = [ "cw-storage-plus 1.0.1", "cwd-macros", "exec-control", + "neutron-sdk 0.1.0 (git+https://github.com/neutron-org/neutron-sdk.git)", "schemars", "serde", "thiserror", @@ -1546,6 +1587,24 @@ dependencies = [ "time", ] +[[package]] +name = "tendermint-proto" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5895470f28c530f8ae8c4071bf8190304ce00bd131d25e81730453124a3375c" +dependencies = [ + "bytes", + "flex-error", + "num-derive", + "num-traits", + "prost 0.11.3", + "prost-types", + "serde", + "serde_bytes", + "subtle-encoding", + "time", +] + [[package]] name = "thiserror" version = "1.0.37" diff --git a/Makefile b/Makefile index eeb73735..b771bc9d 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ check_contracts: compile: @./build_release.sh -build: schema clippy fmt compile check_contracts +build: schema clippy fmt test compile check_contracts diff --git a/artifacts/checksums.txt b/artifacts/checksums.txt index 0e19eed9..ea6a448b 100644 --- a/artifacts/checksums.txt +++ b/artifacts/checksums.txt @@ -8,6 +8,6 @@ acba35a90a5b81256eda8055512e0e9117586b5869687986ebf8f0bf14a6d605 cwd_subdao_pro 341fce8e9437a4f3475d331e54acd12ef092450a39bff804543f9ee4c87e6556 cwd_subdao_timelock_single.wasm de33de7ae51e0d0f64534dac4562f339cad66966f8b4d5762060b850f5f2f114 neutron_distribution.wasm 1982982b78b417fd87108c731f91a2226704b00fe722d0ad6985c45e5e444c89 neutron_reserve.wasm -ea4a6c7d9cb14bed91eb572b2531070db1a7f7dace1e217d759ea2cebaa769b5 neutron_treasury.wasm +fb003c04917dad5e51c8bcbc5c172705f8c86c1ce88c5a139b828f5b88b18de2 neutron_treasury.wasm dc5542363aba30fb46f32d2cf0e883cc8122736bc149a11ce3d3cfeb4ef9830c neutron_vault.wasm 7516c30b8868f52596306c1f54a6daf83da53f8e585cd1fe6210990cb2747051 neutron_voting_registry.wasm diff --git a/artifacts/checksums_intermediate.txt b/artifacts/checksums_intermediate.txt index c3b43c01..80269e61 100644 --- a/artifacts/checksums_intermediate.txt +++ b/artifacts/checksums_intermediate.txt @@ -10,4 +10,4 @@ f9affa3dbdd7632bfd2c9b90de9e959cf441f94fdbb5b47068781de94fa4fcce target/wasm32- e27881309f58faeebada41e462a1210a068f6194db4834faa5f6fb24a6c4d295 target/wasm32-unknown-unknown/release/cwd_subdao_core.wasm 21bfa6eb92d9b98c4fe73567471b09cb849614d0ff8a739e6bfe3d28c24e0094 target/wasm32-unknown-unknown/release/neutron_distribution.wasm 143a562b387797caff1e6c51ab80a4ce678419e00b2986b362b7aff378b762e5 target/wasm32-unknown-unknown/release/neutron_reserve.wasm -117bfb323974358045d534d1362fdc26d1fed805ec357da9ba1fd38f17fc714e target/wasm32-unknown-unknown/release/neutron_treasury.wasm +766ae9b870511352aa835e53b88bee081b565c31af69ddacc9d5bbc32f41c3fc target/wasm32-unknown-unknown/release/neutron_treasury.wasm diff --git a/artifacts/neutron_treasury.wasm b/artifacts/neutron_treasury.wasm index dd9f0a41..d370fe49 100644 Binary files a/artifacts/neutron_treasury.wasm and b/artifacts/neutron_treasury.wasm differ diff --git a/contracts/subdaos/cwd-subdao-timelock-single/src/testing/mod.rs b/contracts/subdaos/cwd-subdao-timelock-single/src/testing/mod.rs index a1e507b6..6e26792e 100644 --- a/contracts/subdaos/cwd-subdao-timelock-single/src/testing/mod.rs +++ b/contracts/subdaos/cwd-subdao-timelock-single/src/testing/mod.rs @@ -1,2 +1,2 @@ -mod mock_querier; +pub mod mock_querier; mod tests; diff --git a/contracts/tokenomics/distribution/src/msg.rs b/contracts/tokenomics/distribution/src/msg.rs index b09c1ec4..9feafc46 100644 --- a/contracts/tokenomics/distribution/src/msg.rs +++ b/contracts/tokenomics/distribution/src/msg.rs @@ -44,7 +44,6 @@ pub enum QueryMsg { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct StatsResponse { - pub total_received: Uint128, pub total_distributed: Uint128, pub last_balance: Uint128, } diff --git a/contracts/tokenomics/reserve/src/lib.rs b/contracts/tokenomics/reserve/src/lib.rs index caff7ec1..f4c03d7b 100644 --- a/contracts/tokenomics/reserve/src/lib.rs +++ b/contracts/tokenomics/reserve/src/lib.rs @@ -2,5 +2,6 @@ pub mod contract; pub mod error; pub mod msg; pub mod state; + #[cfg(test)] mod testing; diff --git a/contracts/tokenomics/treasury/.cargo/schema/execute_msg.json b/contracts/tokenomics/treasury/.cargo/schema/execute_msg.json index 991dcceb..d44ea9a7 100644 --- a/contracts/tokenomics/treasury/.cargo/schema/execute_msg.json +++ b/contracts/tokenomics/treasury/.cargo/schema/execute_msg.json @@ -66,6 +66,14 @@ "string", "null" ] + }, + "vesting_denominator": { + "type": [ + "integer", + "null" + ], + "format": "uint128", + "minimum": 0.0 } } } diff --git a/contracts/tokenomics/treasury/.cargo/schema/instantiate_msg.json b/contracts/tokenomics/treasury/.cargo/schema/instantiate_msg.json index cdd51d4c..9b394b78 100644 --- a/contracts/tokenomics/treasury/.cargo/schema/instantiate_msg.json +++ b/contracts/tokenomics/treasury/.cargo/schema/instantiate_msg.json @@ -8,7 +8,8 @@ "distribution_rate", "min_period", "owner", - "reserve_contract" + "reserve_contract", + "vesting_denominator" ], "properties": { "denom": { @@ -38,6 +39,12 @@ "reserve_contract": { "description": "Address of reserve contract", "type": "string" + }, + "vesting_denominator": { + "description": "Vesting release equation denominator", + "type": "integer", + "format": "uint128", + "minimum": 0.0 } }, "definitions": { diff --git a/contracts/tokenomics/treasury/Cargo.toml b/contracts/tokenomics/treasury/Cargo.toml index 261711e8..f2911b55 100644 --- a/contracts/tokenomics/treasury/Cargo.toml +++ b/contracts/tokenomics/treasury/Cargo.toml @@ -13,6 +13,7 @@ crate-type = ["cdylib", "rlib"] backtraces = ["cosmwasm-std/backtraces"] [dependencies] +neutron_bindings = { package = "neutron-sdk", git = "https://github.com/neutron-org/neutron-sdk.git" } cosmwasm-std = { version = "1.0" } cw-storage-plus = "1.0.1" schemars = "0.8.1" diff --git a/contracts/tokenomics/treasury/schema/execute_msg.json b/contracts/tokenomics/treasury/schema/execute_msg.json index 1370cc07..2bb9c2cc 100644 --- a/contracts/tokenomics/treasury/schema/execute_msg.json +++ b/contracts/tokenomics/treasury/schema/execute_msg.json @@ -72,6 +72,14 @@ "string", "null" ] + }, + "vesting_denominator": { + "type": [ + "integer", + "null" + ], + "format": "uint128", + "minimum": 0.0 } } } diff --git a/contracts/tokenomics/treasury/schema/instantiate_msg.json b/contracts/tokenomics/treasury/schema/instantiate_msg.json index 3b7a745e..34b790f2 100644 --- a/contracts/tokenomics/treasury/schema/instantiate_msg.json +++ b/contracts/tokenomics/treasury/schema/instantiate_msg.json @@ -9,7 +9,8 @@ "main_dao_address", "min_period", "reserve_contract", - "security_dao_address" + "security_dao_address", + "vesting_denominator" ], "properties": { "denom": { @@ -43,6 +44,12 @@ "security_dao_address": { "description": "Address of security DAO contract", "type": "string" + }, + "vesting_denominator": { + "description": "Vesting release function denominator", + "type": "integer", + "format": "uint128", + "minimum": 0.0 } }, "definitions": { diff --git a/contracts/tokenomics/treasury/src/contract.rs b/contracts/tokenomics/treasury/src/contract.rs index 99ef46ca..2038f592 100644 --- a/contracts/tokenomics/treasury/src/contract.rs +++ b/contracts/tokenomics/treasury/src/contract.rs @@ -1,3 +1,4 @@ +use crate::distribution_params::DistributionParams; use crate::error::ContractError; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; @@ -8,11 +9,15 @@ use cosmwasm_std::{ use exec_control::pause::{ can_pause, can_unpause, validate_duration, PauseError, PauseInfoResponse, }; +use neutron_bindings::bindings::query::InterchainQueries; use crate::msg::{DistributeMsg, ExecuteMsg, InstantiateMsg, QueryMsg, StatsResponse}; use crate::state::{ - Config, CONFIG, LAST_DISTRIBUTION_TIME, PAUSED_UNTIL, TOTAL_DISTRIBUTED, TOTAL_RECEIVED, - TOTAL_RESERVED, + Config, CONFIG, LAST_BURNED_COINS_AMOUNT, LAST_DISTRIBUTION_TIME, PAUSED_UNTIL, + TOTAL_DISTRIBUTED, TOTAL_RESERVED, +}; +use crate::vesting::{ + get_burned_coins, safe_burned_coins_for_period, update_distribution_stats, vesting_function, }; //-------------------------------------------------------------------------------------------------- @@ -21,11 +26,23 @@ use crate::state::{ #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( - deps: DepsMut, + deps: DepsMut, _env: Env, _info: MessageInfo, msg: InstantiateMsg, -) -> StdResult { +) -> Result { + if (msg.distribution_rate > Decimal::one()) || (msg.distribution_rate < Decimal::zero()) { + return Err(ContractError::InvalidDistributionRate( + "distribution_rate must be between 0 and 1".to_string(), + )); + } + + if msg.vesting_denominator == 0 { + return Err(ContractError::InvalidVestingDenominator( + "vesting_denominator must be more than zero".to_string(), + )); + } + let config = Config { denom: msg.denom, min_period: msg.min_period, @@ -34,19 +51,20 @@ pub fn instantiate( distribution_rate: msg.distribution_rate, main_dao_address: deps.api.addr_validate(&msg.main_dao_address)?, security_dao_address: deps.api.addr_validate(&msg.security_dao_address)?, + vesting_denominator: msg.vesting_denominator, }; CONFIG.save(deps.storage, &config)?; - TOTAL_RECEIVED.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)?; PAUSED_UNTIL.save(deps.storage, &None)?; + LAST_BURNED_COINS_AMOUNT.save(deps.storage, &Uint128::zero())?; Ok(Response::new()) } pub fn execute_pause( - deps: DepsMut, + deps: DepsMut, env: Env, sender: Addr, duration: u64, @@ -77,7 +95,10 @@ pub fn execute_pause( .add_attribute("paused_until_height", paused_until_height.to_string())) } -pub fn execute_unpause(deps: DepsMut, sender: Addr) -> Result { +pub fn execute_unpause( + deps: DepsMut, + sender: Addr, +) -> Result { let config: Config = CONFIG.load(deps.storage)?; can_unpause(&sender, &config.main_dao_address)?; @@ -89,7 +110,7 @@ pub fn execute_unpause(deps: DepsMut, sender: Addr) -> Result StdResult { +fn get_pause_info(deps: Deps, env: &Env) -> StdResult { Ok(match PAUSED_UNTIL.may_load(deps.storage)?.unwrap_or(None) { Some(paused_until_height) => { if env.block.height.ge(&paused_until_height) { @@ -110,7 +131,7 @@ fn get_pause_info(deps: Deps, env: &Env) -> StdResult { #[cfg_attr(not(feature = "library"), entry_point)] pub fn execute( - deps: DepsMut, + deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, @@ -143,14 +164,18 @@ pub fn execute( distribution_contract, reserve_contract, security_dao_address, + vesting_denominator, } => execute_update_config( deps, info, - distribution_rate, - min_period, distribution_contract, reserve_contract, security_dao_address, + DistributionParams { + distribution_rate, + min_period, + vesting_denominator, + }, ), ExecuteMsg::Pause { duration } => execute_pause(deps, env, info.sender, duration), ExecuteMsg::Unpause {} => execute_unpause(deps, info.sender), @@ -158,7 +183,7 @@ pub fn execute( } pub fn execute_transfer_ownership( - deps: DepsMut, + deps: DepsMut, info: MessageInfo, new_owner_addr: Addr, ) -> Result { @@ -181,20 +206,19 @@ pub fn execute_transfer_ownership( } pub fn execute_update_config( - deps: DepsMut, + deps: DepsMut, info: MessageInfo, - distribution_rate: Option, - min_period: Option, distribution_contract: Option, reserve_contract: Option, security_dao_address: Option, + distribution_params: DistributionParams, ) -> Result { let mut config: Config = CONFIG.load(deps.storage)?; if info.sender != config.main_dao_address { return Err(ContractError::Unauthorized {}); } - if let Some(min_period) = min_period { + if let Some(min_period) = distribution_params.min_period { config.min_period = min_period; } if let Some(distribution_contract) = distribution_contract { @@ -206,7 +230,7 @@ pub fn execute_update_config( if let Some(security_dao_address) = security_dao_address { config.security_dao_address = deps.api.addr_validate(security_dao_address.as_str())?; } - if let Some(distribution_rate) = distribution_rate { + if let Some(distribution_rate) = distribution_params.distribution_rate { if (distribution_rate > Decimal::one()) || (distribution_rate < Decimal::zero()) { return Err(ContractError::InvalidDistributionRate( "distribution_rate must be between 0 and 1".to_string(), @@ -214,6 +238,14 @@ pub fn execute_update_config( } config.distribution_rate = distribution_rate; } + if let Some(vesting_denominator) = distribution_params.vesting_denominator { + if vesting_denominator == 0 { + return Err(ContractError::InvalidVestingDenominator( + "vesting_denominator must be more than zero".to_string(), + )); + } + config.vesting_denominator = vesting_denominator; + } CONFIG.save(deps.storage, &config)?; @@ -223,12 +255,19 @@ pub fn execute_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( + "vesting_denominator", + config.vesting_denominator.to_string(), + ) .add_attribute("owner", config.main_dao_address)) } -pub fn execute_distribute(deps: DepsMut, env: Env) -> Result { +pub fn execute_distribute( + deps: DepsMut, + env: Env, +) -> Result { let config: Config = CONFIG.load(deps.storage)?; - let denom = config.denom; + let denom = config.denom.clone(); let current_time = env.block.time.seconds(); if current_time - LAST_DISTRIBUTION_TIME.load(deps.storage)? < config.min_period { return Err(ContractError::TooSoonToDistribute {}); @@ -243,40 +282,33 @@ pub fn execute_distribute(deps: DepsMut, env: Env) -> Result Result 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)?), @@ -296,23 +328,54 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { } } -pub fn query_paused(deps: Deps, env: Env) -> StdResult { +pub fn query_paused(deps: Deps, env: Env) -> StdResult { to_binary(&get_pause_info(deps, &env)?) } -pub fn query_config(deps: Deps) -> StdResult { +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)?; +pub fn query_stats(deps: Deps) -> StdResult { let total_distributed = TOTAL_DISTRIBUTED.load(deps.storage)?; let total_reserved = TOTAL_RESERVED.load(deps.storage)?; + let total_processed_burned_coins = LAST_BURNED_COINS_AMOUNT.load(deps.storage)?; Ok(StatsResponse { - total_received, total_distributed, total_reserved, + total_processed_burned_coins, }) } + +//-------------------------------------------------------------------------------------------------- +// Helpers +//-------------------------------------------------------------------------------------------------- + +pub fn create_distribution_response( + config: Config, + to_distribute: Uint128, + to_reserve: Uint128, + denom: String, +) -> StdResult { + 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.clone()), + msg: to_binary(&DistributeMsg::Fund {})?, + }); + 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) +} diff --git a/contracts/tokenomics/treasury/src/distribution_params.rs b/contracts/tokenomics/treasury/src/distribution_params.rs new file mode 100644 index 00000000..6125e489 --- /dev/null +++ b/contracts/tokenomics/treasury/src/distribution_params.rs @@ -0,0 +1,7 @@ +use cosmwasm_std::Decimal; + +pub struct DistributionParams { + pub distribution_rate: Option, + pub min_period: Option, + pub vesting_denominator: Option, +} diff --git a/contracts/tokenomics/treasury/src/error.rs b/contracts/tokenomics/treasury/src/error.rs index 9654c4df..726e4fbc 100644 --- a/contracts/tokenomics/treasury/src/error.rs +++ b/contracts/tokenomics/treasury/src/error.rs @@ -19,9 +19,15 @@ pub enum ContractError { #[error("Invalid distribution rate")] InvalidDistributionRate(String), + #[error("Invalid vesting denominator")] + InvalidVestingDenominator(String), + #[error("Too soon to distribute")] TooSoonToDistribute {}, + #[error("no coins were burned, nothing to distribute")] + NoBurnedCoins {}, + #[error("Overflow")] OverflowError(#[from] OverflowError), } diff --git a/contracts/tokenomics/treasury/src/lib.rs b/contracts/tokenomics/treasury/src/lib.rs index 23cbb03f..273900d1 100644 --- a/contracts/tokenomics/treasury/src/lib.rs +++ b/contracts/tokenomics/treasury/src/lib.rs @@ -1,6 +1,9 @@ pub mod contract; +mod distribution_params; mod error; pub mod msg; pub mod state; +pub mod vesting; + #[cfg(test)] mod testing; diff --git a/contracts/tokenomics/treasury/src/msg.rs b/contracts/tokenomics/treasury/src/msg.rs index 72fb32d4..4214a3c2 100644 --- a/contracts/tokenomics/treasury/src/msg.rs +++ b/contracts/tokenomics/treasury/src/msg.rs @@ -17,6 +17,8 @@ pub struct InstantiateMsg { pub reserve_contract: String, /// Address of security DAO contract pub security_dao_address: String, + /// Vesting release function denominator + pub vesting_denominator: u128, } #[pausable] @@ -36,6 +38,7 @@ pub enum ExecuteMsg { distribution_contract: Option, reserve_contract: Option, security_dao_address: Option, + vesting_denominator: Option, }, } @@ -51,9 +54,9 @@ pub enum QueryMsg { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct StatsResponse { - pub total_received: Uint128, pub total_distributed: Uint128, pub total_reserved: Uint128, + pub total_processed_burned_coins: Uint128, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] diff --git a/contracts/tokenomics/treasury/src/state.rs b/contracts/tokenomics/treasury/src/state.rs index 2ec95c0c..3430fd98 100644 --- a/contracts/tokenomics/treasury/src/state.rs +++ b/contracts/tokenomics/treasury/src/state.rs @@ -20,12 +20,15 @@ pub struct Config { /// Address of the security DAO contract pub security_dao_address: Addr, + + // Denomintator used int the vesting release function + pub vesting_denominator: u128, } -pub const TOTAL_RECEIVED: Item = Item::new("total_received"); pub const TOTAL_DISTRIBUTED: Item = Item::new("total_distributed"); pub const TOTAL_RESERVED: Item = Item::new("total_reserved"); +pub const LAST_BURNED_COINS_AMOUNT: Item = Item::new("last_burned_coins_amount"); pub const LAST_DISTRIBUTION_TIME: Item = Item::new("last_grab_time"); pub const CONFIG: Item = Item::new("config"); diff --git a/contracts/tokenomics/treasury/src/testing/mock_querier.rs b/contracts/tokenomics/treasury/src/testing/mock_querier.rs index df2eaa1f..3ec8f14a 100644 --- a/contracts/tokenomics/treasury/src/testing/mock_querier.rs +++ b/contracts/tokenomics/treasury/src/testing/mock_querier.rs @@ -1,17 +1,24 @@ use std::marker::PhantomData; use cosmwasm_std::{ + from_slice, testing::{MockApi, MockQuerier, MockStorage}, - Coin, OwnedDeps, + to_binary, Binary, Coin, ContractResult, OwnedDeps, Querier, QuerierResult, QueryRequest, + SystemError, SystemResult, +}; +use neutron_bindings::{ + bindings::query::InterchainQueries, + query::total_burned_neutrons::TotalBurnedNeutronsAmountResponse, }; const MOCK_CONTRACT_ADDR: &str = "cosmos2contract"; pub fn mock_dependencies( contract_balance: &[Coin], -) -> OwnedDeps { +) -> OwnedDeps { let contract_addr = MOCK_CONTRACT_ADDR; - let custom_querier = MockQuerier::new(&[(contract_addr, contract_balance)]); + let custom_querier: WasmMockQuerier = + WasmMockQuerier::new(MockQuerier::new(&[(contract_addr, contract_balance)])); OwnedDeps { storage: MockStorage::default(), @@ -20,3 +27,54 @@ pub fn mock_dependencies( custom_query_type: PhantomData, } } + +pub struct WasmMockQuerier { + base: MockQuerier, + total_burned_neutrons: Binary, + throw_error: bool, +} + +impl Querier for WasmMockQuerier { + fn raw_query(&self, bin_request: &[u8]) -> QuerierResult { + let request: QueryRequest = match from_slice(bin_request) { + Ok(v) => v, + Err(e) => { + return QuerierResult::Err(SystemError::InvalidRequest { + error: format!("Parsing query request: {}", e), + request: bin_request.into(), + }); + } + }; + self.handle_query(&request) + } +} + +impl WasmMockQuerier { + pub fn new(base: MockQuerier) -> Self { + WasmMockQuerier { + base, + total_burned_neutrons: to_binary(&Vec::::with_capacity(0)).unwrap(), + throw_error: false, + } + } + + pub fn handle_query(&self, request: &QueryRequest) -> QuerierResult { + match &request { + QueryRequest::Custom(InterchainQueries::TotalBurnedNeutronsAmount {}) => { + if self.throw_error { + return SystemResult::Ok(ContractResult::Err("Contract error".to_string())); + } + SystemResult::Ok(ContractResult::Ok(self.total_burned_neutrons.clone())) + } + _ => self.base.handle_query(request), + } + } + + pub fn set_total_burned_neutrons(&mut self, coin: Coin) { + self.total_burned_neutrons = to_binary(&TotalBurnedNeutronsAmountResponse { coin }).unwrap() + } + + pub fn set_total_burned_neutrons_error(&mut self, error_state: bool) { + self.throw_error = error_state + } +} diff --git a/contracts/tokenomics/treasury/src/testing/mod.rs b/contracts/tokenomics/treasury/src/testing/mod.rs index a1e507b6..6e26792e 100644 --- a/contracts/tokenomics/treasury/src/testing/mod.rs +++ b/contracts/tokenomics/treasury/src/testing/mod.rs @@ -1,2 +1,2 @@ -mod mock_querier; +pub mod mock_querier; mod tests; diff --git a/contracts/tokenomics/treasury/src/testing/tests.rs b/contracts/tokenomics/treasury/src/testing/tests.rs index 44647ab2..e40a87fe 100644 --- a/contracts/tokenomics/treasury/src/testing/tests.rs +++ b/contracts/tokenomics/treasury/src/testing/tests.rs @@ -3,9 +3,10 @@ use std::str::FromStr; use cosmwasm_std::{ coin, coins, from_binary, testing::{mock_env, mock_info}, - to_binary, BankMsg, Coin, CosmosMsg, Decimal, DepsMut, Empty, Uint128, WasmMsg, + to_binary, BankMsg, Coin, CosmosMsg, Decimal, DepsMut, StdError, Uint128, WasmMsg, }; use exec_control::pause::{PauseError, PauseInfoResponse}; +use neutron_bindings::bindings::query::InterchainQueries; use crate::contract::query; use crate::error::ContractError; @@ -13,13 +14,15 @@ use crate::msg::QueryMsg; use crate::{ contract::{execute, instantiate}, msg::{DistributeMsg, ExecuteMsg, InstantiateMsg}, - state::{CONFIG, TOTAL_DISTRIBUTED, TOTAL_RECEIVED, TOTAL_RESERVED}, + state::{ + CONFIG, LAST_BURNED_COINS_AMOUNT, LAST_DISTRIBUTION_TIME, TOTAL_DISTRIBUTED, TOTAL_RESERVED, + }, testing::mock_querier::mock_dependencies, }; const DENOM: &str = "denom"; -pub fn init_base_contract(deps: DepsMut, distribution_rate: &str) { +pub fn init_base_contract(deps: DepsMut, distribution_rate: &str) { let msg = InstantiateMsg { denom: DENOM.to_string(), min_period: 1000, @@ -28,6 +31,7 @@ pub fn init_base_contract(deps: DepsMut, distribution_rate: &str) { distribution_rate: Decimal::from_str(distribution_rate).unwrap(), main_dao_address: "main_dao".to_string(), security_dao_address: "security_dao_address".to_string(), + vesting_denominator: 100_000_000_000u128, }; let info = mock_info("creator", &coins(2, DENOM)); instantiate(deps, mock_env(), info, msg).unwrap(); @@ -120,20 +124,12 @@ fn test_pause() { assert_eq!(pause_info, PauseInfoResponse::Unpaused {}); } -#[test] -fn test_collect_with_no_money() { - let mut deps = mock_dependencies(&[]); - 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()); - assert_eq!(res.unwrap_err(), ContractError::NoFundsToDistribute {}); -} - #[test] fn test_distribute_success() { let mut deps = mock_dependencies(&[coin(1000000, DENOM)]); init_base_contract(deps.as_mut(), "0.23"); + deps.querier + .set_total_burned_neutrons(coin(10000000, DENOM)); let msg = ExecuteMsg::Distribute {}; let res = execute(deps.as_mut(), mock_env(), mock_info("anyone", &[]), msg); assert!(res.is_ok()); @@ -145,7 +141,7 @@ fn test_distribute_success() { contract_addr: "distribution_contract".to_string(), funds: vec![Coin { denom: DENOM.to_string(), - amount: Uint128::from(230000u128) + amount: Uint128::from(23u128) }], msg: to_binary(&DistributeMsg::Fund {}).unwrap(), }) @@ -156,22 +152,124 @@ fn test_distribute_success() { to_address: "reserve_contract".to_string(), amount: vec![Coin { denom: DENOM.to_string(), - amount: Uint128::from(770000u128) + amount: Uint128::from(77u128) }] }) ); - 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)); + assert_eq!(total_reserved, Uint128::from(77u128)); let total_distributed = TOTAL_DISTRIBUTED.load(deps.as_ref().storage).unwrap(); - assert_eq!(total_distributed, Uint128::from(230000u128)); + assert_eq!(total_distributed, Uint128::from(23u128)); +} + +#[test] +fn test_burned_maximim_limit() { + let mut deps = mock_dependencies(&[coin(1000000, DENOM)]); + init_base_contract(deps.as_mut(), "0.23"); + deps.querier + .set_total_burned_neutrons(coin(u32::MAX.into(), DENOM)); + let msg = ExecuteMsg::Distribute {}; + execute(deps.as_mut(), mock_env(), mock_info("anyone", &[]), msg).unwrap(); + + let total_reserved = TOTAL_RESERVED.load(deps.as_ref().storage).unwrap(); + assert_eq!(total_reserved, Uint128::from(32372u128)); + let total_distributed = TOTAL_DISTRIBUTED.load(deps.as_ref().storage).unwrap(); + assert_eq!(total_distributed, Uint128::from(9669u128)); + let total_processed_burned_coins = LAST_BURNED_COINS_AMOUNT + .load(deps.as_ref().storage) + .unwrap(); + assert_eq!(total_processed_burned_coins, Uint128::from(u32::MAX)); +} + +#[test] +fn test_burned_maximim_limit_overflow() { + let mut deps = mock_dependencies(&[coin(1000000, DENOM)]); + init_base_contract(deps.as_mut(), "0.23"); + + let total_burned_neutrons = u128::from(u32::MAX) + 10000u128; + + deps.querier + .set_total_burned_neutrons(coin(total_burned_neutrons, DENOM)); + let msg = ExecuteMsg::Distribute {}; + execute( + deps.as_mut(), + mock_env(), + mock_info("anyone", &[]), + msg.clone(), + ) + .unwrap(); + + let total_reserved = TOTAL_RESERVED.load(deps.as_ref().storage).unwrap(); + assert_eq!(total_reserved, Uint128::from(32372u128)); + let total_distributed = TOTAL_DISTRIBUTED.load(deps.as_ref().storage).unwrap(); + assert_eq!(total_distributed, Uint128::from(9669u128)); + let total_processed_burned_coins = LAST_BURNED_COINS_AMOUNT + .load(deps.as_ref().storage) + .unwrap(); + + // Should process only u32::MAX coins to protect from overflow + assert_eq!(total_processed_burned_coins, Uint128::from(u32::MAX)); + + LAST_DISTRIBUTION_TIME + .save(deps.as_mut().storage, &0) + .unwrap(); + + // On second call should process rest of the coins + execute(deps.as_mut(), mock_env(), mock_info("anyone", &[]), msg).unwrap(); + let total_processed_burned_coins = LAST_BURNED_COINS_AMOUNT + .load(deps.as_ref().storage) + .unwrap(); + assert_eq!( + total_processed_burned_coins, + Uint128::from(total_burned_neutrons) + ); +} + +#[test] +fn test_collect_with_no_money() { + let mut deps = mock_dependencies(&[]); + 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()); + assert_eq!(res.unwrap_err(), ContractError::NoFundsToDistribute {}); +} + +#[test] +fn test_no_burned_coins_with_denom_error() { + let mut deps = mock_dependencies(&[coin(1000000, DENOM)]); + init_base_contract(deps.as_mut(), "0.0"); + deps.querier.set_total_burned_neutrons_error(true); + let msg = ExecuteMsg::Distribute {}; + let res = execute(deps.as_mut(), mock_env(), mock_info("anyone", &[]), msg); + + assert!(res.is_err()); + assert_eq!( + res.unwrap_err(), + ContractError::Std(StdError::generic_err( + "Generic error: Querier contract error: Contract error" + )) + ); +} + +#[test] +fn test_no_burned_coins_for_period_error() { + let mut deps = mock_dependencies(&[coin(1000000, DENOM)]); + init_base_contract(deps.as_mut(), "0.0"); + deps.querier.set_total_burned_neutrons(coin(0, DENOM)); + let msg = ExecuteMsg::Distribute {}; + let res = execute(deps.as_mut(), mock_env(), mock_info("anyone", &[]), msg); + + assert!(res.is_err()); + assert_eq!(res.unwrap_err(), ContractError::NoBurnedCoins {}); } #[test] fn test_distribute_zero_to_reserve() { let mut deps = mock_dependencies(&[coin(1000000, DENOM)]); init_base_contract(deps.as_mut(), "1"); + deps.querier + .set_total_burned_neutrons(coin(10000000, DENOM)); let msg = ExecuteMsg::Distribute {}; let res = execute(deps.as_mut(), mock_env(), mock_info("anyone", &[]), msg); assert!(res.is_ok()); @@ -183,24 +281,24 @@ fn test_distribute_zero_to_reserve() { contract_addr: "distribution_contract".to_string(), funds: vec![Coin { denom: DENOM.to_string(), - amount: Uint128::from(1000000u128) + amount: Uint128::from(100u128) }], 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)); + assert_eq!(total_distributed, Uint128::from(100u128)); } #[test] fn test_distribute_zero_to_distribution_contract() { let mut deps = mock_dependencies(&[coin(1000000, DENOM)]); init_base_contract(deps.as_mut(), "0"); + deps.querier + .set_total_burned_neutrons(coin(10000000, DENOM)); let msg = ExecuteMsg::Distribute {}; let res = execute(deps.as_mut(), mock_env(), mock_info("anyone", &[]), msg); assert!(res.is_ok()); @@ -212,14 +310,12 @@ fn test_distribute_zero_to_distribution_contract() { to_address: "reserve_contract".to_string(), amount: vec![Coin { denom: DENOM.to_string(), - amount: Uint128::from(1000000u128) + amount: Uint128::from(100u128) }] }) ); - 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)); + assert_eq!(total_reserved, Uint128::from(100u128)); let total_distributed = TOTAL_DISTRIBUTED.load(deps.as_ref().storage).unwrap(); assert_eq!(total_distributed, Uint128::from(0u128)); } @@ -232,6 +328,7 @@ fn test_update_config_unauthorized() { distribution_contract: None, reserve_contract: None, distribution_rate: None, + vesting_denominator: None, min_period: None, security_dao_address: None, }; @@ -248,6 +345,7 @@ fn test_update_config_success() { distribution_contract: Some("new_contract".to_string()), reserve_contract: Some("new_reserve_contract".to_string()), distribution_rate: Some(Decimal::from_str("0.11").unwrap()), + vesting_denominator: Some(1000u128), min_period: Some(3000), security_dao_address: Some("security_dao_address_contract".to_string()), }; @@ -257,6 +355,7 @@ fn test_update_config_success() { assert_eq!(config.distribution_contract, "new_contract"); assert_eq!(config.reserve_contract, "new_reserve_contract"); assert_eq!(config.distribution_rate, Decimal::from_str("0.11").unwrap()); + assert_eq!(config.vesting_denominator, 1000u128); assert_eq!(config.min_period, 3000); assert_eq!(config.security_dao_address, "security_dao_address_contract") } @@ -269,6 +368,7 @@ fn test_update_distribution_rate_below_the_limit() { distribution_contract: None, reserve_contract: None, distribution_rate: Some(Decimal::from_str("2").unwrap()), + vesting_denominator: None, min_period: None, security_dao_address: None, }; diff --git a/contracts/tokenomics/treasury/src/vesting.rs b/contracts/tokenomics/treasury/src/vesting.rs new file mode 100644 index 00000000..ceb0c734 --- /dev/null +++ b/contracts/tokenomics/treasury/src/vesting.rs @@ -0,0 +1,285 @@ +use cosmwasm_std::{Decimal, Deps, DepsMut, StdError, StdResult, Uint128}; +use neutron_bindings::{ + bindings::query::InterchainQueries, query::total_burned_neutrons::query_total_burned_neutrons, +}; + +use crate::state::{LAST_BURNED_COINS_AMOUNT, TOTAL_DISTRIBUTED, TOTAL_RESERVED}; + +/// Function calculates how many coins should be released for the current period +/// based on the current balance and the number of coins burned for the period +/// Implemented vesting function is linear and is defined as: y=x/vesting_denominator +/// In order to optimize the function, we use the following formula: y=x - ((vesting_denominator-1) / vesting_denominator)^ * x +pub fn vesting_function( + current_balance: Uint128, + burned_coins_for_period: u32, + vesting_denominator: u128, +) -> StdResult { + if current_balance.is_zero() || burned_coins_for_period == 0 { + return Ok(Uint128::zero()); + } + + let current_balance = Decimal::from_atomics(current_balance, 0).map_err(|err| { + StdError::generic_err(format!("Unable to convert Uint128 to Decimal. {:?}", err)) + })?; + + let multiplier = Decimal::from_ratio(vesting_denominator - 1, vesting_denominator) // vesting_denominator-1 / vesting_denominator + .checked_pow(burned_coins_for_period)?; // ^ + + let coins_left = multiplier.checked_mul(current_balance)?; // (vesting_denominator-1 / vesting_denominator)^ * x + + let rounded = current_balance.checked_sub(coins_left)?.ceil(); + + Uint128::try_from(rounded.to_string().as_str()) +} + +pub fn safe_burned_coins_for_period( + burned_coins: Uint128, + last_burned_coins: Uint128, +) -> StdResult { + let burned_coins_for_period = burned_coins.checked_sub(last_burned_coins)?; + + if burned_coins_for_period > Uint128::from(u32::MAX) { + return Ok(u32::MAX); + } + + u32::try_from(burned_coins_for_period.u128()).map_err(|_err| { + StdError::generic_err("Burned coins amount for period is too big to be converted to u32") + }) +} + +pub fn update_distribution_stats( + deps: DepsMut, + to_distribute: Uint128, + to_reserve: Uint128, + burned_coins: Uint128, +) -> StdResult<()> { + // update stats + 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_BURNED_COINS_AMOUNT.save(deps.storage, &burned_coins)?; + + Ok(()) +} + +pub fn get_burned_coins(deps: Deps, denom: &String) -> StdResult { + let res = + query_total_burned_neutrons(deps).map_err(|err| StdError::generic_err(err.to_string()))?; + + if res.coin.denom == *denom { + return Ok(res.coin.amount); + } + + Err(StdError::not_found("Burned coins")) +} + +#[cfg(test)] +mod test { + use std::str::FromStr; + + use cosmwasm_std::coin; + + use crate::testing::mock_querier::mock_dependencies; + + use super::*; + + const DENOM: &str = "denom"; + + #[test] + fn test_safe_burned_coins_for_period() { + assert_eq!( + safe_burned_coins_for_period(Uint128::from(100u128), Uint128::zero()).unwrap(), + 100u32 + ); + + assert_eq!( + safe_burned_coins_for_period(Uint128::from(100u128), Uint128::from(50u128)).unwrap(), + 50u32 + ); + + assert_eq!( + safe_burned_coins_for_period(Uint128::from(u32::MAX), Uint128::zero()).unwrap(), + u32::MAX + ); + + assert_eq!( + safe_burned_coins_for_period(Uint128::from_str("5294967295").unwrap(), Uint128::zero()) + .unwrap(), + u32::MAX + ); + + assert_eq!( + safe_burned_coins_for_period( + Uint128::from_str("5294967295").unwrap(), + Uint128::from(u32::MAX) + ) + .unwrap(), + 1000000000u32 + ); + + assert_eq!( + safe_burned_coins_for_period( + Uint128::from_str("5294967295").unwrap(), + Uint128::from(1000u32) + ) + .unwrap(), + u32::MAX + ); + } + + #[test] + fn test_get_burned_coins_single_coin() { + let mut deps = mock_dependencies(&[coin(1000000, DENOM)]); + + deps.querier.set_total_burned_neutrons(coin(100, DENOM)); + + let burned_tokens = get_burned_coins(deps.as_ref(), &DENOM.to_string()).unwrap(); + assert_eq!(burned_tokens, Uint128::from(100u128)); + } + + #[test] + fn test_get_burned_coins_not_supported_denom() { + let mut deps = mock_dependencies(&[coin(1000000, DENOM)]); + + deps.querier + .set_total_burned_neutrons(coin(100, "custom_denom")); + + let burned_tokens = get_burned_coins(deps.as_ref(), &DENOM.to_string()); + assert_eq!( + burned_tokens.err(), + Some(StdError::not_found("Burned coins")) + ); + } + + #[test] + fn test_get_burned_coins_with_query_error() { + let mut deps = mock_dependencies(&[coin(1000000, DENOM)]); + + deps.querier.set_total_burned_neutrons_error(true); + + let burned_tokens = get_burned_coins(deps.as_ref(), &DENOM.to_string()); + assert_eq!( + burned_tokens.err(), + Some(StdError::generic_err( + "Generic error: Querier contract error: Contract error" + )) + ); + + deps.querier.set_total_burned_neutrons_error(false); + } + + #[test] + fn test_vesting_function_return_value() { + assert_eq!( + vesting_function(Uint128::new(100), 1, 2u128).unwrap(), + Uint128::new(50), + ); + + assert_eq!( + vesting_function(Uint128::new(100), 2, 3u128).unwrap(), + Uint128::new(56) + ); + + assert_eq!( + vesting_function(Uint128::new(100), 4, 4u128).unwrap(), + Uint128::new(69) + ); + + assert_eq!( + vesting_function( + Uint128::new(20_000_000), + 4_294_967_295, // u64::MAX + 100_000_000_000u128 + ) + .unwrap(), + Uint128::new(840808) + ); + + assert_eq!( + vesting_function(Uint128::new(1000000000), 4000000, 100_000_000_000u128).unwrap(), + Uint128::new(40000) + ); + + assert_eq!( + vesting_function(Uint128::new(100000000), 66666666, 100_000_000_000u128).unwrap(), + Uint128::new(66645) + ); + + assert_eq!( + vesting_function(Uint128::new(441978163), 10000000, 100_000_000_000u128).unwrap(), + Uint128::new(44196) + ); + + assert_eq!( + vesting_function(Uint128::new(441978163), 66758565, 100_000_000_000u128).unwrap(), + Uint128::new(294960) + ); + + assert_eq!( + vesting_function(Uint128::new(441978163), 18989885, 100_000_000_000u128).unwrap(), + Uint128::new(83924) + ); + + assert_eq!( + vesting_function( + Uint128::from_str("441978163000").unwrap(), + 441978163, + 100_000_000_000u128 + ) + .unwrap(), + Uint128::new(1949136417) + ); + + assert_eq!( + vesting_function( + Uint128::from_str("20000000000000").unwrap(), + 2_000_000_000, + 100_000_000_000u128 + ) + .unwrap(), + Uint128::from_str("396026534292").unwrap() + ); + } + + #[test] + fn test_vesting_full_consumption_simulation() { + let current_balance = Uint128::from_str("20000000000000").unwrap(); + + let avg_burned_coins_per_block = Uint128::new(5_000_000); + let total_blocks = Uint128::new(1000); + let mut total_burned_coins = avg_burned_coins_per_block + .checked_mul(total_blocks) + .unwrap(); + + let mut total_vested_coins = Uint128::zero(); + + while total_burned_coins > Uint128::from(u32::MAX) { + total_vested_coins += vesting_function( + current_balance - total_vested_coins, + u32::MAX, + 100_000_000_000u128, + ) + .unwrap(); + + total_burned_coins -= Uint128::from(u32::MAX); + } + let burned_coins_left = u32::try_from(total_burned_coins.u128()).unwrap(); + + total_vested_coins += vesting_function( + current_balance - total_vested_coins, + burned_coins_left, + 100_000_000_000u128, + ) + .unwrap(); + + assert_eq!( + total_vested_coins, + Uint128::from_str("975411511021").unwrap() + ); + } +}