diff --git a/Cargo.lock b/Cargo.lock index b59c2eba..b841079d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1024,9 +1024,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "sylvia" -version = "0.6.1" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "244466d32c81f9421db755e437924126233815cd0f17fca0dd7472d1045eb4c1" +checksum = "f358d505f46900e55154f028f18811961ebb58f7a92954ec03086ffb2b26cf51" dependencies = [ "anyhow", "cosmwasm-schema", @@ -1043,9 +1043,9 @@ dependencies = [ [[package]] name = "sylvia-derive" -version = "0.6.1" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b9a418ce96b68c6cf770edea4bb6049a7d2ed20f85e03560bfe375494ccfcd3" +checksum = "bed182fb775d756fdfe7e87174a4e43f1c446c8f9aff1de38a2165dd04b7d805" dependencies = [ "convert_case", "proc-macro-crate", diff --git a/Cargo.toml b/Cargo.toml index f88f45c6..5cc0d5a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ mesh-converter = { path = "./contracts/consumer/converter" } mesh-simple-price-feed = { path = "./contracts/consumer/simple-price-feed" } mesh-virtual-staking = { path = "./contracts/consumer/virtual-staking" } -sylvia = "0.6.0" +sylvia = "0.7.1" cosmwasm-schema = "1.2" cosmwasm-std = { version = "1.2", features = ["ibc3", "cosmwasm_1_2"] } cosmwasm-storage = "1.2" @@ -55,4 +55,3 @@ overflow-checks = true [profile.release.package.mesh-vault] codegen-units = 1 incremental = false - diff --git a/contracts/provider/external-staking/src/contract.rs b/contracts/provider/external-staking/src/contract.rs index cddd92ec..6fd44f21 100644 --- a/contracts/provider/external-staking/src/contract.rs +++ b/contracts/provider/external-staking/src/contract.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{ - coin, ensure, ensure_eq, from_binary, to_binary, Addr, Binary, Coin, Decimal, DepsMut, Env, - Event, IbcMsg, Order, Response, StdError, StdResult, Storage, Uint128, Uint256, WasmMsg, + coin, ensure, ensure_eq, to_binary, Addr, Coin, Decimal, DepsMut, Env, Event, IbcMsg, Order, + Response, StdResult, Storage, Uint128, Uint256, WasmMsg, }; use cw2::set_contract_version; use cw_storage_plus::{Bounder, Item, Map}; @@ -9,12 +9,10 @@ use cw_utils::{nonpayable, PaymentError}; use sylvia::contract; use sylvia::types::{ExecCtx, InstantiateCtx, QueryCtx}; -use mesh_apis::cross_staking_api::{self, CrossStakingApi}; +use mesh_apis::cross_staking_api::{self}; use mesh_apis::ibc::AddValidator; use mesh_apis::ibc::ProviderPacket; -use mesh_apis::local_staking_api::MaxSlashResponse; use mesh_apis::vault_api::VaultApiHelper; -use mesh_sync::Lockable; use mesh_sync::Tx; use crate::crdt::CrdtState; @@ -22,8 +20,8 @@ use crate::error::ContractError; use crate::ibc::{packet_timeout, IBC_CHANNEL}; use crate::msg::{ AllPendingRewards, AllTxsResponse, AuthorizedEndpointResponse, ConfigResponse, - IbcChannelResponse, ListRemoteValidatorsResponse, MaybePendingRewards, MaybeStake, - ReceiveVirtualStake, StakeInfo, StakesResponse, TxResponse, ValidatorPendingRewards, + IbcChannelResponse, ListRemoteValidatorsResponse, PendingRewards, StakeInfo, StakesResponse, + TxResponse, ValidatorPendingRewards, }; use crate::state::{Config, Distribution, Stake}; @@ -43,7 +41,7 @@ fn clamp_page_limit(limit: Option) -> usize { pub struct ExternalStakingContract<'a> { pub config: Item<'a, Config>, /// Stakes indexed by `(owner, validator)` pair - pub stakes: Map<'a, (&'a Addr, &'a str), Lockable>, + pub stakes: Map<'a, (&'a Addr, &'a str), Stake>, /// Per-validator distribution information pub distribution: Map<'a, &'a str, Distribution>, /// Pending txs information @@ -146,7 +144,7 @@ impl ExternalStakingContract<'_> { }; // Load stake - let mut stake_lock = self.stakes.load(deps.storage, (&tx_user, &tx_validator))?; + let mut stake = self.stakes.load(deps.storage, (&tx_user, &tx_validator))?; // Load distribution let mut distribution = self @@ -154,10 +152,8 @@ impl ExternalStakingContract<'_> { .may_load(deps.storage, &tx_validator)? .unwrap_or_default(); - // Commit amount (need to unlock it first) - stake_lock.unlock_write()?; - let stake = stake_lock.write()?; - stake.stake += tx_amount; + // Commit stake + stake.stake.commit_add(tx_amount); // Distribution alignment stake @@ -167,7 +163,7 @@ impl ExternalStakingContract<'_> { // Save stake self.stakes - .save(deps.storage, (&tx_user, &tx_validator), &stake_lock)?; + .save(deps.storage, (&tx_user, &tx_validator), &stake)?; // Save distribution self.distribution @@ -198,7 +194,7 @@ impl ExternalStakingContract<'_> { ContractError::WrongTypeTx(tx_id, tx) ); - let (_tx_amount, tx_user, tx_validator) = match tx { + let (tx_amount, tx_user, tx_validator) = match tx { Tx::InFlightRemoteStaking { amount, user, @@ -209,14 +205,14 @@ impl ExternalStakingContract<'_> { }; // Load stake - let mut stake_lock = self.stakes.load(deps.storage, (&tx_user, &tx_validator))?; + let mut stake = self.stakes.load(deps.storage, (&tx_user, &tx_validator))?; - // Release stake lock - stake_lock.unlock_write()?; + // Rollback add amount + stake.stake.rollback_add(tx_amount); // Save stake self.stakes - .save(deps.storage, (&tx_user, &tx_validator), &stake_lock)?; + .save(deps.storage, (&tx_user, &tx_validator), &stake)?; // Remove tx self.pending_txs.remove(deps.storage, tx_id); @@ -300,9 +296,10 @@ impl ExternalStakingContract<'_> { validator: String, amount: Coin, ) -> Result { - nonpayable(&ctx.info)?; + let ExecCtx { info, deps, env } = ctx; + nonpayable(&info)?; - let config = self.config.load(ctx.deps.storage)?; + let config = self.config.load(deps.storage)?; ensure_eq!( amount.denom, @@ -310,43 +307,40 @@ impl ExternalStakingContract<'_> { ContractError::InvalidDenom(config.denom) ); - let mut stake_lock = self + let mut stake = self .stakes - .may_load(ctx.deps.storage, (&ctx.info.sender, &validator))? + .may_load(deps.storage, (&info.sender, &validator))? .unwrap_or_default(); - let stake = stake_lock.read()?; ensure!( - stake.stake >= amount.amount, - ContractError::NotEnoughStake(stake.stake) + stake.stake.low() >= amount.amount, + ContractError::NotEnoughStake(stake.stake.low()) ); - stake_lock.lock_write()?; - self.stakes.save( - ctx.deps.storage, - (&ctx.info.sender, &validator), - &stake_lock, - )?; + stake.stake.prepare_sub(amount.amount, Uint128::zero())?; + + self.stakes + .save(deps.storage, (&info.sender, &validator), &stake)?; // Create new tx - let tx_id = self.next_tx_id(ctx.deps.storage)?; + let tx_id = self.next_tx_id(deps.storage)?; // Save tx let new_tx = Tx::InFlightRemoteUnstaking { id: tx_id, amount: amount.amount, - user: ctx.info.sender.clone(), + user: info.sender.clone(), validator: validator.clone(), }; - self.pending_txs.save(ctx.deps.storage, tx_id, &new_tx)?; + self.pending_txs.save(deps.storage, tx_id, &new_tx)?; #[allow(unused_mut)] let mut resp = Response::new() .add_attribute("action", "unstake") .add_attribute("amount", amount.amount.to_string()) - .add_attribute("owner", ctx.info.sender); + .add_attribute("owner", info.sender); - let channel = IBC_CHANNEL.load(ctx.deps.storage)?; + let channel = IBC_CHANNEL.load(deps.storage)?; let packet = ProviderPacket::Unstake { validator, unstake: amount, @@ -355,7 +349,7 @@ impl ExternalStakingContract<'_> { let msg = IbcMsg::SendPacket { channel_id: channel.endpoint.channel_id, data: to_binary(&packet)?, - timeout: packet_timeout(&ctx.env), + timeout: packet_timeout(&env), }; // send packet if we are ibc enabled // TODO: send in test code when we can handle it @@ -403,7 +397,7 @@ impl ExternalStakingContract<'_> { let config = self.config.load(deps.storage)?; // Load stake - let mut stake_lock = self.stakes.load(deps.storage, (&tx_user, &tx_validator))?; + let mut stake = self.stakes.load(deps.storage, (&tx_user, &tx_validator))?; // Load distribution let mut distribution = self @@ -411,10 +405,8 @@ impl ExternalStakingContract<'_> { .may_load(deps.storage, &tx_validator)? .unwrap_or_default(); - // Commit amount (need to unlock it first) - stake_lock.unlock_write()?; - let stake = stake_lock.write()?; - stake.stake -= tx_amount; + // Commit sub amount + stake.stake.commit_sub(tx_amount); // FIXME? Release period being computed after successful IBC tx // (Note: this is good for now, but can be revisited in v1 design) @@ -433,7 +425,7 @@ impl ExternalStakingContract<'_> { // Save stake self.stakes - .save(deps.storage, (&tx_user, &tx_validator), &stake_lock)?; + .save(deps.storage, (&tx_user, &tx_validator), &stake)?; // Save distribution self.distribution @@ -455,7 +447,7 @@ impl ExternalStakingContract<'_> { matches!(tx, Tx::InFlightRemoteUnstaking { .. }), ContractError::WrongTypeTx(tx_id, tx) ); - let (_tx_amount, tx_user, tx_validator) = match tx { + let (tx_amount, tx_user, tx_validator) = match tx { Tx::InFlightRemoteUnstaking { amount, user, @@ -466,14 +458,14 @@ impl ExternalStakingContract<'_> { }; // Load stake - let mut stake_lock = self.stakes.load(deps.storage, (&tx_user, &tx_validator))?; + let mut stake = self.stakes.load(deps.storage, (&tx_user, &tx_validator))?; - // Release stake lock - stake_lock.unlock_write()?; + // Rollback sub amount + stake.stake.rollback_sub(tx_amount); // Save stake self.stakes - .save(deps.storage, (&tx_user, &tx_validator), &stake_lock)?; + .save(deps.storage, (&tx_user, &tx_validator), &stake)?; // Remove tx self.pending_txs.remove(deps.storage, tx_id); @@ -522,24 +514,20 @@ impl ExternalStakingContract<'_> { let config = self.config.load(ctx.deps.storage)?; - let stake_locks: Vec<_> = self + let stakes: Vec<_> = self .stakes .prefix(&ctx.info.sender) .range(ctx.deps.storage, None, None, Order::Ascending) .collect::>()?; - let released: Uint128 = stake_locks + let released: Uint128 = stakes .into_iter() - .map(|(validator, mut stake_lock)| -> Result<_, ContractError> { - let stake = stake_lock.write()?; + .map(|(validator, mut stake)| -> Result<_, ContractError> { let released = stake.release_pending(&ctx.env.block); if !released.is_zero() { - self.stakes.save( - ctx.deps.storage, - (&ctx.info.sender, &validator), - &stake_lock, - )? + self.stakes + .save(ctx.deps.storage, (&ctx.info.sender, &validator), &stake)? } Ok(released) @@ -641,19 +629,17 @@ impl ExternalStakingContract<'_> { ) -> Result { nonpayable(&ctx.info)?; - let mut stake_lock = self + let stake = self .stakes .may_load(ctx.deps.storage, (&ctx.info.sender, &validator))? .unwrap_or_default(); - let stake = stake_lock.read()?; - let distribution = self .distribution .may_load(ctx.deps.storage, &validator)? .unwrap_or_default(); - let amount = Self::calculate_reward(stake, &distribution)?; + let amount = Self::calculate_reward(&stake, &distribution)?; if amount.is_zero() { return Err(ContractError::NoRewards); @@ -667,15 +653,6 @@ impl ExternalStakingContract<'_> { .add_attribute("recipient", &remote_recipient) .add_attribute("amount", amount.to_string()); - // lock the stake. the withdrawn_funds will be updated on a commit, - // left unchanged on rollback - stake_lock.lock_write()?; - self.stakes.save( - ctx.deps.storage, - (&ctx.info.sender, &validator), - &stake_lock, - )?; - // prepare the pending tx let tx_id = self.next_tx_id(ctx.deps.storage)?; let new_tx = Tx::InFlightTransferFunds { @@ -761,26 +738,18 @@ impl ExternalStakingContract<'_> { deps: DepsMut, tx_id: u64, ) -> Result<(), ContractError> { - // Load tx let tx = self.pending_txs.load(deps.storage, tx_id)?; - self.pending_txs.remove(deps.storage, tx_id); - // Verify tx is of the right type and get data - let (staker, validator) = match tx { - Tx::InFlightTransferFunds { - staker, validator, .. - } => (staker, validator), + // Verify tx is of the right type and remove it from the map + match tx { + Tx::InFlightTransferFunds { .. } => { + self.pending_txs.remove(deps.storage, tx_id); + } _ => { return Err(ContractError::WrongTypeTx(tx_id, tx)); } }; - // release the write lock and leave state unchanged - let mut stake_lock = self.stakes.load(deps.storage, (&staker, &validator))?; - stake_lock.unlock_write()?; - self.stakes - .save(deps.storage, (&staker, &validator), &stake_lock)?; - Ok(()) } @@ -808,13 +777,12 @@ impl ExternalStakingContract<'_> { } }; - // release the write lock and update withdrawn_funds to hold this transfer - let mut stake_lock = self.stakes.load(deps.storage, (&staker, &validator))?; - stake_lock.unlock_write()?; - let stake = stake_lock.write()?; + // Update withdrawn_funds to hold this transfer + let mut stake = self.stakes.load(deps.storage, (&staker, &validator))?; stake.withdrawn_funds += amount; + self.stakes - .save(deps.storage, (&staker, &validator), &stake_lock)?; + .save(deps.storage, (&staker, &validator), &stake)?; Ok(()) } @@ -860,23 +828,21 @@ impl ExternalStakingContract<'_> { /// Queries for stake info /// - /// If stake is not existing in the system is queried, the zero-stake is returned + /// If stake does not exist for (user, validator) pair, the zero-stake is returned #[msg(query)] pub fn stake( &self, ctx: QueryCtx, user: String, validator: String, - ) -> Result { + ) -> Result { let user = ctx.deps.api.addr_validate(&user)?; - let stake_lock = self + let stake = self .stakes .may_load(ctx.deps.storage, (&user, &validator))? .unwrap_or_default(); - match stake_lock.read() { - Ok(stake) => Ok(MaybeStake::Stake(stake.clone())), - Err(_) => Ok(MaybeStake::Locked {}), - } + + Ok(stake) } /// Paginated list of user stakes. @@ -900,14 +866,11 @@ impl ExternalStakingContract<'_> { .prefix(&user) .range(ctx.deps.storage, bound, None, Order::Ascending) .map(|item| { - item.map(|(validator, stake_lock)| { + item.map(|(validator, stake)| { Ok::(StakeInfo { owner: user.to_string(), validator, - stake: match stake_lock.read() { - Ok(stake) => MaybeStake::Stake(stake.clone()), - Err(_) => MaybeStake::Locked {}, - }, + stake, }) })? }) @@ -962,30 +925,25 @@ impl ExternalStakingContract<'_> { ctx: QueryCtx, user: String, validator: String, - ) -> Result { + ) -> Result { let user = ctx.deps.api.addr_validate(&user)?; - let stake_lock = self + let stake = self .stakes .may_load(ctx.deps.storage, (&user, &validator))? .unwrap_or_default(); - match stake_lock.read() { - Ok(stake) => { - let distribution = self - .distribution - .may_load(ctx.deps.storage, &validator)? - .unwrap_or_default(); - let amount = Self::calculate_reward(stake, &distribution)?; - let config = self.config.load(ctx.deps.storage)?; + let distribution = self + .distribution + .may_load(ctx.deps.storage, &validator)? + .unwrap_or_default(); - Ok(MaybePendingRewards::Rewards(coin( - amount.u128(), - config.rewards_denom, - ))) - } - Err(_) => Ok(MaybePendingRewards::Locked {}), - } + let amount = Self::calculate_reward(&stake, &distribution)?; + let config = self.config.load(ctx.deps.storage)?; + + Ok(PendingRewards { + rewards: coin(amount.u128(), config.rewards_denom), + }) } /// Returns how much rewards are to be withdrawn by particular user, iterating over all validators. @@ -1011,22 +969,17 @@ impl ExternalStakingContract<'_> { .range(ctx.deps.storage, bound, None, Order::Ascending) .take(limit) .map(|item| { - let (validator, stake_lock) = item?; - match stake_lock.read() { - Ok(stake) => { - let distribution = self - .distribution - .may_load(ctx.deps.storage, &validator)? - .unwrap_or_default(); - let amount = Self::calculate_reward(stake, &distribution)?; - Ok::<_, ContractError>(ValidatorPendingRewards::new( - validator, - amount.u128(), - &config.rewards_denom, - )) - } - Err(_) => Ok(ValidatorPendingRewards::new_locked(validator)), - } + let (validator, stake) = item?; + let distribution = self + .distribution + .may_load(ctx.deps.storage, &validator)? + .unwrap_or_default(); + let amount = Self::calculate_reward(&stake, &distribution)?; + Ok::<_, ContractError>(ValidatorPendingRewards::new( + validator, + amount.u128(), + &config.rewards_denom, + )) }) .collect::>()?; @@ -1040,8 +993,13 @@ impl ExternalStakingContract<'_> { // could be enforced by taking user and validator in arguments, then fetching data, but // sometimes data are used also for different calculations so we want to avoid double // fetching. - fn calculate_reward(stake: &Stake, distribution: &Distribution) -> Result { - let points = distribution.points_per_stake * Uint256::from(stake.stake); + fn calculate_reward( + stake: &Stake, + distribution: &Distribution, + ) -> Result { + // Calculating rewards with the `lower` value goes agains the user, but the possible + // errors are small and temporary. + let points = distribution.points_per_stake * Uint256::from(stake.stake.low()); let points = stake.points_alignment.align(points); let total = Uint128::try_from(points / DISTRIBUTION_POINTS_SCALE)?; @@ -1051,10 +1009,14 @@ impl ExternalStakingContract<'_> { } pub mod cross_staking { + use crate::msg::ReceiveVirtualStake; + use super::*; + use cosmwasm_std::{from_binary, Binary}; + use mesh_apis::{cross_staking_api::CrossStakingApi, local_staking_api::MaxSlashResponse}; - #[contract] - #[messages(cross_staking_api as CrossStakingApi)] + #[contract(module=crate::contract)] + #[messages(mesh_apis::cross_staking_api as CrossStakingApi)] impl CrossStakingApi for ExternalStakingContract<'_> { type Error = ContractError; @@ -1087,15 +1049,17 @@ pub mod cross_staking { { return Err(ContractError::ValidatorNotActive(msg.validator)); } - let mut stake_lock = self + let mut stake = self .stakes .may_load(ctx.deps.storage, (&owner, &msg.validator))? .unwrap_or_default(); - // Write lock and save stake and distribution - stake_lock.lock_write()?; + // Prepare stake addition and save stake. + // We don't check for max here, as this call can only come from the `vault` contract, which already + // performed the proper check. + stake.stake.prepare_add(amount.amount, None)?; self.stakes - .save(ctx.deps.storage, (&owner, &msg.validator), &stake_lock)?; + .save(ctx.deps.storage, (&owner, &msg.validator), &stake)?; // Save tx let new_tx = Tx::InFlightRemoteStaking { diff --git a/contracts/provider/external-staking/src/error.rs b/contracts/provider/external-staking/src/error.rs index 383176bd..1f67eff6 100644 --- a/contracts/provider/external-staking/src/error.rs +++ b/contracts/provider/external-staking/src/error.rs @@ -1,7 +1,7 @@ use cosmwasm_std::{ConversionOverflowError, StdError, Uint128}; use cw_utils::PaymentError; use mesh_apis::ibc::VersionError; -use mesh_sync::{LockError, Tx}; +use mesh_sync::{RangeError, Tx}; use thiserror::Error; #[derive(Error, Debug, PartialEq)] @@ -18,9 +18,6 @@ pub enum ContractError { #[error("{0}")] Conversion(#[from] ConversionOverflowError), - #[error("{0}")] - Lock(#[from] LockError), - #[error("Unauthorized")] Unauthorized, @@ -56,4 +53,7 @@ pub enum ContractError { #[error("No staking rewards to be withdrawn")] NoRewards, + + #[error("{0}")] + Range(#[from] RangeError), } diff --git a/contracts/provider/external-staking/src/msg.rs b/contracts/provider/external-staking/src/msg.rs index d48c400d..0280dbd8 100644 --- a/contracts/provider/external-staking/src/msg.rs +++ b/contracts/provider/external-staking/src/msg.rs @@ -58,29 +58,12 @@ impl From for ConfigResponse { } } -/// Response for stake query on one user and validator -#[cw_serde] -pub enum MaybeStake { - Stake(Stake), - Locked {}, -} - -impl MaybeStake { - /// Designed for test code, unwrap or panic if Locked - pub fn unwrap(self) -> Stake { - match self { - MaybeStake::Stake(stake) => stake, - MaybeStake::Locked {} => panic!("Stake is locked"), - } - } -} - /// Stake-related information including user address and validator #[cw_serde] pub struct StakeInfo { pub owner: String, pub validator: String, - pub stake: MaybeStake, + pub stake: Stake, } impl StakeInfo { @@ -88,7 +71,7 @@ impl StakeInfo { Self { owner: owner.to_string(), validator: validator.to_string(), - stake: MaybeStake::Stake(stake.clone()), + stake: stake.clone(), } } } @@ -119,20 +102,10 @@ pub struct UsersResponse { /// Response for pending rewards query on one validator #[cw_serde] -pub enum MaybePendingRewards { - Rewards(Coin), - Locked {}, +pub struct PendingRewards { + pub rewards: Coin, } -impl MaybePendingRewards { - /// Designed for test code, unwrap or panic if Locked - pub fn unwrap(self) -> Coin { - match self { - MaybePendingRewards::Rewards(coin) => coin, - MaybePendingRewards::Locked {} => panic!("Pending rewards are locked"), - } - } -} /// Response for pending rewards query on all validator #[cw_serde] pub struct AllPendingRewards { @@ -142,20 +115,16 @@ pub struct AllPendingRewards { #[cw_serde] pub struct ValidatorPendingRewards { pub validator: String, - pub rewards: MaybePendingRewards, + pub rewards: PendingRewards, } impl ValidatorPendingRewards { pub fn new(validator: impl Into, amount: u128, denom: impl Into) -> Self { Self { validator: validator.into(), - rewards: MaybePendingRewards::Rewards(coin(amount, denom)), - } - } - pub fn new_locked(validator: impl Into) -> Self { - Self { - validator: validator.into(), - rewards: MaybePendingRewards::Locked {}, + rewards: PendingRewards { + rewards: coin(amount, denom), + }, } } } diff --git a/contracts/provider/external-staking/src/multitest.rs b/contracts/provider/external-staking/src/multitest.rs index 824085e4..711f5cc3 100644 --- a/contracts/provider/external-staking/src/multitest.rs +++ b/contracts/provider/external-staking/src/multitest.rs @@ -1,6 +1,6 @@ use anyhow::Result as AnyResult; -use cosmwasm_std::{coin, coins, to_binary, Addr, Decimal}; +use cosmwasm_std::{coin, coins, to_binary, Addr, Decimal, Uint128}; use mesh_apis::ibc::AddValidator; use mesh_native_staking::contract::multitest_utils::CodeId as NativeStakingCodeId; use mesh_native_staking::contract::InstantiateMsg as NativeStakingInstantiateMsg; @@ -8,7 +8,7 @@ use mesh_native_staking_proxy::contract::multitest_utils::CodeId as NativeStakin use mesh_vault::contract::multitest_utils::{CodeId as VaultCodeId, VaultContractProxy}; use mesh_vault::msg::StakingInitInfo; -use mesh_sync::Tx; +use mesh_sync::{Tx, ValueRange}; use cw_multi_test::App as MtApp; use sylvia::multitest::App; @@ -189,6 +189,7 @@ fn staking() { ) .call(users[0]) .unwrap(); + contract .test_commit_stake(get_last_external_staking_pending_tx_id(&contract).unwrap()) .call("test") @@ -263,27 +264,23 @@ fn staking() { // Querying for particular stakes let stake = contract .stake(users[0].to_owned(), validators[0].to_owned()) - .unwrap() .unwrap(); - assert_eq!(stake.stake.u128(), 200); + assert_eq!(stake.stake, ValueRange::new_val(Uint128::new(200))); let stake = contract .stake(users[0].to_owned(), validators[1].to_owned()) - .unwrap() .unwrap(); - assert_eq!(stake.stake.u128(), 100); + assert_eq!(stake.stake, ValueRange::new_val(Uint128::new(100))); let stake = contract .stake(users[1].to_owned(), validators[0].to_owned()) - .unwrap() .unwrap(); - assert_eq!(stake.stake.u128(), 100); + assert_eq!(stake.stake, ValueRange::new_val(Uint128::new(100))); let stake = contract .stake(users[1].to_owned(), validators[1].to_owned()) - .unwrap() .unwrap(); - assert_eq!(stake.stake.u128(), 200); + assert_eq!(stake.stake, ValueRange::new_val(Uint128::new(200))); // Querying fo all the stakes let stakes = contract.stakes(users[0].to_owned(), None, None).unwrap(); @@ -466,27 +463,23 @@ fn unstaking() { // Unstaken should be immediately visible on staken amount let stake = contract .stake(users[0].to_string(), validators[0].to_string()) - .unwrap() .unwrap(); - assert_eq!(stake.stake.u128(), 150); + assert_eq!(stake.stake, ValueRange::new_val(Uint128::new(150))); let stake = contract .stake(users[0].to_string(), validators[1].to_string()) - .unwrap() .unwrap(); - assert_eq!(stake.stake.u128(), 100); + assert_eq!(stake.stake, ValueRange::new_val(Uint128::new(100))); let stake = contract .stake(users[1].to_string(), validators[0].to_string()) - .unwrap() .unwrap(); - assert_eq!(stake.stake.u128(), 240); + assert_eq!(stake.stake, ValueRange::new_val(Uint128::new(240))); let stake = contract .stake(users[1].to_string(), validators[1].to_string()) - .unwrap() .unwrap(); - assert_eq!(stake.stake.u128(), 0); + assert_eq!(stake.stake, ValueRange::new_val(Uint128::zero())); // But not on vault side let claim = vault @@ -559,27 +552,23 @@ fn unstaking() { // Verify proper stake values let stake = contract .stake(users[0].to_string(), validators[0].to_string()) - .unwrap() .unwrap(); - assert_eq!(stake.stake.u128(), 80); + assert_eq!(stake.stake, ValueRange::new_val(Uint128::new(80))); let stake = contract .stake(users[0].to_string(), validators[1].to_string()) - .unwrap() .unwrap(); - assert_eq!(stake.stake.u128(), 10); + assert_eq!(stake.stake, ValueRange::new_val(Uint128::new(10))); let stake = contract .stake(users[1].to_string(), validators[0].to_string()) - .unwrap() .unwrap(); - assert_eq!(stake.stake.u128(), 240); + assert_eq!(stake.stake, ValueRange::new_val(Uint128::new(240))); let stake = contract .stake(users[1].to_string(), validators[1].to_string()) - .unwrap() .unwrap(); - assert_eq!(stake.stake.u128(), 0); + assert_eq!(stake.stake, ValueRange::new_val(Uint128::new(0))); // Another timetravel - just enough for first batch of stakes to release, // too early for second batch @@ -710,6 +699,7 @@ fn distribution() { ) .call(users[0]) .unwrap(); + // TODO: Hardcoded external-staking's commit_stake call (lack of IBC support yet). // This should be through `IbcPacketAckMsg` let last_external_staking_tx = get_last_external_staking_pending_tx_id(&contract).unwrap(); @@ -769,25 +759,25 @@ fn distribution() { let rewards = contract .pending_rewards(users[0].to_owned(), validators[0].to_owned()) .unwrap() - .unwrap(); + .rewards; assert_eq!(rewards.amount.u128(), 20); let rewards = contract .pending_rewards(users[1].to_owned(), validators[0].to_owned()) .unwrap() - .unwrap(); + .rewards; assert_eq!(rewards.amount.u128(), 30); let rewards = contract .pending_rewards(users[0].to_owned(), validators[1].to_owned()) .unwrap() - .unwrap(); + .rewards; assert_eq!(rewards.amount.u128(), 30); let rewards = contract .pending_rewards(users[1].to_owned(), validators[1].to_owned()) .unwrap() - .unwrap(); + .rewards; assert_eq!(rewards.amount.u128(), 0); // Show all rewards skips validators that were never staked on @@ -825,25 +815,25 @@ fn distribution() { let rewards = contract .pending_rewards(users[0].to_owned(), validators[0].to_owned()) .unwrap() - .unwrap(); + .rewards; assert_eq!(rewards.amount.u128(), 48); let rewards = contract .pending_rewards(users[1].to_owned(), validators[0].to_owned()) .unwrap() - .unwrap(); + .rewards; assert_eq!(rewards.amount.u128(), 72); let rewards = contract .pending_rewards(users[0].to_owned(), validators[1].to_owned()) .unwrap() - .unwrap(); + .rewards; assert_eq!(rewards.amount.u128(), 30); let rewards = contract .pending_rewards(users[1].to_owned(), validators[1].to_owned()) .unwrap() - .unwrap(); + .rewards; assert_eq!(rewards.amount.u128(), 0); // Withdraw rewards @@ -851,6 +841,7 @@ fn distribution() { .withdraw_rewards(validators[0].to_owned(), remote[0].to_owned()) .call(users[0]) .unwrap(); + let tx_id = get_last_external_staking_pending_tx_id(&contract).unwrap(); contract .test_commit_withdraw_rewards(tx_id) @@ -877,6 +868,22 @@ fn distribution() { .call(users[1]) .unwrap(); + // Rewards withrawal should not affect the stake + let stake = contract + .stake(users[0].to_owned(), validators[0].to_owned()) + .unwrap(); + assert_eq!(stake.stake, ValueRange::new_val(Uint128::new(200))); + + let stake = contract + .stake(users[0].to_owned(), validators[1].to_owned()) + .unwrap(); + assert_eq!(stake.stake, ValueRange::new_val(Uint128::new(100))); + + let stake = contract + .stake(users[1].to_owned(), validators[0].to_owned()) + .unwrap(); + assert_eq!(stake.stake, ValueRange::new_val(Uint128::new(300))); + // error if 0 rewards available let err = contract .withdraw_rewards(validators[1].to_owned(), remote[1].to_owned()) @@ -886,29 +893,35 @@ fn distribution() { let tx_id = get_last_external_staking_pending_tx_id(&contract); assert_eq!(tx_id, None); + // Stake remains unaffected after rewards withdrawal failure + let stake = contract + .stake(users[1].to_owned(), validators[1].to_owned()) + .unwrap(); + assert_eq!(stake.stake, ValueRange::new_val(Uint128::new(0))); + // Rewards should not be withdrawable anymore let rewards = contract .pending_rewards(users[0].to_owned(), validators[0].to_owned()) .unwrap() - .unwrap(); + .rewards; assert_eq!(rewards.amount.u128(), 0); let rewards = contract .pending_rewards(users[1].to_owned(), validators[0].to_owned()) .unwrap() - .unwrap(); + .rewards; assert_eq!(rewards.amount.u128(), 0); let rewards = contract .pending_rewards(users[0].to_owned(), validators[1].to_owned()) .unwrap() - .unwrap(); + .rewards; assert_eq!(rewards.amount.u128(), 0); let rewards = contract .pending_rewards(users[1].to_owned(), validators[1].to_owned()) .unwrap() - .unwrap(); + .rewards; assert_eq!(rewards.amount.u128(), 0); // Another distribution - making it equal @@ -924,13 +937,13 @@ fn distribution() { let rewards = contract .pending_rewards(users[0].to_owned(), validators[0].to_owned()) .unwrap() - .unwrap(); + .rewards; assert_eq!(rewards.amount.u128(), 4); let rewards = contract .pending_rewards(users[1].to_owned(), validators[0].to_owned()) .unwrap() - .unwrap(); + .rewards; assert_eq!(rewards.amount.u128(), 6); // Now yet another unequal distribution to play around keeping all correct when weights are @@ -991,25 +1004,25 @@ fn distribution() { let rewards = contract .pending_rewards(users[0].to_owned(), validators[0].to_owned()) .unwrap() - .unwrap(); + .rewards; assert_eq!(rewards.amount.u128(), 8); let rewards = contract .pending_rewards(users[1].to_owned(), validators[0].to_owned()) .unwrap() - .unwrap(); + .rewards; assert_eq!(rewards.amount.u128(), 12); let rewards = contract .pending_rewards(users[0].to_owned(), validators[1].to_owned()) .unwrap() - .unwrap(); + .rewards; assert_eq!(rewards.amount.u128(), 11); let rewards = contract .pending_rewards(users[1].to_owned(), validators[1].to_owned()) .unwrap() - .unwrap(); + .rewards; assert_eq!(rewards.amount.u128(), 0); // Now distribute some nice values @@ -1031,25 +1044,25 @@ fn distribution() { let rewards = contract .pending_rewards(users[0].to_owned(), validators[0].to_owned()) .unwrap() - .unwrap(); + .rewards; assert_eq!(rewards.amount.u128(), 18); let rewards = contract .pending_rewards(users[1].to_owned(), validators[0].to_owned()) .unwrap() - .unwrap(); + .rewards; assert_eq!(rewards.amount.u128(), 22); let rewards = contract .pending_rewards(users[0].to_owned(), validators[1].to_owned()) .unwrap() - .unwrap(); + .rewards; assert_eq!(rewards.amount.u128(), 21); let rewards = contract .pending_rewards(users[1].to_owned(), validators[1].to_owned()) .unwrap() - .unwrap(); + .rewards; assert_eq!(rewards.amount.u128(), 30); // And some more distribution fun - we are 50/50 on validators[1], so distributing odd number of @@ -1066,13 +1079,13 @@ fn distribution() { let rewards = contract .pending_rewards(users[0].to_owned(), validators[0].to_owned()) .unwrap() - .unwrap(); + .rewards; assert_eq!(rewards.amount.u128(), 20); let rewards = contract .pending_rewards(users[1].to_owned(), validators[0].to_owned()) .unwrap() - .unwrap(); + .rewards; assert_eq!(rewards.amount.u128(), 25); // More unstaking - to make it both ways by both stakers on at least one validator, for sake of @@ -1116,13 +1129,13 @@ fn distribution() { let rewards = contract .pending_rewards(users[0].to_owned(), validators[0].to_owned()) .unwrap() - .unwrap(); + .rewards; assert_eq!(rewards.amount.u128(), 28); let rewards = contract .pending_rewards(users[1].to_owned(), validators[0].to_owned()) .unwrap() - .unwrap(); + .rewards; assert_eq!(rewards.amount.u128(), 29); // Withdraw only by users[0] @@ -1157,29 +1170,50 @@ fn distribution() { .call(users[1]) .unwrap(); + // Rewards withrawal should not affect the stake + let stake = contract + .stake(users[0].to_owned(), validators[0].to_owned()) + .unwrap(); + assert_eq!(stake.stake, ValueRange::new_val(Uint128::new(150))); + + let stake = contract + .stake(users[0].to_owned(), validators[1].to_owned()) + .unwrap(); + assert_eq!(stake.stake, ValueRange::new_val(Uint128::new(100))); + + let stake = contract + .stake(users[1].to_owned(), validators[0].to_owned()) + .unwrap(); + assert_eq!(stake.stake, ValueRange::new_val(Uint128::new(100))); + + let stake = contract + .stake(users[1].to_owned(), validators[1].to_owned()) + .unwrap(); + assert_eq!(stake.stake, ValueRange::new_val(Uint128::new(300))); + // Check withdrawals and accounts let rewards = contract .pending_rewards(users[0].to_owned(), validators[0].to_owned()) .unwrap() - .unwrap(); + .rewards; assert_eq!(rewards.amount.u128(), 0); let rewards = contract .pending_rewards(users[1].to_owned(), validators[0].to_owned()) .unwrap() - .unwrap(); + .rewards; assert_eq!(rewards.amount.u128(), 29); let rewards = contract .pending_rewards(users[0].to_owned(), validators[1].to_owned()) .unwrap() - .unwrap(); + .rewards; assert_eq!(rewards.amount.u128(), 0); let rewards = contract .pending_rewards(users[1].to_owned(), validators[1].to_owned()) .unwrap() - .unwrap(); + .rewards; assert_eq!(rewards.amount.u128(), 30); // Final distribution - 10 tokens to both validators @@ -1201,25 +1235,25 @@ fn distribution() { let rewards = contract .pending_rewards(users[0].to_owned(), validators[0].to_owned()) .unwrap() - .unwrap(); + .rewards; assert_eq!(rewards.amount.u128(), 6); let rewards = contract .pending_rewards(users[1].to_owned(), validators[0].to_owned()) .unwrap() - .unwrap(); + .rewards; assert_eq!(rewards.amount.u128(), 33); let rewards = contract .pending_rewards(users[0].to_owned(), validators[1].to_owned()) .unwrap() - .unwrap(); + .rewards; assert_eq!(rewards.amount.u128(), 2); let rewards = contract .pending_rewards(users[1].to_owned(), validators[1].to_owned()) .unwrap() - .unwrap(); + .rewards; assert_eq!(rewards.amount.u128(), 37); let all_rewards = contract diff --git a/contracts/provider/external-staking/src/state.rs b/contracts/provider/external-staking/src/state.rs index fd85cde9..eeb406be 100644 --- a/contracts/provider/external-staking/src/state.rs +++ b/contracts/provider/external-staking/src/state.rs @@ -1,6 +1,7 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{BlockInfo, Decimal, Timestamp, Uint128, Uint256}; use mesh_apis::vault_api::VaultApiHelper; +use mesh_sync::ValueRange; use crate::points_alignment::PointsAlignment; @@ -26,7 +27,7 @@ pub struct Config { pub struct Stake { /// How much tokens user staken and not in unbonding period /// via this contract - pub stake: Uint128, + pub stake: ValueRange, /// List of token batches scheduled for unbonding /// /// Items should only be added to the end of this list, with `release_at` being @@ -44,7 +45,7 @@ impl Stake { /// Create simplified stake (mostly for tests) pub fn from_amount(amount: Uint128) -> Self { Self { - stake: amount, + stake: ValueRange::new_val(amount), ..Default::default() } } diff --git a/contracts/provider/vault/src/contract.rs b/contracts/provider/vault/src/contract.rs index 77d14f30..82387f68 100644 --- a/contracts/provider/vault/src/contract.rs +++ b/contracts/provider/vault/src/contract.rs @@ -650,7 +650,6 @@ impl VaultContract<'_> { .range(storage, None, None, Order::Ascending) .try_fold(ValueRange::new_val(Uint128::zero()), |max_lien, item| { let (_, lien) = item?; - // FIXME: Use max_range here when user lock is removed Ok::<_, ContractError>(max_range(max_lien, lien.amount)) })?; Ok(())