diff --git a/CHANGELOG.md b/CHANGELOG.md index 48321f57c6..4b4e5d91ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// - native-client/socks5-client: `use_extended_packet_size` Debug config option to make the client use 'ExtendedPacketSize' for its traffic (32kB as opposed to 2kB in 1.0.2) ([#1671]) - wasm-client: uses updated wasm-compatible `client-core` so that it's now capable of packet retransmission, cover traffic and poisson delay (among other things!) ([#1673]) - validator-api: add `interval_operating_cost` and `profit_margin_percent` to cmpute reward estimation endpoint +- vesting-contract: optional locked token pledge cap per account ([#1687]), defaults to 100_000 NYM ### Fixed @@ -26,6 +27,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// - socks5 client: graceful shutdown should fix error on disconnect in nym-connect ([#1591]) - wasm-client: fixed build errors on MacOS and changed example JS code to use mainnet ([#1585]) - gateway-client: will attempt to read now as many as 8 websocket messages at once, assuming they're already available on the socket ([#1669]) +- moved `Percent` struct to to `contracts-common`, change affects explorer-api [#1541]: https://github.com/nymtech/nym/pull/1541 [#1558]: https://github.com/nymtech/nym/pull/1558 @@ -39,6 +41,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// [#1669]: https://github.com/nymtech/nym/pull/1669 [#1671]: https://github.com/nymtech/nym/pull/1671 [#1673]: https://github.com/nymtech/nym/pull/1673 +[#1687]: https://github.com/nymtech/nym/pull/1687 ## [nym-binaries-1.0.2](https://github.com/nymtech/nym/tree/nym-binaries-1.0.2) diff --git a/Cargo.lock b/Cargo.lock index 627cb1f2b4..bfe183e51c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -737,7 +737,9 @@ name = "contracts-common" version = "0.1.0" dependencies = [ "cosmwasm-std", + "schemars", "serde", + "thiserror", ] [[package]] @@ -1584,6 +1586,7 @@ dependencies = [ "chrono", "clap 3.2.8", "dotenv", + "contracts-common", "humantime-serde", "isocountry", "itertools", @@ -3349,6 +3352,7 @@ dependencies = [ "coconut-interface", "config", "console-subscriber", + "contracts-common", "cosmwasm-std", "credential-storage", "credentials", @@ -6474,6 +6478,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" name = "vesting-contract" version = "1.1.0" dependencies = [ + "contracts-common", "cosmwasm-std", "cw-storage-plus", "mixnet-contract-common", @@ -6487,7 +6492,9 @@ dependencies = [ name = "vesting-contract-common" version = "0.1.0" dependencies = [ + "contracts-common", "cosmwasm-std", + "log", "mixnet-contract-common", "schemars", "serde", diff --git a/common/client-libs/validator-client/src/nymd/mod.rs b/common/client-libs/validator-client/src/nymd/mod.rs index 151bb30b23..481cb021be 100644 --- a/common/client-libs/validator-client/src/nymd/mod.rs +++ b/common/client-libs/validator-client/src/nymd/mod.rs @@ -15,14 +15,12 @@ use cosmrs::rpc::query::Query; use cosmrs::rpc::Error as TendermintRpcError; use cosmrs::rpc::HttpClientUrl; use cosmrs::tx::Msg; -use cosmwasm_std::Uint128; use execute::execute; use network_defaults::{ChainDetails, NymNetworkDetails}; use serde::{Deserialize, Serialize}; use std::convert::TryInto; use std::time::SystemTime; use vesting_contract_common::ExecuteMsg as VestingExecuteMsg; -use vesting_contract_common::QueryMsg as VestingQueryMsg; pub use crate::nymd::cosmwasm_client::client::CosmWasmClient; pub use crate::nymd::cosmwasm_client::signing_client::SigningCosmWasmClient; @@ -47,6 +45,7 @@ pub use fee::{gas_price::GasPrice, GasAdjustable, GasAdjustment}; use mixnet_contract_common::MixId; pub use signing_client::Client as SigningNymdClient; pub use traits::{VestingQueryClient, VestingSigningClient}; +use vesting_contract_common::PledgeCap; pub mod coin; pub mod cosmwasm_client; @@ -482,16 +481,6 @@ impl NymdClient { self.client.get_total_supply().await } - pub async fn vesting_get_locked_pledge_cap(&self) -> Result - where - C: CosmWasmClient + Sync, - { - let request = VestingQueryMsg::GetLockedPledgeCap {}; - self.client - .query_contract_smart(self.vesting_contract_address(), &request) - .await - } - pub async fn simulate(&self, messages: I) -> Result where C: SigningCosmWasmClient + Sync, @@ -737,12 +726,16 @@ impl NymdClient { #[execute("vesting")] fn _vesting_update_locked_pledge_cap( &self, - amount: Uint128, + address: String, + cap: PledgeCap, fee: Option, ) -> (VestingExecuteMsg, Option) where C: SigningCosmWasmClient + Sync, { - (VestingExecuteMsg::UpdateLockedPledgeCap { amount }, fee) + ( + VestingExecuteMsg::UpdateLockedPledgeCap { address, cap }, + fee, + ) } } diff --git a/common/client-libs/validator-client/src/nymd/traits/vesting_signing_client.rs b/common/client-libs/validator-client/src/nymd/traits/vesting_signing_client.rs index f1c22c0078..eebf0adfa3 100644 --- a/common/client-libs/validator-client/src/nymd/traits/vesting_signing_client.rs +++ b/common/client-libs/validator-client/src/nymd/traits/vesting_signing_client.rs @@ -9,6 +9,7 @@ use async_trait::async_trait; use mixnet_contract_common::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; use mixnet_contract_common::{Gateway, MixId, MixNode}; use vesting_contract_common::messages::{ExecuteMsg as VestingExecuteMsg, VestingSpecification}; +use vesting_contract_common::PledgeCap; #[async_trait] pub trait VestingSigningClient { @@ -105,6 +106,7 @@ pub trait VestingSigningClient { staking_address: Option, vesting_spec: Option, amount: Coin, + cap: Option, fee: Option, ) -> Result; } @@ -382,6 +384,7 @@ impl VestingSigningClient for NymdClient staking_address: Option, vesting_spec: Option, amount: Coin, + cap: Option, fee: Option, ) -> Result { let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); @@ -389,6 +392,7 @@ impl VestingSigningClient for NymdClient owner_address: owner_address.to_string(), staking_address, vesting_spec, + cap, }; self.client .execute( diff --git a/common/commands/src/validator/vesting/create_vesting_schedule.rs b/common/commands/src/validator/vesting/create_vesting_schedule.rs index 65268d27ed..c7c133673d 100644 --- a/common/commands/src/validator/vesting/create_vesting_schedule.rs +++ b/common/commands/src/validator/vesting/create_vesting_schedule.rs @@ -12,6 +12,7 @@ use validator_client::nymd::AccountId; use validator_client::nymd::VestingSigningClient; use validator_client::nymd::{CosmosCoin, Denom}; use vesting_contract_common::messages::VestingSpecification; +use vesting_contract_common::PledgeCap; use crate::context::SigningClient; @@ -34,6 +35,12 @@ pub struct Args { #[clap(long)] pub staking_address: Option, + + #[clap( + long, + help = "Pledge cap as either absolute uNYM value or percentage, floats need to be in the 0.0 to 1.0 range and will be parsed as percentages, integers will be parsed as uNYM" + )] + pub pledge_cap: Option, } pub async fn create(args: Args, client: SigningClient, network_details: &NymNetworkDetails) { @@ -55,6 +62,7 @@ pub async fn create(args: Args, client: SigningClient, network_details: &NymNetw args.staking_address, Some(vesting), coin.into(), + args.pledge_cap, None, ) .await diff --git a/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml b/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml index 2145acc513..1c1ec8ebee 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml +++ b/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml @@ -9,3 +9,5 @@ edition = "2021" [dependencies] cosmwasm-std = "1.0.0" serde = { version = "1.0", features = ["derive"] } +schemars = "0.8" +thiserror = "1" diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/types.rs b/common/cosmwasm-smart-contracts/contracts-common/src/types.rs index 6c25d95552..320212143d 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/src/types.rs +++ b/common/cosmwasm-smart-contracts/contracts-common/src/types.rs @@ -1,7 +1,120 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use serde::{Deserialize, Serialize}; +use cosmwasm_std::Decimal; +use cosmwasm_std::Uint128; +use schemars::JsonSchema; +use serde::de::Error; +use serde::{Deserialize, Deserializer, Serialize}; +use std::fmt::{self, Display, Formatter}; +use std::ops::Mul; +use std::str::FromStr; +use thiserror::Error; + +pub fn truncate_decimal(amount: Decimal) -> Uint128 { + amount * Uint128::new(1) +} + +#[derive(Error, Debug)] +pub enum ContractsCommonError { + #[error("Provided percent value ({0}) is greater than 100%")] + InvalidPercent(Decimal), + + #[error("{source}")] + StdErr { + #[from] + source: cosmwasm_std::StdError, + }, +} + +/// Percent represents a value between 0 and 100% +/// (i.e. between 0.0 and 1.0) +#[derive( + Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Serialize, Deserialize, JsonSchema, +)] +pub struct Percent(#[serde(deserialize_with = "de_decimal_percent")] Decimal); + +impl Percent { + pub fn new(value: Decimal) -> Result { + if value > Decimal::one() { + Err(ContractsCommonError::InvalidPercent(value)) + } else { + Ok(Percent(value)) + } + } + + pub fn is_zero(&self) -> bool { + self.0 == Decimal::zero() + } + + pub fn from_percentage_value(value: u64) -> Result { + Percent::new(Decimal::percent(value)) + } + + pub fn value(&self) -> Decimal { + self.0 + } + + pub fn round_to_integer(&self) -> u8 { + let hundred = Decimal::from_ratio(100u32, 1u32); + // we know the cast from u128 to u8 is a safe one since the internal value must be within 0 - 1 range + truncate_decimal(hundred * self.0).u128() as u8 + } +} + +impl Display for Percent { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let adjusted = Decimal::from_atomics(100u32, 0).unwrap() * self.0; + write!(f, "{}%", adjusted) + } +} + +impl FromStr for Percent { + type Err = ContractsCommonError; + + fn from_str(s: &str) -> Result { + Percent::new(Decimal::from_str(s)?) + } +} + +impl Mul for Percent { + type Output = Decimal; + + fn mul(self, rhs: Decimal) -> Self::Output { + self.0 * rhs + } +} + +impl Mul for Decimal { + type Output = Decimal; + + fn mul(self, rhs: Percent) -> Self::Output { + rhs * self + } +} + +impl Mul for Percent { + type Output = Uint128; + + fn mul(self, rhs: Uint128) -> Self::Output { + self.0 * rhs + } +} + +// implement custom Deserialize because we want to validate Percent has the correct range +fn de_decimal_percent<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let v = Decimal::deserialize(deserializer)?; + if v > Decimal::one() { + Err(D::Error::custom( + "provided decimal percent is larger than 100%", + )) + } else { + Ok(v) + } +} // TODO: there's no reason this couldn't be used for proper binaries, but in that case // perhaps the struct should get renamed and moved to a "more" common crate diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs index 436f4cad07..e0d369f933 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs @@ -13,9 +13,6 @@ pub enum MixnetContractError { source: cosmwasm_std::StdError, }, - #[error("Provided percent value is greater than 100%")] - InvalidPercent, - #[error("Attempted to subtract decimals with overflow ({minuend}.sub({subtrahend}))")] OverflowDecimalSubtraction { minuend: Decimal, diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/helpers.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/helpers.rs index 8a6a8f3366..4d9ca75459 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/helpers.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/helpers.rs @@ -1,6 +1,7 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use contracts_common::truncate_decimal; use cosmwasm_std::{Coin, Decimal, Uint128}; /// Truncates all decimal points so that the reward would fit in a `Coin` and so that we would @@ -17,7 +18,3 @@ pub fn truncate_reward(reward: Decimal, denom: impl Into) -> Coin { pub fn truncate_reward_amount(reward: Decimal) -> Uint128 { truncate_decimal(reward) } - -pub fn truncate_decimal(amount: Decimal) -> Uint128 { - amount * Uint128::new(1) -} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs index 7d6e8f2d0d..f4af0362db 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs @@ -2,15 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 use crate::error::MixnetContractError; -use crate::rewarding::helpers::truncate_decimal; use crate::{Layer, RewardedSetNodeStatus}; -use cosmwasm_std::{Addr, Uint128}; -use cosmwasm_std::{Coin, Decimal}; +use cosmwasm_std::Addr; +use cosmwasm_std::Coin; use schemars::JsonSchema; -use serde::de::Error; -use serde::{Deserialize, Deserializer, Serialize}; -use std::fmt::{self, Display, Formatter}; -use std::ops::{Index, Mul}; +use serde::{Deserialize, Serialize}; +use std::ops::Index; // type aliases for better reasoning about available data pub type IdentityKey = String; @@ -24,87 +21,6 @@ pub type BlockHeight = u64; pub type EpochEventId = u32; pub type IntervalEventId = u32; -/// Percent represents a value between 0 and 100% -/// (i.e. between 0.0 and 1.0) -#[derive( - Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Serialize, Deserialize, JsonSchema, -)] -pub struct Percent(#[serde(deserialize_with = "de_decimal_percent")] Decimal); - -impl Percent { - pub fn new(value: Decimal) -> Result { - if value > Decimal::one() { - Err(MixnetContractError::InvalidPercent) - } else { - Ok(Percent(value)) - } - } - - pub fn is_zero(&self) -> bool { - self.0 == Decimal::zero() - } - - pub fn from_percentage_value(value: u64) -> Result { - Percent::new(Decimal::percent(value)) - } - - pub fn value(&self) -> Decimal { - self.0 - } - - pub fn round_to_integer(&self) -> u8 { - let hundred = Decimal::from_ratio(100u32, 1u32); - // we know the cast from u128 to u8 is a safe one since the internal value must be within 0 - 1 range - truncate_decimal(hundred * self.0).u128() as u8 - } -} - -impl Display for Percent { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let adjusted = Decimal::from_atomics(100u32, 0).unwrap() * self.0; - write!(f, "{}%", adjusted) - } -} - -impl Mul for Percent { - type Output = Decimal; - - fn mul(self, rhs: Decimal) -> Self::Output { - self.0 * rhs - } -} - -impl Mul for Decimal { - type Output = Decimal; - - fn mul(self, rhs: Percent) -> Self::Output { - rhs * self - } -} - -impl Mul for Percent { - type Output = Uint128; - - fn mul(self, rhs: Uint128) -> Self::Output { - self.0 * rhs - } -} - -// implement custom Deserialize because we want to validate Percent has the correct range -fn de_decimal_percent<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - let v = Decimal::deserialize(deserializer)?; - if v > Decimal::one() { - Err(D::Error::custom( - "provided decimal percent is larger than 100%", - )) - } else { - Ok(v) - } -} - #[derive(Debug, Default, Serialize, Deserialize, Copy, Clone, Eq, PartialEq)] pub struct LayerDistribution { pub layer1: u64, @@ -209,7 +125,7 @@ pub struct PagedRewardedSetResponse { #[cfg(test)] mod tests { - use super::*; + use contracts_common::Percent; #[test] fn percent_serde() { diff --git a/common/cosmwasm-smart-contracts/vesting-contract/Cargo.toml b/common/cosmwasm-smart-contracts/vesting-contract/Cargo.toml index 4bb2c9a123..c2fa50a159 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/Cargo.toml +++ b/common/cosmwasm-smart-contracts/vesting-contract/Cargo.toml @@ -6,8 +6,10 @@ edition = "2021" [dependencies] cosmwasm-std = "1.0.0" mixnet-contract-common = { path = "../mixnet-contract" } +contracts-common = { path = "../contracts-common" } serde = { version = "1.0", features = ["derive"] } schemars = "0.8" +log = "0.4" ts-rs = {version = "6.1.2", optional = true} [features] diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs index 1d7b4d0ab1..4d494510b5 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs @@ -1,6 +1,10 @@ +use std::str::FromStr; + // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use contracts_common::Percent; use cosmwasm_std::{Addr, Coin, Timestamp, Uint128}; +use log::warn; use mixnet_contract_common::MixId; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -42,6 +46,46 @@ impl PledgeData { } } +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub enum PledgeCap { + Percent(Percent), + Absolute(Uint128), // This has to be in unym +} + +impl FromStr for PledgeCap { + type Err = String; + + fn from_str(cap: &str) -> Result { + let cap = cap.replace('_', "").replace(',', "."); + match Percent::from_str(&cap) { + Ok(p) => { + if p.is_zero() { + warn!("Pledge cap set to 0%, are you sure this is right?") + } + Ok(PledgeCap::Percent(p)) + } + Err(_) => { + match cap.parse::() { + Ok(i) => { + if i < 100_000_000_000 { + warn!("PledgeCap set to less then 100_000 NYM, are you sure this is right?"); + } + Ok(PledgeCap::Absolute(Uint128::from(i))) + } + Err(_e) => Err(format!("Could not parse {} as Percent or Uint128", cap)), + } + } + } + } +} + +impl Default for PledgeCap { + fn default() -> Self { + PledgeCap::Absolute(Uint128::from(100_000_000_000u128)) + } +} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct OriginalVestingResponse { pub amount: Coin, @@ -98,3 +142,32 @@ pub struct AllDelegationsResponse { pub delegations: Vec, pub start_next_after: Option<(u32, MixId, u64)>, } + +#[cfg(test)] +mod test { + use contracts_common::Percent; + use cosmwasm_std::Uint128; + use std::str::FromStr; + + use crate::PledgeCap; + + #[test] + fn test_pledge_cap_from_str() { + assert_eq!( + PledgeCap::from_str("0.1").unwrap(), + PledgeCap::Percent(Percent::from_percentage_value(10).unwrap()) + ); + assert_eq!( + PledgeCap::from_str("0,1").unwrap(), + PledgeCap::Percent(Percent::from_percentage_value(10).unwrap()) + ); + assert_eq!( + PledgeCap::from_str("100_000_000_000").unwrap(), + PledgeCap::Absolute(Uint128::new(100_000_000_000)) + ); + assert_eq!( + PledgeCap::from_str("100000000000").unwrap(), + PledgeCap::Absolute(Uint128::new(100_000_000_000)) + ); + } +} diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs index 9a9d21fdae..98f8024dbc 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{Coin, Timestamp, Uint128}; +use cosmwasm_std::{Coin, Timestamp}; use mixnet_contract_common::{ mixnode::{MixNodeConfigUpdate, MixNodeCostParams}, Gateway, MixId, MixNode, @@ -6,6 +6,8 @@ use mixnet_contract_common::{ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use crate::PledgeCap; + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct InitMsg { @@ -83,6 +85,7 @@ pub enum ExecuteMsg { owner_address: String, staking_address: Option, vesting_spec: Option, + cap: Option, }, WithdrawVestedCoins { amount: Coin, @@ -120,7 +123,8 @@ pub enum ExecuteMsg { to_address: Option, }, UpdateLockedPledgeCap { - amount: Uint128, + address: String, + cap: PledgeCap, }, } @@ -201,7 +205,6 @@ pub enum QueryMsg { GetCurrentVestingPeriod { address: String, }, - GetLockedPledgeCap {}, GetDelegationTimes { address: String, mix_id: MixId, diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 3490bbf2fd..a62c529faf 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -221,7 +221,9 @@ name = "contracts-common" version = "0.1.0" dependencies = [ "cosmwasm-std", + "schemars", "serde", + "thiserror", ] [[package]] @@ -1606,6 +1608,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" name = "vesting-contract" version = "1.1.0" dependencies = [ + "contracts-common", "cosmwasm-std", "cw-storage-plus", "mixnet-contract-common", @@ -1619,7 +1622,9 @@ dependencies = [ name = "vesting-contract-common" version = "0.1.0" dependencies = [ + "contracts-common", "cosmwasm-std", + "log", "mixnet-contract-common", "schemars", "serde", diff --git a/contracts/vesting/Cargo.toml b/contracts/vesting/Cargo.toml index 3c03d92c91..f55bd649f1 100644 --- a/contracts/vesting/Cargo.toml +++ b/contracts/vesting/Cargo.toml @@ -15,6 +15,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract" } +contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" } vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract" } cosmwasm-std = { version = "1.0.0 "} diff --git a/contracts/vesting/src/contract.rs b/contracts/vesting/src/contract.rs index 99a2acb9f4..9d1cee6711 100644 --- a/contracts/vesting/src/contract.rs +++ b/contracts/vesting/src/contract.rs @@ -1,8 +1,8 @@ use crate::errors::ContractError; use crate::queued_migrations::migrate_to_v2_mixnet_contract; use crate::storage::{ - account_from_address, locked_pledge_cap, update_locked_pledge_cap, BlockTimestampSecs, ADMIN, - DELEGATIONS, MIXNET_CONTRACT_ADDRESS, MIX_DENOM, + account_from_address, BlockTimestampSecs, ADMIN, DELEGATIONS, MIXNET_CONTRACT_ADDRESS, + MIX_DENOM, }; use crate::traits::{ DelegatingAccount, GatewayBondingAccount, MixnodeBondingAccount, VestingAccount, @@ -25,8 +25,8 @@ use vesting_contract_common::messages::{ ExecuteMsg, InitMsg, MigrateMsg, QueryMsg, VestingSpecification, }; use vesting_contract_common::{ - AllDelegationsResponse, DelegationTimesResponse, OriginalVestingResponse, Period, PledgeData, - VestingDelegation, + AllDelegationsResponse, DelegationTimesResponse, OriginalVestingResponse, Period, PledgeCap, + PledgeData, VestingDelegation, }; pub const INITIAL_LOCKED_PLEDGE_CAP: Uint128 = Uint128::new(100_000_000_000); @@ -59,8 +59,8 @@ pub fn execute( msg: ExecuteMsg, ) -> Result { match msg { - ExecuteMsg::UpdateLockedPledgeCap { amount } => { - try_update_locked_pledge_cap(amount, info, deps) + ExecuteMsg::UpdateLockedPledgeCap { address, cap } => { + try_update_locked_pledge_cap(address, cap, info, deps) } ExecuteMsg::TrackReward { amount, address } => { try_track_reward(deps, info, amount, &address) @@ -88,10 +88,12 @@ pub fn execute( owner_address, staking_address, vesting_spec, + cap, } => try_create_periodic_vesting_account( &owner_address, staking_address, vesting_spec, + cap, info, env, deps, @@ -144,14 +146,18 @@ pub fn execute( /// /// Callable by ADMIN only, see [instantiate]. pub fn try_update_locked_pledge_cap( - amount: Uint128, + address: String, + cap: PledgeCap, info: MessageInfo, deps: DepsMut, ) -> Result { if info.sender != ADMIN.load(deps.storage)? { return Err(ContractError::NotAdmin(info.sender.as_str().to_string())); } - update_locked_pledge_cap(amount, deps.storage)?; + let mut account = account_from_address(&address, deps.storage, deps.api)?; + + account.pledge_cap = Some(cap); + // update_locked_pledge_cap(amount, deps.storage)?; Ok(Response::default()) } @@ -430,6 +436,7 @@ fn try_create_periodic_vesting_account( owner_address: &str, staking_address: Option, vesting_spec: Option, + cap: Option, info: MessageInfo, env: Env, deps: DepsMut<'_>, @@ -437,6 +444,7 @@ fn try_create_periodic_vesting_account( if info.sender != ADMIN.load(deps.storage)? { return Err(ContractError::NotAdmin(info.sender.as_str().to_string())); } + let mix_denom = MIX_DENOM.load(deps.storage)?; let account_exists = account_from_address(owner_address, deps.storage, deps.api).is_ok(); @@ -452,6 +460,11 @@ fn try_create_periodic_vesting_account( let owner_address = deps.api.addr_validate(owner_address)?; let staking_address = if let Some(staking_address) = staking_address { + let staking_account_exists = + account_from_address(&staking_address, deps.storage, deps.api).is_ok(); + if staking_account_exists { + return Err(ContractError::StakingAccountAlreadyExists(staking_address)); + } Some(deps.api.addr_validate(&staking_address)?) } else { None @@ -472,6 +485,7 @@ fn try_create_periodic_vesting_account( coin.clone(), start_time, periods, + cap, deps.storage, )?; @@ -486,7 +500,6 @@ fn try_create_periodic_vesting_account( #[entry_point] pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result { let query_res = match msg { - QueryMsg::GetLockedPledgeCap {} => to_binary(&get_locked_pledge_cap(deps)), QueryMsg::LockedCoins { vesting_account_address, block_time, @@ -567,11 +580,6 @@ pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result) -> Uint128 { - locked_pledge_cap(deps.storage) -} - /// Get current vesting period for a given [crate::vesting::Account]. pub fn try_get_current_vesting_period( address: &str, diff --git a/contracts/vesting/src/errors.rs b/contracts/vesting/src/errors.rs index 92b8524292..bc992e7ca4 100644 --- a/contracts/vesting/src/errors.rs +++ b/contracts/vesting/src/errors.rs @@ -44,6 +44,8 @@ pub enum ContractError { InvalidAddress(String), #[error("VESTING ({}): Account already exists: {0}", line!())] AccountAlreadyExists(String), + #[error("VESTING ({}): Staking account already exists: {0}", line!())] + StakingAccountAlreadyExists(String), #[error("VESTING ({}): Too few coins sent for vesting account creation, sent {sent}, need at least {need}", line!())] MinVestingFunds { sent: u128, need: u128 }, #[error("VESTING ({}): Maximum amount of locked coins has already been pledged: {current}, cap is {cap}", line!())] diff --git a/contracts/vesting/src/storage.rs b/contracts/vesting/src/storage.rs index eda3f93d72..a1216fa87e 100644 --- a/contracts/vesting/src/storage.rs +++ b/contracts/vesting/src/storage.rs @@ -1,5 +1,5 @@ +use crate::errors::ContractError; use crate::vesting::Account; -use crate::{contract::INITIAL_LOCKED_PLEDGE_CAP, errors::ContractError}; use cosmwasm_std::{Addr, Api, Storage, Uint128}; use cw_storage_plus::{Item, Map}; use mixnet_contract_common::{IdentityKey, MixId}; @@ -20,21 +20,6 @@ pub const DELEGATIONS: Map<'_, (u32, MixId, BlockTimestampSecs), Uint128> = Map: pub const ADMIN: Item<'_, String> = Item::new("adm"); pub const MIXNET_CONTRACT_ADDRESS: Item<'_, String> = Item::new("mix"); pub const MIX_DENOM: Item<'_, String> = Item::new("den"); -pub const LOCKED_PLEDGE_CAP: Item<'_, Uint128> = Item::new("lck"); - -pub fn locked_pledge_cap(storage: &dyn Storage) -> Uint128 { - LOCKED_PLEDGE_CAP - .load(storage) - .unwrap_or(INITIAL_LOCKED_PLEDGE_CAP) -} - -pub fn update_locked_pledge_cap( - amount: Uint128, - storage: &mut dyn Storage, -) -> Result<(), ContractError> { - LOCKED_PLEDGE_CAP.save(storage, &amount)?; - Ok(()) -} pub fn save_delegation( key: (u32, MixId, BlockTimestampSecs), diff --git a/contracts/vesting/src/support/tests.rs b/contracts/vesting/src/support/tests.rs index 8084e48739..8f24359f52 100644 --- a/contracts/vesting/src/support/tests.rs +++ b/contracts/vesting/src/support/tests.rs @@ -2,9 +2,12 @@ pub mod helpers { use crate::contract::instantiate; use crate::vesting::{populate_vesting_periods, Account}; + use contracts_common::Percent; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info, MockApi, MockQuerier}; use cosmwasm_std::{Addr, Coin, Empty, Env, MemoryStorage, OwnedDeps, Storage, Uint128}; + use std::str::FromStr; use vesting_contract_common::messages::{InitMsg, VestingSpecification}; + use vesting_contract_common::PledgeCap; pub const TEST_COIN_DENOM: &str = "unym"; @@ -37,6 +40,7 @@ pub mod helpers { }, start_time_ts, periods, + None, storage, ) .unwrap() @@ -56,6 +60,29 @@ pub mod helpers { }, start_time, periods, + Some(PledgeCap::from_str("0.1").unwrap()), + storage, + ) + .unwrap() + } + + pub fn vesting_account_percent_fixture(storage: &mut dyn Storage, env: &Env) -> Account { + let start_time = env.block.time; + let periods = + populate_vesting_periods(start_time.seconds(), VestingSpecification::default()); + + Account::new( + Addr::unchecked("owner"), + Some(Addr::unchecked("staking")), + Coin { + amount: Uint128::new(1_000_000_000_000), + denom: TEST_COIN_DENOM.to_string(), + }, + start_time, + periods, + Some(PledgeCap::Percent( + Percent::from_percentage_value(10).unwrap(), + )), storage, ) .unwrap() diff --git a/contracts/vesting/src/vesting/account/delegating_account.rs b/contracts/vesting/src/vesting/account/delegating_account.rs index 1951dd6b61..62fa42b21e 100644 --- a/contracts/vesting/src/vesting/account/delegating_account.rs +++ b/contracts/vesting/src/vesting/account/delegating_account.rs @@ -1,5 +1,4 @@ use crate::errors::ContractError; -use crate::storage::locked_pledge_cap; use crate::storage::save_delegation; use crate::storage::MIXNET_CONTRACT_ADDRESS; use crate::traits::DelegatingAccount; @@ -38,8 +37,9 @@ impl DelegatingAccount for Account { storage: &mut dyn Storage, ) -> Result { let current_balance = self.load_balance(storage)?; - let total_pledged_after = self.total_pledged_locked(storage, env)? + coin.amount; - let locked_pledge_cap = locked_pledge_cap(storage); + let total_pledged_locked = self.total_pledged_locked(storage, env)?; + let total_pledged_after = total_pledged_locked + coin.amount; + let locked_pledge_cap = self.absolute_pledge_cap()?; if locked_pledge_cap < total_pledged_after { return Err(ContractError::LockedPledgeCapReached { diff --git a/contracts/vesting/src/vesting/account/gateway_bonding_account.rs b/contracts/vesting/src/vesting/account/gateway_bonding_account.rs index e9979b71a6..91cdd41c68 100644 --- a/contracts/vesting/src/vesting/account/gateway_bonding_account.rs +++ b/contracts/vesting/src/vesting/account/gateway_bonding_account.rs @@ -1,6 +1,5 @@ use super::PledgeData; use crate::errors::ContractError; -use crate::storage::locked_pledge_cap; use crate::storage::MIXNET_CONTRACT_ADDRESS; use crate::traits::GatewayBondingAccount; use crate::traits::VestingAccount; @@ -22,8 +21,9 @@ impl GatewayBondingAccount for Account { storage: &mut dyn Storage, ) -> Result { let current_balance = self.load_balance(storage)?; - let total_pledged_after = self.total_pledged_locked(storage, env)? + pledge.amount; - let locked_pledge_cap = locked_pledge_cap(storage); + let total_pledged_locked = self.total_pledged_locked(storage, env)?; + let total_pledged_after = total_pledged_locked + pledge.amount; + let locked_pledge_cap = self.absolute_pledge_cap()?; if locked_pledge_cap < total_pledged_after { return Err(ContractError::LockedPledgeCapReached { diff --git a/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs b/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs index 7fd182e8c9..e3aec82584 100644 --- a/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs +++ b/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs @@ -3,7 +3,6 @@ use super::Account; use crate::errors::ContractError; -use crate::storage::locked_pledge_cap; use crate::storage::MIXNET_CONTRACT_ADDRESS; use crate::traits::MixnodeBondingAccount; use crate::traits::VestingAccount; @@ -39,8 +38,9 @@ impl MixnodeBondingAccount for Account { storage: &mut dyn Storage, ) -> Result { let current_balance = self.load_balance(storage)?; - let total_pledged_after = self.total_pledged_locked(storage, env)? + pledge.amount; - let locked_pledge_cap = locked_pledge_cap(storage); + let total_pledged_locked = self.total_pledged_locked(storage, env)?; + let total_pledged_after = total_pledged_locked + pledge.amount; + let locked_pledge_cap = self.absolute_pledge_cap()?; if locked_pledge_cap < total_pledged_after { return Err(ContractError::LockedPledgeCapReached { diff --git a/contracts/vesting/src/vesting/account/mod.rs b/contracts/vesting/src/vesting/account/mod.rs index 203ab3feb2..8520dad85e 100644 --- a/contracts/vesting/src/vesting/account/mod.rs +++ b/contracts/vesting/src/vesting/account/mod.rs @@ -5,12 +5,13 @@ use crate::storage::{ remove_delegation, remove_gateway_pledge, save_account, save_balance, save_bond_pledge, save_gateway_pledge, save_withdrawn, BlockTimestampSecs, DELEGATIONS, KEY, }; +use crate::traits::VestingAccount; use cosmwasm_std::{Addr, Coin, Order, Storage, Timestamp, Uint128}; use cw_storage_plus::Bound; use mixnet_contract_common::MixId; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use vesting_contract_common::{Period, PledgeData}; +use vesting_contract_common::{Period, PledgeCap, PledgeData}; mod delegating_account; mod gateway_bonding_account; @@ -31,6 +32,8 @@ pub struct Account { pub periods: Vec, pub coin: Coin, storage_key: u32, + #[serde(default)] + pub pledge_cap: Option, } impl Account { @@ -40,6 +43,7 @@ impl Account { coin: Coin, start_time: Timestamp, periods: Vec, + pledge_cap: Option, storage: &mut dyn Storage, ) -> Result { let storage_key = generate_storage_key(storage)?; @@ -51,12 +55,24 @@ impl Account { periods, coin, storage_key, + pledge_cap, }; save_account(&account, storage)?; account.save_balance(amount, storage)?; Ok(account) } + pub fn pledge_cap(&self) -> PledgeCap { + self.pledge_cap.clone().unwrap_or_default() + } + + pub fn absolute_pledge_cap(&self) -> Result { + match self.pledge_cap() { + PledgeCap::Absolute(cap) => Ok(cap), + PledgeCap::Percent(p) => Ok(p * self.get_original_vesting().amount.amount), + } + } + pub fn coin(&self) -> Coin { self.coin.clone() } diff --git a/contracts/vesting/src/vesting/account/vesting_account.rs b/contracts/vesting/src/vesting/account/vesting_account.rs index d7eeeb0946..53f39e2891 100644 --- a/contracts/vesting/src/vesting/account/vesting_account.rs +++ b/contracts/vesting/src/vesting/account/vesting_account.rs @@ -123,12 +123,13 @@ impl VestingAccount for Account { storage: &dyn Storage, ) -> Result { let block_time = block_time.unwrap_or(env.block.time); - let period = self.get_current_vesting_period(block_time); let withdrawn = self.load_withdrawn(storage)?; let max_available = self .get_vested_coins(Some(block_time), env, storage)? .amount .saturating_sub(withdrawn); + + let period = self.get_current_vesting_period(block_time); let start_time = match period { Period::Before => 0, Period::After => u64::MAX, @@ -155,15 +156,8 @@ impl VestingAccount for Account { let block_time = block_time.unwrap_or(env.block.time); let delegated_free = self.get_delegated_free(Some(block_time), env, storage)?; - let period = self.get_current_vesting_period(block_time); - let start_time = match period { - Period::Before => 0, - Period::After => u64::MAX, - Period::In(idx) => self.periods[idx as usize].start_time, - }; - let delegations_before_start_time = - self.total_delegations_at_timestamp(storage, start_time)?; + self.total_delegations_at_timestamp(storage, block_time.seconds())?; let amount = delegations_before_start_time - delegated_free.amount; diff --git a/contracts/vesting/src/vesting/mod.rs b/contracts/vesting/src/vesting/mod.rs index 3e46c46496..d678d2aab8 100644 --- a/contracts/vesting/src/vesting/mod.rs +++ b/contracts/vesting/src/vesting/mod.rs @@ -38,6 +38,7 @@ pub fn populate_vesting_periods( mod tests { use crate::contract::*; use crate::storage::*; + use crate::support::tests::helpers::vesting_account_percent_fixture; use crate::support::tests::helpers::{ init_contract, vesting_account_mid_fixture, vesting_account_new_fixture, TEST_COIN_DENOM, }; @@ -51,6 +52,7 @@ mod tests { use mixnet_contract_common::{Gateway, MixNode, Percent}; use vesting_contract_common::messages::{ExecuteMsg, VestingSpecification}; use vesting_contract_common::Period; + use vesting_contract_common::PledgeCap; #[test] fn test_account_creation() { @@ -61,6 +63,7 @@ mod tests { owner_address: "owner".to_string(), staking_address: Some("staking".to_string()), vesting_spec: None, + cap: Some(PledgeCap::Absolute(Uint128::from(100_000_000_000u128))), }; // Try creating an account when not admin let response = execute(deps.as_mut(), env.clone(), info.clone(), msg.clone()); @@ -389,12 +392,38 @@ mod tests { assert_eq!(locked_coins.amount, Uint128::new(660_000_000_000)); } + #[test] + fn test_percent_cap() { + let mut deps = init_contract(); + let env = mock_env(); + + let account = vesting_account_percent_fixture(&mut deps.storage, &env); + + assert_eq!( + account.absolute_pledge_cap().unwrap(), + Uint128::new(100_000_000_000) + ) + } + #[test] fn test_delegations() { let mut deps = init_contract(); let env = mock_env(); - let account = vesting_account_new_fixture(&mut deps.storage, &env); + // let account = vesting_account_new_fixture(&mut deps.storage, &env); + + let msg = ExecuteMsg::CreateAccount { + owner_address: "owner".to_string(), + staking_address: Some("staking".to_string()), + vesting_spec: None, + cap: Some(PledgeCap::Absolute(Uint128::from(100_000_000_000u128))), + }; + let info = mock_info("admin", &coins(1_000_000_000_000, TEST_COIN_DENOM)); + + let _response = execute(deps.as_mut(), env.clone(), info.clone(), msg.clone()); + let account = load_account(&Addr::unchecked("owner"), &deps.storage) + .unwrap() + .unwrap(); // Try delegating too much let err = account.try_delegate_to_mixnode( @@ -434,7 +463,7 @@ mod tests { let balance = account.load_balance(&deps.storage).unwrap(); assert_eq!(balance, Uint128::new(910000000000)); - // Try delegating too much again + // Try delegating too much againcalca let err = account.try_delegate_to_mixnode( 1, Coin { @@ -449,6 +478,10 @@ mod tests { let total_delegations = account.total_delegations_for_mix(1, &deps.storage).unwrap(); assert_eq!(Uint128::new(90_000_000_000), total_delegations); + let account = load_account(&Addr::unchecked("owner"), &deps.storage) + .unwrap() + .unwrap(); + // Current period -> block_time: None let delegated_free = account .get_delegated_free(None, &env, &deps.storage) @@ -813,6 +846,7 @@ mod tests { }, Timestamp::from_seconds(account_creation_timestamp), periods, + Some(PledgeCap::Absolute(Uint128::from(100_000_000_000u128))), deps.as_mut().storage, ) .unwrap(); diff --git a/explorer-api/Cargo.toml b/explorer-api/Cargo.toml index f9edf01b44..8aae21b2ef 100644 --- a/explorer-api/Cargo.toml +++ b/explorer-api/Cargo.toml @@ -27,6 +27,7 @@ maxminddb = "0.23.0" dotenv = "0.15.0" mixnet-contract-common = { path = "../common/cosmwasm-smart-contracts/mixnet-contract" } +contracts-common = { path = "../common/cosmwasm-smart-contracts/contracts-common" } network-defaults = { path = "../common/network-defaults" } task = { path = "../common/task" } validator-client = { path = "../common/client-libs/validator-client", features=["nymd-client"] } diff --git a/explorer-api/src/mix_node/econ_stats.rs b/explorer-api/src/mix_node/econ_stats.rs index cb068b11a6..22f106b1aa 100644 --- a/explorer-api/src/mix_node/econ_stats.rs +++ b/explorer-api/src/mix_node/econ_stats.rs @@ -4,7 +4,7 @@ use crate::client::ThreadsafeValidatorClient; use crate::helpers::best_effort_small_dec_to_f64; use crate::mix_node::models::EconomicDynamicsStats; -use mixnet_contract_common::rewarding::helpers::truncate_decimal; +use contracts_common::truncate_decimal; use mixnet_contract_common::MixId; pub(crate) async fn retrieve_mixnode_econ_stats( diff --git a/nym-connect/Cargo.lock b/nym-connect/Cargo.lock index 01a52ecf52..5a102a8d6e 100644 --- a/nym-connect/Cargo.lock +++ b/nym-connect/Cargo.lock @@ -752,7 +752,9 @@ name = "contracts-common" version = "0.1.0" dependencies = [ "cosmwasm-std", + "schemars", "serde", + "thiserror", ] [[package]] @@ -6158,6 +6160,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" name = "vesting-contract" version = "1.1.0" dependencies = [ + "contracts-common", "cosmwasm-std", "cw-storage-plus", "mixnet-contract-common", @@ -6171,7 +6174,9 @@ dependencies = [ name = "vesting-contract-common" version = "0.1.0" dependencies = [ + "contracts-common", "cosmwasm-std", + "log", "mixnet-contract-common", "schemars", "serde", diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index e861e1b5fc..1c44d275a5 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -665,7 +665,9 @@ name = "contracts-common" version = "0.1.0" dependencies = [ "cosmwasm-std", + "schemars", "serde", + "thiserror", ] [[package]] @@ -5308,6 +5310,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" name = "vesting-contract" version = "1.1.0" dependencies = [ + "contracts-common", "cosmwasm-std", "cw-storage-plus", "mixnet-contract-common", @@ -5321,7 +5324,9 @@ dependencies = [ name = "vesting-contract-common" version = "0.1.0" dependencies = [ + "contracts-common", "cosmwasm-std", + "log", "mixnet-contract-common", "schemars", "serde", diff --git a/validator-api/Cargo.toml b/validator-api/Cargo.toml index 498773a970..b4c82a5748 100644 --- a/validator-api/Cargo.toml +++ b/validator-api/Cargo.toml @@ -72,6 +72,7 @@ crypto = { path = "../common/crypto" } gateway-client = { path = "../common/client-libs/gateway-client" } inclusion-probability = { path = "../common/inclusion-probability" } mixnet-contract-common = { path = "../common/cosmwasm-smart-contracts/mixnet-contract" } +contracts-common = { path = "../common/cosmwasm-smart-contracts/contracts-common" } multisig-contract-common = { path = "../common/cosmwasm-smart-contracts/multisig-contract" } nymcoconut = { path = "../common/nymcoconut", optional = true } nymsphinx = { path = "../common/nymsphinx" } diff --git a/validator-api/src/node_status_api/cache.rs b/validator-api/src/node_status_api/cache.rs index dd942ff6ed..ef5df963bf 100644 --- a/validator-api/src/node_status_api/cache.rs +++ b/validator-api/src/node_status_api/cache.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::contract_cache::{Cache, CacheNotification, ValidatorCache}; -use mixnet_contract_common::rewarding::helpers::truncate_decimal; +use contracts_common::truncate_decimal; use mixnet_contract_common::{MixId, MixNodeDetails, RewardingParams}; use rocket::fairing::AdHoc; use serde::Serialize;