From cef94c2054b809822e254e6d09545449b64ea795 Mon Sep 17 00:00:00 2001 From: Jakub Bogucki Date: Mon, 15 Aug 2022 21:18:13 +0200 Subject: [PATCH 01/25] multi-test: WIP add staking and distribution modules --- packages/multi-test/src/app.rs | 22 ++-- packages/multi-test/src/lib.rs | 2 +- packages/multi-test/src/staking.rs | 172 ++++++++++++++++++++++++++++- 3 files changed, 184 insertions(+), 12 deletions(-) diff --git a/packages/multi-test/src/app.rs b/packages/multi-test/src/app.rs index 8227bff24..74b6aa78b 100644 --- a/packages/multi-test/src/app.rs +++ b/packages/multi-test/src/app.rs @@ -17,7 +17,9 @@ use crate::bank::{Bank, BankKeeper, BankSudo}; use crate::contracts::Contract; use crate::executor::{AppResponse, Executor}; use crate::module::{FailingModule, Module}; -use crate::staking::{Distribution, FailingDistribution, FailingStaking, Staking, StakingSudo}; +use crate::staking::{ + Distribution, FailingDistribution, FailingStaking, StakeKeeper, Staking, StakingSudo, +}; use crate::transactions::transactional; use crate::wasm::{ContractData, Wasm, WasmKeeper, WasmSudo}; @@ -44,7 +46,7 @@ pub struct App< Storage = MockStorage, Custom = FailingModule, Wasm = WasmKeeper, - Staking = FailingStaking, + Staking = StakeKeeper, Distr = FailingDistribution, > { router: Router, @@ -75,7 +77,7 @@ impl BasicApp { BankKeeper, FailingModule, WasmKeeper, - FailingStaking, + StakeKeeper, FailingDistribution, >, &dyn Api, @@ -97,7 +99,7 @@ where BankKeeper, FailingModule, WasmKeeper, - FailingStaking, + StakeKeeper, FailingDistribution, >, &dyn Api, @@ -159,7 +161,7 @@ pub type BasicAppBuilder = AppBuilder< MockStorage, FailingModule, WasmKeeper, - FailingStaking, + StakeKeeper, FailingDistribution, >; @@ -182,7 +184,7 @@ impl Default MockStorage, FailingModule, WasmKeeper, - FailingStaking, + StakeKeeper, FailingDistribution, > { @@ -198,7 +200,7 @@ impl MockStorage, FailingModule, WasmKeeper, - FailingStaking, + StakeKeeper, FailingDistribution, > { @@ -211,7 +213,7 @@ impl bank: BankKeeper::new(), wasm: WasmKeeper::new(), custom: FailingModule::new(), - staking: FailingStaking::new(), + staking: StakeKeeper::new(), distribution: FailingDistribution::new(), } } @@ -224,7 +226,7 @@ impl MockStorage, FailingModule, WasmKeeper, - FailingStaking, + StakeKeeper, FailingDistribution, > where @@ -241,7 +243,7 @@ where bank: BankKeeper::new(), wasm: WasmKeeper::new(), custom: FailingModule::new(), - staking: FailingStaking::new(), + staking: StakeKeeper::new(), distribution: FailingDistribution::new(), } } diff --git a/packages/multi-test/src/lib.rs b/packages/multi-test/src/lib.rs index 662db3bb6..4f54c991a 100644 --- a/packages/multi-test/src/lib.rs +++ b/packages/multi-test/src/lib.rs @@ -28,5 +28,5 @@ pub use crate::bank::{Bank, BankKeeper, BankSudo}; pub use crate::contracts::{Contract, ContractWrapper}; pub use crate::executor::{AppResponse, Executor}; pub use crate::module::{FailingModule, Module}; -pub use crate::staking::{FailingDistribution, FailingStaking, Staking, StakingSudo}; +pub use crate::staking::{FailingDistribution, FailingStaking, StakeKeeper, Staking, StakingSudo}; pub use crate::wasm::{Wasm, WasmKeeper, WasmSudo}; diff --git a/packages/multi-test/src/staking.rs b/packages/multi-test/src/staking.rs index 20254a87e..973ce5b41 100644 --- a/packages/multi-test/src/staking.rs +++ b/packages/multi-test/src/staking.rs @@ -1,9 +1,23 @@ -use cosmwasm_std::{Decimal, DistributionMsg, Empty, StakingMsg, StakingQuery}; +use anyhow::{bail, Result as AnyResult}; use schemars::JsonSchema; +use cosmwasm_std::{ + Addr, Api, Binary, BlockInfo, Coin, Decimal, DistributionMsg, Empty, Event, Querier, + StakingMsg, StakingQuery, Storage, DelegationResponse, FullDelegation, to_binary, coin, Uint128 +}; +use cosmwasm_storage::{prefixed, prefixed_read}; +use cw_storage_plus::Map; +use cw_utils::NativeBalance; + +use crate::app::CosmosRouter; +use crate::executor::AppResponse; use crate::module::FailingModule; use crate::Module; +const STAKES: Map<&Addr, NativeBalance> = Map::new("stakes"); + +pub const NAMESPACE_STAKING: &[u8] = b"staking"; + // We need to expand on this, but we will need this to properly test out staking #[derive(Clone, std::fmt::Debug, PartialEq, Eq, JsonSchema)] pub enum StakingSudo { @@ -24,3 +38,159 @@ pub trait Distribution: Module; impl Distribution for FailingDistribution {} + +#[derive(Default)] +pub struct StakeKeeper {} + +impl StakeKeeper { + pub fn new() -> Self { + StakeKeeper {} + } + + pub fn init_balance( + &self, + storage: &mut dyn Storage, + account: &Addr, + amount: Vec, + ) -> AnyResult<()> { + let mut storage = prefixed(storage, NAMESPACE_STAKING); + self.set_balance(&mut storage, account, amount) + } + + fn get_stakes(&self, storage: &dyn Storage, account: &Addr) -> AnyResult> { + let val = STAKES.may_load(storage, account)?; + Ok(val.unwrap_or_default().into_vec()) + } + + fn set_balance( + &self, + storage: &mut dyn Storage, + account: &Addr, + amount: Vec, + ) -> AnyResult<()> { + let mut stake = NativeBalance(amount); + stake.normalize(); + STAKES.save(storage, account, &stake).map_err(Into::into) + } + + fn add_stake( + &self, + storage: &mut dyn Storage, + to_address: Addr, + amount: Vec, + ) -> AnyResult<()> { + let amount = self.normalize_amount(amount)?; + let b = self.get_stakes(storage, &to_address)?; + let b = NativeBalance(b) + NativeBalance(amount); + self.set_balance(storage, &to_address, b.into_vec()) + } + + fn remove_stake( + &self, + storage: &mut dyn Storage, + from_address: Addr, + amount: Vec, + ) -> AnyResult<()> { + let amount = self.normalize_amount(amount)?; + let a = self.get_stakes(storage, &from_address)?; + let a = (NativeBalance(a) - amount)?; + self.set_balance(storage, &from_address, a.into_vec()) + } + + /// Filters out all 0 value coins and returns an error if the resulting Vec is empty + fn normalize_amount(&self, amount: Vec) -> AnyResult> { + let res: Vec<_> = amount.into_iter().filter(|x| !x.amount.is_zero()).collect(); + if res.is_empty() { + bail!("Cannot transfer empty coins amount") + } else { + Ok(res) + } + } +} + +impl Staking for StakeKeeper {} + +impl Module for StakeKeeper { + type ExecT = StakingMsg; + type QueryT = StakingQuery; + type SudoT = StakingSudo; + + fn execute( + &self, + _api: &dyn Api, + storage: &mut dyn Storage, + _router: &dyn CosmosRouter, + _block: &BlockInfo, + sender: Addr, + msg: StakingMsg, + ) -> AnyResult { + let mut staking_storage = prefixed(storage, NAMESPACE_STAKING); + match msg { + StakingMsg::Delegate { validator, amount } => { + let events = vec![Event::new("delegate") + .add_attribute("recipient", &validator) + .add_attribute("sender", &sender) + .add_attribute("amount", format!("{}{}", amount.amount, amount.denom))]; + self.add_stake(&mut staking_storage, sender, vec![amount])?; + Ok(AppResponse { events, data: None }) + }, + StakingMsg::Undelegate { validator, amount } => { + let events = vec![Event::new("undelegate") + .add_attribute("from", &validator) + .add_attribute("to", &sender) + .add_attribute("amount", format!("{}{}", amount.amount, amount.denom))]; + self.remove_stake(&mut staking_storage, sender, vec![amount])?; + Ok(AppResponse { events, data: None }) + } + m => bail!("Unsupported staking message: {:?}", m), + } + } + + fn sudo( + &self, + _api: &dyn Api, + _storage: &mut dyn Storage, + _router: &dyn CosmosRouter, + _block: &BlockInfo, + msg: StakingSudo, + ) -> AnyResult { + match msg { + s => bail!("Unsupported staking sudo message: {:?}", s), + } + } + + fn query( + &self, + api: &dyn Api, + storage: &dyn Storage, + _querier: &dyn Querier, + _block: &BlockInfo, + request: StakingQuery, + ) -> AnyResult { + let staking_storage = prefixed_read(storage, NAMESPACE_STAKING); + match request { + StakingQuery::Delegation { delegator, validator } => { + // for now validator is ignored, as I want to support only one validator + let delegator = api.addr_validate(&delegator)?; + let stakes = match self.get_stakes(&staking_storage, &delegator) { + Ok(stakes) => stakes[0].clone(), + Err(_) => { + let response = DelegationResponse { delegation: None }; + return Ok(to_binary(&response)?); + } + }; + // set fixed reward ratio 1:10 per delegated amoutn + let reward = coin((stakes.amount / Uint128::new(10)).u128(), stakes.denom.clone()); + let full_delegation_response = FullDelegation { + delegator, + validator, + amount: stakes, + can_redelegate: coin(0, "testcoin"), + accumulated_rewards: vec![reward], + }; + Ok(to_binary(&full_delegation_response)?) + } + q => bail!("Unsupported staking sudo message: {:?}", q), + } + } +} From 180d049daa706f0b66d6f71a2d66c39923083cbc Mon Sep 17 00:00:00 2001 From: Jakub Bogucki Date: Thu, 18 Aug 2022 15:41:01 +0200 Subject: [PATCH 02/25] multi-test: WIP add distribution module implementation --- packages/multi-test/src/staking.rs | 126 +++++++++++++++++++++++++++-- 1 file changed, 121 insertions(+), 5 deletions(-) diff --git a/packages/multi-test/src/staking.rs b/packages/multi-test/src/staking.rs index 973ce5b41..0aac1ba81 100644 --- a/packages/multi-test/src/staking.rs +++ b/packages/multi-test/src/staking.rs @@ -2,8 +2,9 @@ use anyhow::{bail, Result as AnyResult}; use schemars::JsonSchema; use cosmwasm_std::{ - Addr, Api, Binary, BlockInfo, Coin, Decimal, DistributionMsg, Empty, Event, Querier, - StakingMsg, StakingQuery, Storage, DelegationResponse, FullDelegation, to_binary, coin, Uint128 + coin, to_binary, Addr, Api, Binary, BlockInfo, Coin, Decimal, DelegationResponse, + DistributionMsg, Empty, Event, FullDelegation, Querier, StakingMsg, StakingQuery, Storage, + Uint128, }; use cosmwasm_storage::{prefixed, prefixed_read}; use cw_storage_plus::Map; @@ -133,7 +134,7 @@ impl Module for StakeKeeper { .add_attribute("amount", format!("{}{}", amount.amount, amount.denom))]; self.add_stake(&mut staking_storage, sender, vec![amount])?; Ok(AppResponse { events, data: None }) - }, + } StakingMsg::Undelegate { validator, amount } => { let events = vec![Event::new("undelegate") .add_attribute("from", &validator) @@ -169,7 +170,10 @@ impl Module for StakeKeeper { ) -> AnyResult { let staking_storage = prefixed_read(storage, NAMESPACE_STAKING); match request { - StakingQuery::Delegation { delegator, validator } => { + StakingQuery::Delegation { + delegator, + validator, + } => { // for now validator is ignored, as I want to support only one validator let delegator = api.addr_validate(&delegator)?; let stakes = match self.get_stakes(&staking_storage, &delegator) { @@ -180,7 +184,10 @@ impl Module for StakeKeeper { } }; // set fixed reward ratio 1:10 per delegated amoutn - let reward = coin((stakes.amount / Uint128::new(10)).u128(), stakes.denom.clone()); + let reward = coin( + (stakes.amount / Uint128::new(10)).u128(), + stakes.denom.clone(), + ); let full_delegation_response = FullDelegation { delegator, validator, @@ -194,3 +201,112 @@ impl Module for StakeKeeper { } } } + +#[derive(Default)] +pub struct DistributionKeeper {} + +impl DistributionKeeper { + pub fn new() -> Self { + DistributionKeeper {} + } + + fn get_stakes(&self, storage: &dyn Storage, account: &Addr) -> AnyResult> { + let val = STAKES.may_load(storage, account)?; + Ok(val.unwrap_or_default().into_vec()) + } + + fn set_balance( + &self, + storage: &mut dyn Storage, + account: &Addr, + amount: Vec, + ) -> AnyResult<()> { + let mut stake = NativeBalance(amount); + stake.normalize(); + STAKES.save(storage, account, &stake).map_err(Into::into) + } + + fn add_stake( + &self, + storage: &mut dyn Storage, + to_address: Addr, + amount: Vec, + ) -> AnyResult<()> { + let amount = self.normalize_amount(amount)?; + let b = self.get_stakes(storage, &to_address)?; + let b = NativeBalance(b) + NativeBalance(amount); + self.set_balance(storage, &to_address, b.into_vec()) + } + + /// Filters out all 0 value coins and returns an error if the resulting Vec is empty + fn normalize_amount(&self, amount: Vec) -> AnyResult> { + let res: Vec<_> = amount.into_iter().filter(|x| !x.amount.is_zero()).collect(); + if res.is_empty() { + bail!("Cannot transfer empty coins amount") + } else { + Ok(res) + } + } +} + +impl Distribution for DistributionKeeper {} + +impl Module for DistributionKeeper { + type ExecT = DistributionMsg; + type QueryT = Empty; + type SudoT = Empty; + + fn execute( + &self, + _api: &dyn Api, + storage: &mut dyn Storage, + _router: &dyn CosmosRouter, + _block: &BlockInfo, + sender: Addr, + msg: DistributionMsg, + ) -> AnyResult { + let mut staking_storage = prefixed(storage, NAMESPACE_STAKING); + match msg { + // For now it ignores validator as I want to support only one + DistributionMsg::WithdrawDelegatorReward { validator } => { + let stakes = self.get_stakes(&staking_storage, &sender)?[0].clone(); + // set fixed reward ratio 1:10 per delegated amoutn + let reward = coin( + (stakes.amount / Uint128::new(10)).u128(), + stakes.denom.clone(), + ); + + let events = vec![Event::new("withdraw_delegator_reward") + .add_attribute("validator", &validator) + .add_attribute("sender", &sender) + .add_attribute("amount", format!("{}{}", reward.amount, reward.denom))]; + // add balance to sender + self.add_stake(&mut staking_storage, sender, vec![reward])?; + Ok(AppResponse { events, data: None }) + } + m => bail!("Unsupported distribution message: {:?}", m), + } + } + + fn sudo( + &self, + _api: &dyn Api, + _storage: &mut dyn Storage, + _router: &dyn CosmosRouter, + _block: &BlockInfo, + _msg: Empty, + ) -> AnyResult { + bail!("Something went wrong - Distribution doesn't have sudo messages") + } + + fn query( + &self, + _api: &dyn Api, + _storage: &dyn Storage, + _querier: &dyn Querier, + _block: &BlockInfo, + _request: Empty, + ) -> AnyResult { + bail!("Something went wrong - Distribution doesn't have query messages") + } +} From e5318a6594042420cb77ec2fd8873af59b232f90 Mon Sep 17 00:00:00 2001 From: Jakub Bogucki Date: Thu, 18 Aug 2022 15:46:35 +0200 Subject: [PATCH 03/25] multi-test: Replace FailingDistribution and FailingStaking with DistributionKeeper and StakeKeeper modules --- packages/multi-test/src/app.rs | 22 ++++++++++------------ packages/multi-test/src/lib.rs | 2 +- packages/multi-test/src/staking.rs | 18 ++---------------- packages/multi-test/src/wasm.rs | 10 +++++----- 4 files changed, 18 insertions(+), 34 deletions(-) diff --git a/packages/multi-test/src/app.rs b/packages/multi-test/src/app.rs index 74b6aa78b..18e498418 100644 --- a/packages/multi-test/src/app.rs +++ b/packages/multi-test/src/app.rs @@ -17,9 +17,7 @@ use crate::bank::{Bank, BankKeeper, BankSudo}; use crate::contracts::Contract; use crate::executor::{AppResponse, Executor}; use crate::module::{FailingModule, Module}; -use crate::staking::{ - Distribution, FailingDistribution, FailingStaking, StakeKeeper, Staking, StakingSudo, -}; +use crate::staking::{Distribution, DistributionKeeper, StakeKeeper, Staking, StakingSudo}; use crate::transactions::transactional; use crate::wasm::{ContractData, Wasm, WasmKeeper, WasmSudo}; @@ -47,7 +45,7 @@ pub struct App< Custom = FailingModule, Wasm = WasmKeeper, Staking = StakeKeeper, - Distr = FailingDistribution, + Distr = DistributionKeeper, > { router: Router, api: Api, @@ -78,7 +76,7 @@ impl BasicApp { FailingModule, WasmKeeper, StakeKeeper, - FailingDistribution, + DistributionKeeper, >, &dyn Api, &mut dyn Storage, @@ -100,7 +98,7 @@ where FailingModule, WasmKeeper, StakeKeeper, - FailingDistribution, + DistributionKeeper, >, &dyn Api, &mut dyn Storage, @@ -162,7 +160,7 @@ pub type BasicAppBuilder = AppBuilder< FailingModule, WasmKeeper, StakeKeeper, - FailingDistribution, + DistributionKeeper, >; /// Utility to build App in stages. If particular items wont be set, defaults would be used @@ -185,7 +183,7 @@ impl Default FailingModule, WasmKeeper, StakeKeeper, - FailingDistribution, + DistributionKeeper, > { fn default() -> Self { @@ -201,7 +199,7 @@ impl FailingModule, WasmKeeper, StakeKeeper, - FailingDistribution, + DistributionKeeper, > { /// Creates builder with default components working with empty exec and query messages. @@ -214,7 +212,7 @@ impl wasm: WasmKeeper::new(), custom: FailingModule::new(), staking: StakeKeeper::new(), - distribution: FailingDistribution::new(), + distribution: DistributionKeeper::new(), } } } @@ -227,7 +225,7 @@ impl FailingModule, WasmKeeper, StakeKeeper, - FailingDistribution, + DistributionKeeper, > where ExecC: Debug + Clone + PartialEq + JsonSchema + DeserializeOwned + 'static, @@ -244,7 +242,7 @@ where wasm: WasmKeeper::new(), custom: FailingModule::new(), staking: StakeKeeper::new(), - distribution: FailingDistribution::new(), + distribution: DistributionKeeper::new(), } } } diff --git a/packages/multi-test/src/lib.rs b/packages/multi-test/src/lib.rs index 4f54c991a..572c72c87 100644 --- a/packages/multi-test/src/lib.rs +++ b/packages/multi-test/src/lib.rs @@ -28,5 +28,5 @@ pub use crate::bank::{Bank, BankKeeper, BankSudo}; pub use crate::contracts::{Contract, ContractWrapper}; pub use crate::executor::{AppResponse, Executor}; pub use crate::module::{FailingModule, Module}; -pub use crate::staking::{FailingDistribution, FailingStaking, StakeKeeper, Staking, StakingSudo}; +pub use crate::staking::{DistributionKeeper, StakeKeeper, Staking, StakingSudo}; pub use crate::wasm::{Wasm, WasmKeeper, WasmSudo}; diff --git a/packages/multi-test/src/staking.rs b/packages/multi-test/src/staking.rs index 0aac1ba81..ea747caf3 100644 --- a/packages/multi-test/src/staking.rs +++ b/packages/multi-test/src/staking.rs @@ -12,7 +12,6 @@ use cw_utils::NativeBalance; use crate::app::CosmosRouter; use crate::executor::AppResponse; -use crate::module::FailingModule; use crate::Module; const STAKES: Map<&Addr, NativeBalance> = Map::new("stakes"); @@ -30,16 +29,8 @@ pub enum StakingSudo { pub trait Staking: Module {} -pub type FailingStaking = FailingModule; - -impl Staking for FailingStaking {} - pub trait Distribution: Module {} -pub type FailingDistribution = FailingModule; - -impl Distribution for FailingDistribution {} - #[derive(Default)] pub struct StakeKeeper {} @@ -155,9 +146,7 @@ impl Module for StakeKeeper { _block: &BlockInfo, msg: StakingSudo, ) -> AnyResult { - match msg { - s => bail!("Unsupported staking sudo message: {:?}", s), - } + bail!("Unsupported staking sudo message: {:?}", msg) } fn query( @@ -271,10 +260,7 @@ impl Module for DistributionKeeper { DistributionMsg::WithdrawDelegatorReward { validator } => { let stakes = self.get_stakes(&staking_storage, &sender)?[0].clone(); // set fixed reward ratio 1:10 per delegated amoutn - let reward = coin( - (stakes.amount / Uint128::new(10)).u128(), - stakes.denom.clone(), - ); + let reward = coin((stakes.amount / Uint128::new(10)).u128(), stakes.denom); let events = vec![Event::new("withdraw_delegator_reward") .add_attribute("validator", &validator) diff --git a/packages/multi-test/src/wasm.rs b/packages/multi-test/src/wasm.rs index f615cc0df..bb0490332 100644 --- a/packages/multi-test/src/wasm.rs +++ b/packages/multi-test/src/wasm.rs @@ -941,20 +941,20 @@ mod test { use crate::app::Router; use crate::bank::BankKeeper; use crate::module::FailingModule; + use crate::staking::{DistributionKeeper, StakeKeeper}; use crate::test_helpers::contracts::{caller, error, payout}; use crate::test_helpers::EmptyMsg; use crate::transactions::StorageTransaction; use super::*; - use crate::staking::{FailingDistribution, FailingStaking}; /// Type alias for default build `Router` to make its reference in typical scenario type BasicRouter = Router< BankKeeper, FailingModule, WasmKeeper, - FailingStaking, - FailingDistribution, + StakeKeeper, + DistributionKeeper, >; fn mock_router() -> BasicRouter { @@ -962,8 +962,8 @@ mod test { wasm: WasmKeeper::new(), bank: BankKeeper::new(), custom: FailingModule::new(), - staking: FailingStaking::new(), - distribution: FailingDistribution::new(), + staking: StakeKeeper::new(), + distribution: DistributionKeeper::new(), } } From 2666aa5a4b16e561daf7c3c09cde9a4225bc4c5f Mon Sep 17 00:00:00 2001 From: Jakub Bogucki Date: Fri, 19 Aug 2022 23:40:34 +0200 Subject: [PATCH 04/25] multi-test: Add Distribution to CustomMsg matcher --- packages/multi-test/src/contracts.rs | 1 + packages/multi-test/src/staking.rs | 18 +++++++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/multi-test/src/contracts.rs b/packages/multi-test/src/contracts.rs index 68ef4c30b..b38cdc114 100644 --- a/packages/multi-test/src/contracts.rs +++ b/packages/multi-test/src/contracts.rs @@ -351,6 +351,7 @@ where CosmosMsg::Wasm(wasm) => CosmosMsg::Wasm(wasm), CosmosMsg::Bank(bank) => CosmosMsg::Bank(bank), CosmosMsg::Staking(staking) => CosmosMsg::Staking(staking), + CosmosMsg::Distribution(distribution) => CosmosMsg::Distribution(distribution), CosmosMsg::Custom(_) => unreachable!(), #[cfg(feature = "stargate")] CosmosMsg::Ibc(ibc) => CosmosMsg::Ibc(ibc), diff --git a/packages/multi-test/src/staking.rs b/packages/multi-test/src/staking.rs index ea747caf3..0d0de430d 100644 --- a/packages/multi-test/src/staking.rs +++ b/packages/multi-test/src/staking.rs @@ -177,14 +177,18 @@ impl Module for StakeKeeper { (stakes.amount / Uint128::new(10)).u128(), stakes.denom.clone(), ); - let full_delegation_response = FullDelegation { - delegator, - validator, - amount: stakes, - can_redelegate: coin(0, "testcoin"), - accumulated_rewards: vec![reward], + let full_delegation_response = DelegationResponse { + delegation: Some(FullDelegation { + delegator, + validator, + amount: stakes, + can_redelegate: coin(0, "testcoin"), + accumulated_rewards: vec![reward], + }), }; - Ok(to_binary(&full_delegation_response)?) + + let res = to_binary(&full_delegation_response)?; + Ok(res) } q => bail!("Unsupported staking sudo message: {:?}", q), } From ce4b4c7468bb68c4522937bb569f343f50a969bf Mon Sep 17 00:00:00 2001 From: Jakub Bogucki Date: Sun, 21 Aug 2022 13:27:25 +0200 Subject: [PATCH 05/25] multi-test: Fix bug by removing addition of stake at withdraw_rewards call --- packages/multi-test/src/staking.rs | 38 ++---------------------------- 1 file changed, 2 insertions(+), 36 deletions(-) diff --git a/packages/multi-test/src/staking.rs b/packages/multi-test/src/staking.rs index 0d0de430d..8da4ce75f 100644 --- a/packages/multi-test/src/staking.rs +++ b/packages/multi-test/src/staking.rs @@ -207,39 +207,6 @@ impl DistributionKeeper { let val = STAKES.may_load(storage, account)?; Ok(val.unwrap_or_default().into_vec()) } - - fn set_balance( - &self, - storage: &mut dyn Storage, - account: &Addr, - amount: Vec, - ) -> AnyResult<()> { - let mut stake = NativeBalance(amount); - stake.normalize(); - STAKES.save(storage, account, &stake).map_err(Into::into) - } - - fn add_stake( - &self, - storage: &mut dyn Storage, - to_address: Addr, - amount: Vec, - ) -> AnyResult<()> { - let amount = self.normalize_amount(amount)?; - let b = self.get_stakes(storage, &to_address)?; - let b = NativeBalance(b) + NativeBalance(amount); - self.set_balance(storage, &to_address, b.into_vec()) - } - - /// Filters out all 0 value coins and returns an error if the resulting Vec is empty - fn normalize_amount(&self, amount: Vec) -> AnyResult> { - let res: Vec<_> = amount.into_iter().filter(|x| !x.amount.is_zero()).collect(); - if res.is_empty() { - bail!("Cannot transfer empty coins amount") - } else { - Ok(res) - } - } } impl Distribution for DistributionKeeper {} @@ -258,7 +225,7 @@ impl Module for DistributionKeeper { sender: Addr, msg: DistributionMsg, ) -> AnyResult { - let mut staking_storage = prefixed(storage, NAMESPACE_STAKING); + let staking_storage = prefixed(storage, NAMESPACE_STAKING); match msg { // For now it ignores validator as I want to support only one DistributionMsg::WithdrawDelegatorReward { validator } => { @@ -270,8 +237,7 @@ impl Module for DistributionKeeper { .add_attribute("validator", &validator) .add_attribute("sender", &sender) .add_attribute("amount", format!("{}{}", reward.amount, reward.denom))]; - // add balance to sender - self.add_stake(&mut staking_storage, sender, vec![reward])?; + // TODO: add balance to sender by sending BankMsg transfer Ok(AppResponse { events, data: None }) } m => bail!("Unsupported distribution message: {:?}", m), From 9c1cdb89ba5bc5e7edf138bc2b639cc43834813e Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Fri, 16 Sep 2022 11:02:00 +0200 Subject: [PATCH 06/25] Show how to call "root" on Bank module from Staking --- packages/multi-test/src/staking.rs | 69 ++++++++++++++++++++++++------ 1 file changed, 56 insertions(+), 13 deletions(-) diff --git a/packages/multi-test/src/staking.rs b/packages/multi-test/src/staking.rs index 8da4ce75f..b382d49b0 100644 --- a/packages/multi-test/src/staking.rs +++ b/packages/multi-test/src/staking.rs @@ -2,9 +2,9 @@ use anyhow::{bail, Result as AnyResult}; use schemars::JsonSchema; use cosmwasm_std::{ - coin, to_binary, Addr, Api, Binary, BlockInfo, Coin, Decimal, DelegationResponse, - DistributionMsg, Empty, Event, FullDelegation, Querier, StakingMsg, StakingQuery, Storage, - Uint128, + coin, coins, to_binary, Addr, Api, BankMsg, Binary, BlockInfo, Coin, CustomQuery, Decimal, + DelegationResponse, DistributionMsg, Empty, Event, FullDelegation, Querier, StakingMsg, + StakingQuery, Storage, Uint128, }; use cosmwasm_storage::{prefixed, prefixed_read}; use cw_storage_plus::Map; @@ -12,7 +12,7 @@ use cw_utils::NativeBalance; use crate::app::CosmosRouter; use crate::executor::AppResponse; -use crate::Module; +use crate::{BankSudo, Module}; const STAKES: Map<&Addr, NativeBalance> = Map::new("stakes"); @@ -31,12 +31,16 @@ pub trait Staking: Module {} -#[derive(Default)] -pub struct StakeKeeper {} +pub struct StakeKeeper { + module_addr: Addr, +} impl StakeKeeper { pub fn new() -> Self { - StakeKeeper {} + StakeKeeper { + // define this better?? it is an account for everything held my the staking keeper + module_addr: Addr::unchecked("staking_module"), + } } pub fn init_balance( @@ -107,23 +111,36 @@ impl Module for StakeKeeper { type QueryT = StakingQuery; type SudoT = StakingSudo; - fn execute( + fn execute( &self, - _api: &dyn Api, + api: &dyn Api, storage: &mut dyn Storage, - _router: &dyn CosmosRouter, - _block: &BlockInfo, + router: &dyn CosmosRouter, + block: &BlockInfo, sender: Addr, msg: StakingMsg, ) -> AnyResult { let mut staking_storage = prefixed(storage, NAMESPACE_STAKING); match msg { StakingMsg::Delegate { validator, amount } => { + // TODO: assert amount is the proper denom let events = vec![Event::new("delegate") .add_attribute("recipient", &validator) .add_attribute("sender", &sender) .add_attribute("amount", format!("{}{}", amount.amount, amount.denom))]; - self.add_stake(&mut staking_storage, sender, vec![amount])?; + self.add_stake(&mut staking_storage, sender.clone(), vec![amount.clone()])?; + // move money from sender account to this module (note we can controller sender here) + router.execute( + api, + storage, + block, + sender, + BankMsg::Send { + to_address: self.module_addr.to_string(), + amount: vec![amount], + } + .into(), + )?; Ok(AppResponse { events, data: None }) } StakingMsg::Undelegate { validator, amount } => { @@ -131,7 +148,33 @@ impl Module for StakeKeeper { .add_attribute("from", &validator) .add_attribute("to", &sender) .add_attribute("amount", format!("{}{}", amount.amount, amount.denom))]; - self.remove_stake(&mut staking_storage, sender, vec![amount])?; + self.remove_stake(&mut staking_storage, sender.clone(), vec![amount.clone()])?; + // move token from this module to sender account + // TODO: actually store this so it is released later after unbonding period + // but showing how to do the payback + router.execute( + api, + storage, + block, + self.module_addr.clone(), + BankMsg::Send { + to_address: sender.into_string(), + amount: vec![amount], + } + .into(), + )?; + + // NB: when you need more tokens for staking rewards you can do something like: + router.sudo( + api, + storage, + block, + BankSudo::Mint { + to_address: self.module_addr.to_string(), + amount: coins(123456000, "ucosm"), + } + .into(), + )?; Ok(AppResponse { events, data: None }) } m => bail!("Unsupported staking message: {:?}", m), From 415ea75742c641d1e88e8e54270eac0c2fdf4e8a Mon Sep 17 00:00:00 2001 From: Christoph Otter Date: Tue, 27 Sep 2022 16:18:41 +0200 Subject: [PATCH 07/25] Change event signatures to match cosmos-sdk --- packages/multi-test/src/staking.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/multi-test/src/staking.rs b/packages/multi-test/src/staking.rs index b382d49b0..0fb51d7b4 100644 --- a/packages/multi-test/src/staking.rs +++ b/packages/multi-test/src/staking.rs @@ -124,10 +124,11 @@ impl Module for StakeKeeper { match msg { StakingMsg::Delegate { validator, amount } => { // TODO: assert amount is the proper denom + // see https://github.com/cosmos/cosmos-sdk/blob/v0.46.1/x/staking/keeper/msg_server.go#L251-L256 let events = vec![Event::new("delegate") - .add_attribute("recipient", &validator) - .add_attribute("sender", &sender) - .add_attribute("amount", format!("{}{}", amount.amount, amount.denom))]; + .add_attribute("validator", &validator) + .add_attribute("amount", format!("{}{}", amount.amount, amount.denom)) + .add_attribute("new_shares", amount.amount.to_string())]; // TODO: calculate shares? self.add_stake(&mut staking_storage, sender.clone(), vec![amount.clone()])?; // move money from sender account to this module (note we can controller sender here) router.execute( @@ -144,10 +145,11 @@ impl Module for StakeKeeper { Ok(AppResponse { events, data: None }) } StakingMsg::Undelegate { validator, amount } => { - let events = vec![Event::new("undelegate") - .add_attribute("from", &validator) - .add_attribute("to", &sender) - .add_attribute("amount", format!("{}{}", amount.amount, amount.denom))]; + // see https://github.com/cosmos/cosmos-sdk/blob/v0.46.1/x/staking/keeper/msg_server.go#L378-L383 + let events = vec![Event::new("unbond") + .add_attribute("validator", &validator) + .add_attribute("amount", format!("{}{}", amount.amount, amount.denom)) + .add_attribute("completion_time", "2022-09-27T14:00:00+00:00")]; // TODO: actual date? self.remove_stake(&mut staking_storage, sender.clone(), vec![amount.clone()])?; // move token from this module to sender account // TODO: actually store this so it is released later after unbonding period From 95f28301256f9189638010ed0d56a11c1fdf3be7 Mon Sep 17 00:00:00 2001 From: Christoph Otter Date: Tue, 27 Sep 2022 16:19:58 +0200 Subject: [PATCH 08/25] Handle redelegation msg in StakeKeeper --- packages/multi-test/src/staking.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/multi-test/src/staking.rs b/packages/multi-test/src/staking.rs index 0fb51d7b4..0799ae85b 100644 --- a/packages/multi-test/src/staking.rs +++ b/packages/multi-test/src/staking.rs @@ -179,6 +179,23 @@ impl Module for StakeKeeper { )?; Ok(AppResponse { events, data: None }) } + StakingMsg::Redelegate { + src_validator, + dst_validator, + amount, + } => { + // see https://github.com/cosmos/cosmos-sdk/blob/v0.46.1/x/staking/keeper/msg_server.go#L316-L322 + let events = vec![Event::new("redelegate") + .add_attribute("source_validator", &src_validator) + .add_attribute("destination_validator", &dst_validator) + .add_attribute("amount", format!("{}{}", amount.amount, amount.denom))]; + + // this is not a noop, since there is validation regarding the amount + self.remove_stake(&mut staking_storage, sender.clone(), vec![amount.clone()])?; + self.add_stake(&mut staking_storage, sender.clone(), vec![amount.clone()])?; + + Ok(AppResponse { events, data: None }) + } m => bail!("Unsupported staking message: {:?}", m), } } From 947f96597d488c330be7e6b2f439ab912b194d02 Mon Sep 17 00:00:00 2001 From: Christoph Otter Date: Tue, 27 Sep 2022 16:32:04 +0200 Subject: [PATCH 09/25] Remove unnecessary clones --- packages/multi-test/src/staking.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/multi-test/src/staking.rs b/packages/multi-test/src/staking.rs index 0799ae85b..97f973964 100644 --- a/packages/multi-test/src/staking.rs +++ b/packages/multi-test/src/staking.rs @@ -192,7 +192,7 @@ impl Module for StakeKeeper { // this is not a noop, since there is validation regarding the amount self.remove_stake(&mut staking_storage, sender.clone(), vec![amount.clone()])?; - self.add_stake(&mut staking_storage, sender.clone(), vec![amount.clone()])?; + self.add_stake(&mut staking_storage, sender, vec![amount])?; Ok(AppResponse { events, data: None }) } From bd36ab1957f7fb547afae0fab87bc1432f6a1a45 Mon Sep 17 00:00:00 2001 From: Christoph Otter Date: Tue, 4 Oct 2022 16:51:29 +0200 Subject: [PATCH 10/25] Add staking reward config and more staking queries --- packages/multi-test/src/staking.rs | 127 +++++++++++++++++++++++------ 1 file changed, 102 insertions(+), 25 deletions(-) diff --git a/packages/multi-test/src/staking.rs b/packages/multi-test/src/staking.rs index 97f973964..47c854c09 100644 --- a/packages/multi-test/src/staking.rs +++ b/packages/multi-test/src/staking.rs @@ -2,12 +2,13 @@ use anyhow::{bail, Result as AnyResult}; use schemars::JsonSchema; use cosmwasm_std::{ - coin, coins, to_binary, Addr, Api, BankMsg, Binary, BlockInfo, Coin, CustomQuery, Decimal, + coin, coins, from_slice, to_binary, Addr, AllDelegationsResponse, AllValidatorsResponse, Api, + BankMsg, Binary, BlockInfo, BondedDenomResponse, Coin, CustomQuery, Decimal, Delegation, DelegationResponse, DistributionMsg, Empty, Event, FullDelegation, Querier, StakingMsg, - StakingQuery, Storage, Uint128, + StakingQuery, Storage, Validator, ValidatorResponse, }; use cosmwasm_storage::{prefixed, prefixed_read}; -use cw_storage_plus::Map; +use cw_storage_plus::{Item, Map}; use cw_utils::NativeBalance; use crate::app::CosmosRouter; @@ -15,6 +16,7 @@ use crate::executor::AppResponse; use crate::{BankSudo, Module}; const STAKES: Map<&Addr, NativeBalance> = Map::new("stakes"); +const REWARD_RATIO: Item = Item::new("reward_ratio"); pub const NAMESPACE_STAKING: &[u8] = b"staking"; @@ -25,6 +27,9 @@ pub enum StakingSudo { validator: String, percentage: Decimal, }, + UpdateRewardsRatio { + ratio: Decimal, + }, } pub trait Staking: Module {} @@ -33,13 +38,28 @@ pub trait Distribution: Module Self { + Self::new() + } } impl StakeKeeper { pub fn new() -> Self { StakeKeeper { - // define this better?? it is an account for everything held my the staking keeper + // define this better?? it is an account for everything held by the staking keeper module_addr: Addr::unchecked("staking_module"), + validator: Validator { + address: "testvaloper1".to_string(), + commission: Decimal::percent(10), + max_commission: Decimal::percent(20), + max_change_rate: Decimal::percent(1), + }, + bonded_denom: "TOKEN".to_string(), } } @@ -53,6 +73,28 @@ impl StakeKeeper { self.set_balance(&mut storage, account, amount) } + pub fn set_reward_ratio(&self, storage: &mut dyn Storage, value: &Decimal) -> AnyResult<()> { + let mut storage = prefixed(storage, NAMESPACE_STAKING); + REWARD_RATIO.save(&mut storage, value).map_err(Into::into) + } + + /// Calculates the staking reward for the given stake based on the fixed ratio set using + /// `set_reward_ratio` + fn calculate_rewards(&self, storage: &dyn Storage, stake: &Coin) -> AnyResult { + let reward_radio = self.get_reward_ratio(storage)?; + Ok(coin( + (stake.amount * reward_radio).u128(), + stake.denom.clone(), + )) + } + + /// Returns the set reward ratio, or `0.1` if none was set + fn get_reward_ratio(&self, storage: &dyn Storage) -> AnyResult { + let ratio = REWARD_RATIO.may_load(storage)?; + + Ok(ratio.unwrap_or_else(|| Decimal::from_ratio(1u128, 10u128))) + } + fn get_stakes(&self, storage: &dyn Storage, account: &Addr) -> AnyResult> { let val = STAKES.may_load(storage, account)?; Ok(val.unwrap_or_default().into_vec()) @@ -203,12 +245,19 @@ impl Module for StakeKeeper { fn sudo( &self, _api: &dyn Api, - _storage: &mut dyn Storage, + storage: &mut dyn Storage, _router: &dyn CosmosRouter, _block: &BlockInfo, msg: StakingSudo, ) -> AnyResult { - bail!("Unsupported staking sudo message: {:?}", msg) + let mut staking_storage = prefixed(storage, NAMESPACE_STAKING); + match msg { + StakingSudo::Slash { .. } => todo!("slash validator"), + StakingSudo::UpdateRewardsRatio { ratio } => { + self.set_reward_ratio(&mut staking_storage, &ratio)?; + Ok(AppResponse::default()) + } + } } fn query( @@ -221,11 +270,30 @@ impl Module for StakeKeeper { ) -> AnyResult { let staking_storage = prefixed_read(storage, NAMESPACE_STAKING); match request { + StakingQuery::BondedDenom {} => Ok(to_binary(&BondedDenomResponse { + denom: self.bonded_denom.clone(), + })?), + StakingQuery::AllDelegations { delegator } => { + let delegator = api.addr_validate(&delegator)?; + let stakes = self.get_stakes(&staking_storage, &delegator)?; + Ok(to_binary(&AllDelegationsResponse { + delegations: vec![Delegation { + delegator, + validator: self.validator.address.clone(), + amount: stakes + .get(0) + .cloned() + .unwrap_or_else(|| coin(0, &self.bonded_denom)), + }], + })?) + } StakingQuery::Delegation { delegator, validator, } => { - // for now validator is ignored, as I want to support only one validator + if validator != self.validator.address { + bail!("non-existent validator {}", validator); + } let delegator = api.addr_validate(&delegator)?; let stakes = match self.get_stakes(&staking_storage, &delegator) { Ok(stakes) => stakes[0].clone(), @@ -234,11 +302,8 @@ impl Module for StakeKeeper { return Ok(to_binary(&response)?); } }; - // set fixed reward ratio 1:10 per delegated amoutn - let reward = coin( - (stakes.amount / Uint128::new(10)).u128(), - stakes.denom.clone(), - ); + // calculate rewards using fixed ratio + let reward = self.calculate_rewards(&staking_storage, &stakes)?; let full_delegation_response = DelegationResponse { delegation: Some(FullDelegation { delegator, @@ -252,6 +317,16 @@ impl Module for StakeKeeper { let res = to_binary(&full_delegation_response)?; Ok(res) } + StakingQuery::AllValidators {} => Ok(to_binary(&AllValidatorsResponse { + validators: vec![self.validator.clone()], + })?), + StakingQuery::Validator { address } => Ok(to_binary(&ValidatorResponse { + validator: if self.validator.address == address { + Some(self.validator.clone()) + } else { + None + }, + })?), q => bail!("Unsupported staking sudo message: {:?}", q), } } @@ -264,11 +339,6 @@ impl DistributionKeeper { pub fn new() -> Self { DistributionKeeper {} } - - fn get_stakes(&self, storage: &dyn Storage, account: &Addr) -> AnyResult> { - let val = STAKES.may_load(storage, account)?; - Ok(val.unwrap_or_default().into_vec()) - } } impl Distribution for DistributionKeeper {} @@ -278,22 +348,29 @@ impl Module for DistributionKeeper { type QueryT = Empty; type SudoT = Empty; - fn execute( + fn execute( &self, - _api: &dyn Api, + api: &dyn Api, storage: &mut dyn Storage, - _router: &dyn CosmosRouter, - _block: &BlockInfo, + router: &dyn CosmosRouter, + block: &BlockInfo, sender: Addr, msg: DistributionMsg, ) -> AnyResult { - let staking_storage = prefixed(storage, NAMESPACE_STAKING); + // let staking_storage = prefixed(storage, NAMESPACE_STAKING); match msg { // For now it ignores validator as I want to support only one DistributionMsg::WithdrawDelegatorReward { validator } => { - let stakes = self.get_stakes(&staking_storage, &sender)?[0].clone(); - // set fixed reward ratio 1:10 per delegated amoutn - let reward = coin((stakes.amount / Uint128::new(10)).u128(), stakes.denom); + let response: DelegationResponse = from_slice(&router.query( + api, + storage, + block, + cosmwasm_std::QueryRequest::Staking(StakingQuery::Delegation { + delegator: sender.to_string(), + validator: validator.clone(), + }), + )?)?; + let reward = &response.delegation.unwrap().accumulated_rewards[0]; let events = vec![Event::new("withdraw_delegator_reward") .add_attribute("validator", &validator) From d0030c9786cb4646f1c4122453828997c6f568f3 Mon Sep 17 00:00:00 2001 From: Christoph Otter Date: Wed, 5 Oct 2022 17:30:48 +0200 Subject: [PATCH 11/25] Add multiple validators and slashing --- packages/multi-test/src/staking.rs | 549 ++++++++++++++++++++++++----- 1 file changed, 461 insertions(+), 88 deletions(-) diff --git a/packages/multi-test/src/staking.rs b/packages/multi-test/src/staking.rs index 47c854c09..199341744 100644 --- a/packages/multi-test/src/staking.rs +++ b/packages/multi-test/src/staking.rs @@ -1,22 +1,82 @@ -use anyhow::{bail, Result as AnyResult}; +use std::collections::{BTreeSet, VecDeque}; + +use anyhow::{anyhow, bail, Result as AnyResult}; use schemars::JsonSchema; use cosmwasm_std::{ - coin, coins, from_slice, to_binary, Addr, AllDelegationsResponse, AllValidatorsResponse, Api, - BankMsg, Binary, BlockInfo, BondedDenomResponse, Coin, CustomQuery, Decimal, Delegation, - DelegationResponse, DistributionMsg, Empty, Event, FullDelegation, Querier, StakingMsg, - StakingQuery, Storage, Validator, ValidatorResponse, + coin, coins, ensure, ensure_eq, from_slice, to_binary, Addr, AllDelegationsResponse, + AllValidatorsResponse, Api, BankMsg, Binary, BlockInfo, BondedDenomResponse, Coin, CustomQuery, + Decimal, Delegation, DelegationResponse, DistributionMsg, Empty, Event, FullDelegation, + Querier, StakingMsg, StakingQuery, Storage, Uint128, Validator, ValidatorResponse, }; use cosmwasm_storage::{prefixed, prefixed_read}; use cw_storage_plus::{Item, Map}; -use cw_utils::NativeBalance; +use serde::{Deserialize, Serialize}; use crate::app::CosmosRouter; use crate::executor::AppResponse; use crate::{BankSudo, Module}; -const STAKES: Map<&Addr, NativeBalance> = Map::new("stakes"); -const REWARD_RATIO: Item = Item::new("reward_ratio"); +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +struct StakeInfo { + /// The block height when this stake was last edited. This is needed for slashing + height: u64, + + /// The number of shares of this validator the staker has + shares: Decimal, +} + +impl StakeInfo { + /// The stake of this delegator. Make sure to pass the correct validator in + pub fn stake(&self, validator: &ValidatorInfo) -> Uint128 { + self.shares / validator.total_shares * validator.stake + } +} +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +struct ValidatorInfo { + /// The stakers that have staked with this validator + stakers: BTreeSet, + /// The whole stake of all stakers + stake: Uint128, + /// The block height when this validator was last edited. This is needed for rewards calculation + height: u64, + /// The total number of shares this validator has issued, only used internally for calculating rewards + total_shares: Decimal, + /// The number of available rewards. This is updated whenever a + available_rewards: Decimal, +} + +impl ValidatorInfo { + pub fn new(block_height: u64) -> Self { + Self { + stakers: BTreeSet::new(), + stake: Uint128::zero(), + height: block_height, + total_shares: Decimal::zero(), + available_rewards: Decimal::zero(), + } + } + /// Returns the amount of shares a delegator gets for staking the given amount of tokens (bonded_denom) at this point in time. + /// This should usually be `1:1` unless the delegator was slashed. + pub fn shares_for(&self, stake: Uint128) -> Decimal { + if self.stake.is_zero() { + // first staker always gets 1:1 + Decimal::one() + } else { + Decimal::from_ratio(stake, 1u128) * self.total_shares + / Decimal::from_ratio(self.stake, 1u128) + } + } +} + +const STAKES: Map<(&Addr, &Addr), StakeInfo> = Map::new("stakes"); +const VALIDATOR_MAP: Map<&Addr, Validator> = Map::new("validator_map"); +/// Additional vec of validators, in case the `iterator` feature is disabled +const VALIDATORS: Item> = Item::new("validators"); +/// Contains additional info for each validator +const VALIDATOR_INFO: Map<&Addr, ValidatorInfo> = Map::new("validator_info"); +// TODO: replace with `Deque` +// const UNBONDING_QUEUE: Item> = Item::new("unbonding_queue"); pub const NAMESPACE_STAKING: &[u8] = b"staking"; @@ -27,9 +87,9 @@ pub enum StakingSudo { validator: String, percentage: Decimal, }, - UpdateRewardsRatio { - ratio: Decimal, - }, + /// Causes the unbonding queue to be processed. + /// This needs to be triggered manually, since there is no good place to do this right now + ProcessQueue {}, } pub trait Staking: Module {} @@ -38,8 +98,9 @@ pub trait Distribution: Module, + validator: &Addr, + amount: Coin, ) -> AnyResult<()> { let mut storage = prefixed(storage, NAMESPACE_STAKING); - self.set_balance(&mut storage, account, amount) + + self.add_stake(&mut storage, block, account, validator, amount) } - pub fn set_reward_ratio(&self, storage: &mut dyn Storage, value: &Decimal) -> AnyResult<()> { + /// Add a new validator available for staking + pub fn add_validator( + &self, + api: &dyn Api, + storage: &mut dyn Storage, + block: &BlockInfo, + validator: Validator, + ) -> AnyResult<()> { let mut storage = prefixed(storage, NAMESPACE_STAKING); - REWARD_RATIO.save(&mut storage, value).map_err(Into::into) + let val_addr = api.addr_validate(&validator.address)?; + if VALIDATOR_MAP.may_load(&storage, &val_addr)?.is_some() { + bail!( + "Cannot add validator {}, since a validator with that address already exists", + val_addr + ); + } + + VALIDATOR_MAP.save(&mut storage, &val_addr, &validator)?; + let mut vec = VALIDATORS.may_load(&storage)?.unwrap_or_default(); + vec.push(validator); + VALIDATORS.save(&mut storage, &vec)?; + VALIDATOR_INFO.save(&mut storage, &val_addr, &ValidatorInfo::new(block.height))?; + Ok(()) } /// Calculates the staking reward for the given stake based on the fixed ratio set using /// `set_reward_ratio` - fn calculate_rewards(&self, storage: &dyn Storage, stake: &Coin) -> AnyResult { - let reward_radio = self.get_reward_ratio(storage)?; - Ok(coin( - (stake.amount * reward_radio).u128(), - stake.denom.clone(), - )) + fn calculate_rewards(&self, storage: &dyn Storage, stake: Uint128) -> AnyResult { + todo!("calculate rewards"); } - /// Returns the set reward ratio, or `0.1` if none was set - fn get_reward_ratio(&self, storage: &dyn Storage) -> AnyResult { - let ratio = REWARD_RATIO.may_load(storage)?; - - Ok(ratio.unwrap_or_else(|| Decimal::from_ratio(1u128, 10u128))) + /// Returns the single validator with the given address (or `None` if there is no such validator) + fn get_validator(&self, storage: &dyn Storage, address: &Addr) -> AnyResult> { + Ok(VALIDATOR_MAP.may_load(storage, address)?) } - fn get_stakes(&self, storage: &dyn Storage, account: &Addr) -> AnyResult> { - let val = STAKES.may_load(storage, account)?; - Ok(val.unwrap_or_default().into_vec()) + /// Returns all available validators + fn get_validators(&self, storage: &dyn Storage) -> AnyResult> { + Ok(VALIDATORS.may_load(storage)?.unwrap_or_default()) } - fn set_balance( + fn get_stake( &self, - storage: &mut dyn Storage, + storage: &dyn Storage, account: &Addr, - amount: Vec, - ) -> AnyResult<()> { - let mut stake = NativeBalance(amount); - stake.normalize(); - STAKES.save(storage, account, &stake).map_err(Into::into) + validator: &Addr, + ) -> AnyResult { + let val = STAKES.may_load(storage, (account, validator))?; + let validator_info = VALIDATOR_INFO.may_load(storage, validator)?; + Ok(val + .zip(validator_info) + .map(|(s, validator_info)| s.stake(&validator_info)) + .unwrap_or_default()) } fn add_stake( &self, storage: &mut dyn Storage, - to_address: Addr, - amount: Vec, + block: &BlockInfo, + to_address: &Addr, + validator: &Addr, + amount: Coin, ) -> AnyResult<()> { - let amount = self.normalize_amount(amount)?; - let b = self.get_stakes(storage, &to_address)?; - let b = NativeBalance(b) + NativeBalance(amount); - self.set_balance(storage, &to_address, b.into_vec()) + self.validate_denom(&amount)?; + self.validate_nonzero(&amount)?; + self.update_stake(storage, block, to_address, validator, amount.amount, false) } fn remove_stake( &self, storage: &mut dyn Storage, - from_address: Addr, - amount: Vec, + block: &BlockInfo, + from_address: &Addr, + validator: &Addr, + amount: Coin, + ) -> AnyResult<()> { + self.validate_denom(&amount)?; + self.validate_nonzero(&amount)?; + self.update_stake(storage, block, from_address, validator, amount.amount, true) + } + + fn update_stake( + &self, + storage: &mut dyn Storage, + block: &BlockInfo, + delegator: &Addr, + validator: &Addr, + amount: impl Into, + sub: bool, + ) -> AnyResult<()> { + let amount = amount.into(); + + let mut validator_info = VALIDATOR_INFO + .may_load(storage, validator)? + .unwrap_or_else(|| ValidatorInfo::new(block.height)); + let mut stake_info = STAKES + .may_load(storage, (delegator, validator))? + .unwrap_or_else(|| StakeInfo { + height: block.height, + shares: Decimal::zero(), + }); + + // TODO: update rewards and validator_info.height + + if sub { + // remove the corresponding amount of shares + let shares = validator_info.shares_for(amount); + stake_info.shares -= shares; + + validator_info.stake = validator_info.stake.checked_sub(amount)?; + validator_info.total_shares -= shares; + } else { + let new_shares = validator_info.shares_for(amount); + stake_info.shares += new_shares; + + validator_info.stake = validator_info.stake.checked_add(amount)?; + validator_info.total_shares += new_shares; + } + + // save updated values + if stake_info.shares.is_zero() { + // no more stake, so remove + STAKES.remove(storage, (delegator, validator)); + validator_info.stakers.remove(delegator); + } else { + STAKES.save(storage, (delegator, validator), &stake_info)?; + validator_info.stakers.insert(delegator.clone()); + } + // save updated validator info + VALIDATOR_INFO.save(storage, validator, &validator_info)?; + + Ok(()) + } + + fn slash( + &self, + storage: &mut dyn Storage, + validator: &Addr, + percentage: Decimal, ) -> AnyResult<()> { - let amount = self.normalize_amount(amount)?; - let a = self.get_stakes(storage, &from_address)?; - let a = (NativeBalance(a) - amount)?; - self.set_balance(storage, &from_address, a.into_vec()) + let mut validator_info = VALIDATOR_INFO + .may_load(storage, validator)? + .ok_or_else(|| anyhow!("validator not found"))?; + + // TODO: handle rewards? Either update them before slashing or set them to zero, depending on the slashing logic + + let remaining_percentage = Decimal::one() - percentage; + validator_info.stake = validator_info.stake * remaining_percentage; + + // if the stake is completely gone, we clear all stakers and reinitialize the validator + if validator_info.stake.is_zero() { + // need to remove all stakes + for delegator in validator_info.stakers.iter() { + STAKES.remove(storage, (delegator, validator)); + } + validator_info.stakers.clear(); + validator_info.total_shares = Decimal::zero(); + } + VALIDATOR_INFO.save(storage, validator, &validator_info)?; + Ok(()) + } + + fn process_queue(&self, storage: &mut dyn Storage) -> AnyResult<()> { + // let mut queue = UNBONDING_QUEUE.may_load(storage)?.unwrap_or_default(); + + // while queue.front().is_some() { + // let Some((delegator, info)) = queue.pop_front(); + // } + // Ok(()) + + todo!("process queue") } /// Filters out all 0 value coins and returns an error if the resulting Vec is empty @@ -144,6 +314,31 @@ impl StakeKeeper { Ok(res) } } + + fn validate_nonzero(&self, amount: &Coin) -> AnyResult<()> { + ensure!(!amount.amount.is_zero(), anyhow!("cannot delegate 0 coins")); + Ok(()) + } + + // Asserts that the given coin has the proper denominator + fn validate_denom(&self, amount: &Coin) -> AnyResult<()> { + ensure_eq!( + amount.denom, + self.bonded_denom, + anyhow!( + "cannot delegate coins of denominator {}, only of {}", + amount.denom, + self.bonded_denom + ) + ); + Ok(()) + } + + // Asserts that the given coin has the proper denominator + fn validate_percentage(&self, percentage: Decimal) -> AnyResult<()> { + ensure!(percentage <= Decimal::one(), anyhow!("expected percentage")); + Ok(()) + } } impl Staking for StakeKeeper {} @@ -165,13 +360,20 @@ impl Module for StakeKeeper { let mut staking_storage = prefixed(storage, NAMESPACE_STAKING); match msg { StakingMsg::Delegate { validator, amount } => { - // TODO: assert amount is the proper denom + let validator = api.addr_validate(&validator)?; + // see https://github.com/cosmos/cosmos-sdk/blob/v0.46.1/x/staking/keeper/msg_server.go#L251-L256 let events = vec![Event::new("delegate") .add_attribute("validator", &validator) .add_attribute("amount", format!("{}{}", amount.amount, amount.denom)) .add_attribute("new_shares", amount.amount.to_string())]; // TODO: calculate shares? - self.add_stake(&mut staking_storage, sender.clone(), vec![amount.clone()])?; + self.add_stake( + &mut staking_storage, + block, + &sender, + &validator, + amount.clone(), + )?; // move money from sender account to this module (note we can controller sender here) router.execute( api, @@ -187,12 +389,20 @@ impl Module for StakeKeeper { Ok(AppResponse { events, data: None }) } StakingMsg::Undelegate { validator, amount } => { + let validator = api.addr_validate(&validator)?; + // see https://github.com/cosmos/cosmos-sdk/blob/v0.46.1/x/staking/keeper/msg_server.go#L378-L383 let events = vec![Event::new("unbond") .add_attribute("validator", &validator) .add_attribute("amount", format!("{}{}", amount.amount, amount.denom)) .add_attribute("completion_time", "2022-09-27T14:00:00+00:00")]; // TODO: actual date? - self.remove_stake(&mut staking_storage, sender.clone(), vec![amount.clone()])?; + self.remove_stake( + &mut staking_storage, + block, + &sender, + &validator, + amount.clone(), + )?; // move token from this module to sender account // TODO: actually store this so it is released later after unbonding period // but showing how to do the payback @@ -226,15 +436,22 @@ impl Module for StakeKeeper { dst_validator, amount, } => { + let src_validator = api.addr_validate(&src_validator)?; + let dst_validator = api.addr_validate(&dst_validator)?; // see https://github.com/cosmos/cosmos-sdk/blob/v0.46.1/x/staking/keeper/msg_server.go#L316-L322 let events = vec![Event::new("redelegate") .add_attribute("source_validator", &src_validator) .add_attribute("destination_validator", &dst_validator) .add_attribute("amount", format!("{}{}", amount.amount, amount.denom))]; - // this is not a noop, since there is validation regarding the amount - self.remove_stake(&mut staking_storage, sender.clone(), vec![amount.clone()])?; - self.add_stake(&mut staking_storage, sender, vec![amount])?; + self.remove_stake( + &mut staking_storage, + block, + &sender, + &src_validator, + amount.clone(), + )?; + self.add_stake(&mut staking_storage, block, &sender, &dst_validator, amount)?; Ok(AppResponse { events, data: None }) } @@ -244,7 +461,7 @@ impl Module for StakeKeeper { fn sudo( &self, - _api: &dyn Api, + api: &dyn Api, storage: &mut dyn Storage, _router: &dyn CosmosRouter, _block: &BlockInfo, @@ -252,9 +469,19 @@ impl Module for StakeKeeper { ) -> AnyResult { let mut staking_storage = prefixed(storage, NAMESPACE_STAKING); match msg { - StakingSudo::Slash { .. } => todo!("slash validator"), - StakingSudo::UpdateRewardsRatio { ratio } => { - self.set_reward_ratio(&mut staking_storage, &ratio)?; + StakingSudo::Slash { + validator, + percentage, + } => { + let validator = api.addr_validate(&validator)?; + self.validate_percentage(percentage)?; + + self.slash(&mut staking_storage, &validator, percentage)?; + + Ok(AppResponse::default()) + } + StakingSudo::ProcessQueue {} => { + self.process_queue(&mut staking_storage)?; Ok(AppResponse::default()) } } @@ -275,40 +502,52 @@ impl Module for StakeKeeper { })?), StakingQuery::AllDelegations { delegator } => { let delegator = api.addr_validate(&delegator)?; - let stakes = self.get_stakes(&staking_storage, &delegator)?; - Ok(to_binary(&AllDelegationsResponse { - delegations: vec![Delegation { - delegator, - validator: self.validator.address.clone(), - amount: stakes - .get(0) - .cloned() - .unwrap_or_else(|| coin(0, &self.bonded_denom)), - }], - })?) + let validators = self.get_validators(&staking_storage)?; + + let res: AnyResult> = validators + .into_iter() + .map(|validator| { + let delegator = delegator.clone(); + let amount = self.get_stake( + &staking_storage, + &delegator, + &Addr::unchecked(&validator.address), + )?; + + Ok(Delegation { + delegator, + validator: validator.address, + amount: coin(amount.u128(), &self.bonded_denom), + }) + }) + .collect(); + + Ok(to_binary(&AllDelegationsResponse { delegations: res? })?) } StakingQuery::Delegation { delegator, validator, } => { - if validator != self.validator.address { + let validator_addr = Addr::unchecked(&validator); + let validator_obj = self.get_validator(storage, &validator_addr)?; + if validator_obj.is_none() { bail!("non-existent validator {}", validator); } let delegator = api.addr_validate(&delegator)?; - let stakes = match self.get_stakes(&staking_storage, &delegator) { - Ok(stakes) => stakes[0].clone(), + let stakes = match self.get_stake(&staking_storage, &delegator, &validator_addr) { + Ok(stakes) => stakes, Err(_) => { let response = DelegationResponse { delegation: None }; return Ok(to_binary(&response)?); } }; // calculate rewards using fixed ratio - let reward = self.calculate_rewards(&staking_storage, &stakes)?; + let reward = self.calculate_rewards(&staking_storage, stakes)?; let full_delegation_response = DelegationResponse { delegation: Some(FullDelegation { delegator, validator, - amount: stakes, + amount: coin(stakes.u128(), &self.bonded_denom), can_redelegate: coin(0, "testcoin"), accumulated_rewards: vec![reward], }), @@ -318,14 +557,10 @@ impl Module for StakeKeeper { Ok(res) } StakingQuery::AllValidators {} => Ok(to_binary(&AllValidatorsResponse { - validators: vec![self.validator.clone()], + validators: self.get_validators(&staking_storage)?, })?), StakingQuery::Validator { address } => Ok(to_binary(&ValidatorResponse { - validator: if self.validator.address == address { - Some(self.validator.clone()) - } else { - None - }, + validator: self.get_validator(&staking_storage, &Addr::unchecked(address))?, })?), q => bail!("Unsupported staking sudo message: {:?}", q), } @@ -405,3 +640,141 @@ impl Module for DistributionKeeper { bail!("Something went wrong - Distribution doesn't have query messages") } } + +#[cfg(test)] +mod test { + use crate::app::MockRouter; + + use super::*; + + use cosmwasm_std::testing::{mock_env, MockApi, MockStorage}; + + #[test] + fn add_get_validators() { + let api = MockApi::default(); + let mut store = MockStorage::new(); + let stake = StakeKeeper::new(); + let block = mock_env().block; + + // add validator + let valoper1 = Validator { + address: "testvaloper1".to_string(), + commission: Decimal::percent(10), + max_commission: Decimal::percent(20), + max_change_rate: Decimal::percent(1), + }; + stake + .add_validator(&api, &mut store, &block, valoper1.clone()) + .unwrap(); + + // get it + let staking_storage = prefixed_read(&store, NAMESPACE_STAKING); + let val = stake + .get_validator( + &staking_storage, + &api.addr_validate("testvaloper1").unwrap(), + ) + .unwrap() + .unwrap(); + assert_eq!(val, valoper1); + + // try to add with same address + let valoper1_fake = Validator { + address: "testvaloper1".to_string(), + commission: Decimal::percent(1), + max_commission: Decimal::percent(10), + max_change_rate: Decimal::percent(100), + }; + stake + .add_validator(&api, &mut store, &block, valoper1_fake) + .unwrap_err(); + + // should still be original value + let staking_storage = prefixed_read(&store, NAMESPACE_STAKING); + let val = stake + .get_validator( + &staking_storage, + &api.addr_validate("testvaloper1").unwrap(), + ) + .unwrap() + .unwrap(); + assert_eq!(val, valoper1); + } + + #[test] + fn validator_slashing() { + let api = MockApi::default(); + let router = MockRouter::default(); + let mut store = MockStorage::new(); + let stake = StakeKeeper::new(); + let block = mock_env().block; + + let delegator = Addr::unchecked("delegator"); + let validator = api.addr_validate("testvaloper1").unwrap(); + + // add validator + let valoper1 = Validator { + address: "testvaloper1".to_string(), + commission: Decimal::percent(10), + max_commission: Decimal::percent(20), + max_change_rate: Decimal::percent(1), + }; + stake + .add_validator(&api, &mut store, &block, valoper1) + .unwrap(); + + // stake 100 tokens + let mut staking_storage = prefixed(&mut store, NAMESPACE_STAKING); + stake + .add_stake( + &mut staking_storage, + &block, + &delegator, + &validator, + coin(100, "TOKEN"), + ) + .unwrap(); + + // slash 50% + stake + .sudo( + &api, + &mut store, + &router, + &block, + StakingSudo::Slash { + validator: "testvaloper1".to_string(), + percentage: Decimal::percent(50), + }, + ) + .unwrap(); + + // check stake + let staking_storage = prefixed(&mut store, NAMESPACE_STAKING); + let stake_left = stake + .get_stake(&staking_storage, &delegator, &validator) + .unwrap(); + assert_eq!(stake_left.u128(), 50, "should have slashed 50%"); + + // slash all + stake + .sudo( + &api, + &mut store, + &router, + &block, + StakingSudo::Slash { + validator: "testvaloper1".to_string(), + percentage: Decimal::percent(100), + }, + ) + .unwrap(); + + // check stake + let staking_storage = prefixed(&mut store, NAMESPACE_STAKING); + let stake_left = stake + .get_stake(&staking_storage, &delegator, &validator) + .unwrap(); + assert_eq!(stake_left.u128(), 0, "should have slashed whole stake"); + } +} From 8b11c7c394dd691764a8ffd6008239970c435e79 Mon Sep 17 00:00:00 2001 From: Christoph Otter Date: Wed, 5 Oct 2022 17:50:15 +0200 Subject: [PATCH 12/25] Process unbonding queue --- packages/multi-test/src/staking.rs | 95 ++++++++++++++++-------------- 1 file changed, 52 insertions(+), 43 deletions(-) diff --git a/packages/multi-test/src/staking.rs b/packages/multi-test/src/staking.rs index 199341744..2251663cc 100644 --- a/packages/multi-test/src/staking.rs +++ b/packages/multi-test/src/staking.rs @@ -7,7 +7,7 @@ use cosmwasm_std::{ coin, coins, ensure, ensure_eq, from_slice, to_binary, Addr, AllDelegationsResponse, AllValidatorsResponse, Api, BankMsg, Binary, BlockInfo, BondedDenomResponse, Coin, CustomQuery, Decimal, Delegation, DelegationResponse, DistributionMsg, Empty, Event, FullDelegation, - Querier, StakingMsg, StakingQuery, Storage, Uint128, Validator, ValidatorResponse, + Querier, StakingMsg, StakingQuery, Storage, Timestamp, Uint128, Validator, ValidatorResponse, }; use cosmwasm_storage::{prefixed, prefixed_read}; use cw_storage_plus::{Item, Map}; @@ -76,7 +76,7 @@ const VALIDATORS: Item> = Item::new("validators"); /// Contains additional info for each validator const VALIDATOR_INFO: Map<&Addr, ValidatorInfo> = Map::new("validator_info"); // TODO: replace with `Deque` -// const UNBONDING_QUEUE: Item> = Item::new("unbonding_queue"); +const UNBONDING_QUEUE: Item> = Item::new("unbonding_queue"); pub const NAMESPACE_STAKING: &[u8] = b"staking"; @@ -294,17 +294,6 @@ impl StakeKeeper { Ok(()) } - fn process_queue(&self, storage: &mut dyn Storage) -> AnyResult<()> { - // let mut queue = UNBONDING_QUEUE.may_load(storage)?.unwrap_or_default(); - - // while queue.front().is_some() { - // let Some((delegator, info)) = queue.pop_front(); - // } - // Ok(()) - - todo!("process queue") - } - /// Filters out all 0 value coins and returns an error if the resulting Vec is empty fn normalize_amount(&self, amount: Vec) -> AnyResult> { let res: Vec<_> = amount.into_iter().filter(|x| !x.amount.is_zero()).collect(); @@ -390,6 +379,8 @@ impl Module for StakeKeeper { } StakingMsg::Undelegate { validator, amount } => { let validator = api.addr_validate(&validator)?; + self.validate_denom(&amount)?; + self.validate_nonzero(&amount)?; // see https://github.com/cosmos/cosmos-sdk/blob/v0.46.1/x/staking/keeper/msg_server.go#L378-L383 let events = vec![Event::new("unbond") @@ -403,32 +394,27 @@ impl Module for StakeKeeper { &validator, amount.clone(), )?; - // move token from this module to sender account - // TODO: actually store this so it is released later after unbonding period - // but showing how to do the payback - router.execute( - api, - storage, - block, - self.module_addr.clone(), - BankMsg::Send { - to_address: sender.into_string(), - amount: vec![amount], - } - .into(), - )?; - - // NB: when you need more tokens for staking rewards you can do something like: - router.sudo( - api, - storage, - block, - BankSudo::Mint { - to_address: self.module_addr.to_string(), - amount: coins(123456000, "ucosm"), - } - .into(), - )?; + // add tokens to unbonding queue + let mut queue = UNBONDING_QUEUE + .may_load(&staking_storage)? + .unwrap_or_default(); + queue.push_back(( + sender.clone(), + block.time.plus_seconds(self.unbonding_time), + amount.amount.u128(), + )); + + // // NB: when you need more tokens for staking rewards you can do something like: + // router.sudo( + // api, + // storage, + // block, + // BankSudo::Mint { + // to_address: self.module_addr.to_string(), + // amount: coins(123456000, "ucosm"), + // } + // .into(), + // )?; Ok(AppResponse { events, data: None }) } StakingMsg::Redelegate { @@ -459,12 +445,12 @@ impl Module for StakeKeeper { } } - fn sudo( + fn sudo( &self, api: &dyn Api, storage: &mut dyn Storage, - _router: &dyn CosmosRouter, - _block: &BlockInfo, + router: &dyn CosmosRouter, + block: &BlockInfo, msg: StakingSudo, ) -> AnyResult { let mut staking_storage = prefixed(storage, NAMESPACE_STAKING); @@ -481,7 +467,30 @@ impl Module for StakeKeeper { Ok(AppResponse::default()) } StakingSudo::ProcessQueue {} => { - self.process_queue(&mut staking_storage)?; + let mut queue = UNBONDING_QUEUE.may_load(storage)?.unwrap_or_default(); + + loop { + match queue.front() { + // assuming the queue is sorted by payout_at + Some((_, payout_at, _)) if payout_at <= &block.time => { + // remove from queue + let (delegator, _, amount) = queue.pop_front().unwrap(); + + router.execute( + api, + storage, + block, + self.module_addr.clone(), + BankMsg::Send { + to_address: delegator.into_string(), + amount: vec![coin(amount, &self.bonded_denom)], + } + .into(), + )?; + } + _ => break, + } + } Ok(AppResponse::default()) } } From 294f4a24fd7de0ee35cd80195206f6d818151613 Mon Sep 17 00:00:00 2001 From: Christoph Otter Date: Thu, 6 Oct 2022 14:22:11 +0200 Subject: [PATCH 13/25] Add (buggy) rewards calculation --- packages/multi-test/src/staking.rs | 657 +++++++++++++++++++++++------ 1 file changed, 530 insertions(+), 127 deletions(-) diff --git a/packages/multi-test/src/staking.rs b/packages/multi-test/src/staking.rs index 2251663cc..4cc97628c 100644 --- a/packages/multi-test/src/staking.rs +++ b/packages/multi-test/src/staking.rs @@ -4,10 +4,10 @@ use anyhow::{anyhow, bail, Result as AnyResult}; use schemars::JsonSchema; use cosmwasm_std::{ - coin, coins, ensure, ensure_eq, from_slice, to_binary, Addr, AllDelegationsResponse, - AllValidatorsResponse, Api, BankMsg, Binary, BlockInfo, BondedDenomResponse, Coin, CustomQuery, - Decimal, Delegation, DelegationResponse, DistributionMsg, Empty, Event, FullDelegation, - Querier, StakingMsg, StakingQuery, Storage, Timestamp, Uint128, Validator, ValidatorResponse, + coin, ensure, ensure_eq, to_binary, Addr, AllDelegationsResponse, AllValidatorsResponse, Api, + BankMsg, Binary, BlockInfo, BondedDenomResponse, Coin, CustomQuery, Decimal, Delegation, + DelegationResponse, DistributionMsg, Empty, Event, FullDelegation, Querier, StakingMsg, + StakingQuery, Storage, Timestamp, Uint128, Validator, ValidatorResponse, }; use cosmwasm_storage::{prefixed, prefixed_read}; use cw_storage_plus::{Item, Map}; @@ -17,43 +17,58 @@ use crate::app::CosmosRouter; use crate::executor::AppResponse; use crate::{BankSudo, Module}; -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -struct StakeInfo { - /// The block height when this stake was last edited. This is needed for slashing - height: u64, - - /// The number of shares of this validator the staker has - shares: Decimal, +// Contains some general staking parameters +#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, JsonSchema)] +pub struct StakingInfo { + /// The denominator of the staking token + bonded_denom: String, + /// Time between unbonding and receiving tokens in seconds + unbonding_time: u64, + /// Interest rate per year (60 * 60 * 24 * 365 seconds) + apr: Decimal, } -impl StakeInfo { +/// The number of (conceptual) shares of this validator the staker has. These can be fractional shares +/// Used to calculate the stake. If the validator is slashed, this might not be the same as the stake. +#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, JsonSchema)] +struct Shares(Decimal); + +impl Shares { /// The stake of this delegator. Make sure to pass the correct validator in pub fn stake(&self, validator: &ValidatorInfo) -> Uint128 { - self.shares / validator.total_shares * validator.stake + self.0 / validator.total_shares * validator.stake + } + + pub fn rewards(&self, validator: &ValidatorInfo, rewards: Decimal) -> Decimal { + self.0 * rewards / validator.total_shares } } + +/// Holds some operational data about a validator #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] struct ValidatorInfo { /// The stakers that have staked with this validator stakers: BTreeSet, /// The whole stake of all stakers stake: Uint128, - /// The block height when this validator was last edited. This is needed for rewards calculation - height: u64, + /// The block time when this validator's rewards were last update. This is needed for rewards calculation. + last_rewards_calculation: Timestamp, /// The total number of shares this validator has issued, only used internally for calculating rewards total_shares: Decimal, - /// The number of available rewards. This is updated whenever a - available_rewards: Decimal, + /// The number of available rewards. This is updated in `calculate_rewards`. + /// It is needed to save the current rewards somewhere before adding / removing stake, + /// since the new stake should only apply to future interest, not past interest. + calculated_rewards: Decimal, } impl ValidatorInfo { - pub fn new(block_height: u64) -> Self { + pub fn new(block_time: Timestamp) -> Self { Self { stakers: BTreeSet::new(), stake: Uint128::zero(), - height: block_height, + last_rewards_calculation: block_time, total_shares: Decimal::zero(), - available_rewards: Decimal::zero(), + calculated_rewards: Decimal::zero(), } } /// Returns the amount of shares a delegator gets for staking the given amount of tokens (bonded_denom) at this point in time. @@ -69,13 +84,15 @@ impl ValidatorInfo { } } -const STAKES: Map<(&Addr, &Addr), StakeInfo> = Map::new("stakes"); +const STAKING_INFO: Item = Item::new("staking_info"); +const STAKES: Map<(&Addr, &Addr), Shares> = Map::new("stakes"); const VALIDATOR_MAP: Map<&Addr, Validator> = Map::new("validator_map"); /// Additional vec of validators, in case the `iterator` feature is disabled const VALIDATORS: Item> = Item::new("validators"); /// Contains additional info for each validator const VALIDATOR_INFO: Map<&Addr, ValidatorInfo> = Map::new("validator_info"); -// TODO: replace with `Deque` +/// The queue of unbonding operations. This is needed because unbonding has a waiting time. See [`StakeKeeper`] +/// TODO: replace with `Deque` const UNBONDING_QUEUE: Item> = Item::new("unbonding_queue"); pub const NAMESPACE_STAKING: &[u8] = b"staking"; @@ -83,12 +100,15 @@ pub const NAMESPACE_STAKING: &[u8] = b"staking"; // We need to expand on this, but we will need this to properly test out staking #[derive(Clone, std::fmt::Debug, PartialEq, Eq, JsonSchema)] pub enum StakingSudo { + /// Slashes the given percentage of the validator's stake. + /// For now, you cannot slash after the fact in tests. Slash { validator: String, percentage: Decimal, }, /// Causes the unbonding queue to be processed. - /// This needs to be triggered manually, since there is no good place to do this right now + /// This needs to be triggered manually, since there is no good place to do this right now. + /// In cosmos-sdk, this is done in `EndBlock`, but we don't have that here. ProcessQueue {}, } @@ -98,9 +118,6 @@ pub trait Distribution: Module Self { StakeKeeper { - // define this better?? it is an account for everything held by the staking keeper + // The address of the staking module. This holds all staked tokens. module_addr: Addr::unchecked("staking_module"), - bonded_denom: "TOKEN".to_string(), - unbonding_time: 60, } } + /// Provides some general parameters to the stake keeper + pub fn setup(&self, storage: &mut dyn Storage, staking_info: StakingInfo) -> AnyResult<()> { + let mut storage = prefixed(storage, NAMESPACE_STAKING); + + STAKING_INFO.save(&mut storage, &staking_info)?; + Ok(()) + } + pub fn init_stake( &self, storage: &mut dyn Storage, @@ -141,6 +164,7 @@ impl StakeKeeper { validator: Validator, ) -> AnyResult<()> { let mut storage = prefixed(storage, NAMESPACE_STAKING); + let val_addr = api.addr_validate(&validator.address)?; if VALIDATOR_MAP.may_load(&storage, &val_addr)?.is_some() { bail!( @@ -153,69 +177,224 @@ impl StakeKeeper { let mut vec = VALIDATORS.may_load(&storage)?.unwrap_or_default(); vec.push(validator); VALIDATORS.save(&mut storage, &vec)?; - VALIDATOR_INFO.save(&mut storage, &val_addr, &ValidatorInfo::new(block.height))?; + VALIDATOR_INFO.save(&mut storage, &val_addr, &ValidatorInfo::new(block.time))?; Ok(()) } - /// Calculates the staking reward for the given stake based on the fixed ratio set using - /// `set_reward_ratio` - fn calculate_rewards(&self, storage: &dyn Storage, stake: Uint128) -> AnyResult { - todo!("calculate rewards"); + fn get_staking_info(staking_storage: &dyn Storage) -> AnyResult { + Ok(STAKING_INFO + .may_load(staking_storage)? + .unwrap_or_else(|| StakingInfo { + bonded_denom: "TOKEN".to_string(), + unbonding_time: 60, + apr: Decimal::percent(10), + })) + } + + /// Returns the rewards of the given delegator at the given validator + pub fn get_rewards( + &self, + storage: &dyn Storage, + block: &BlockInfo, + delegator: &Addr, + validator: &Addr, + ) -> AnyResult> { + let staking_storage = prefixed_read(storage, NAMESPACE_STAKING); + + let validator_obj = match self.get_validator(&staking_storage, validator)? { + Some(validator) => validator, + None => bail!("non-existent validator {}", validator), + }; + // calculate rewards using fixed ratio + let shares = match STAKES.load(&staking_storage, (delegator, validator)) { + Ok(stakes) => stakes, + Err(_) => { + return Ok(None); + } + }; + let validator_info = VALIDATOR_INFO.load(&staking_storage, validator)?; + + Self::get_rewards_internal( + &staking_storage, + block, + &shares, + &validator_obj, + &validator_info, + ) + .map(Some) + } + + fn get_rewards_internal( + staking_storage: &dyn Storage, + block: &BlockInfo, + shares: &Shares, + validator: &Validator, + validator_info: &ValidatorInfo, + ) -> AnyResult { + let staking_info = Self::get_staking_info(staking_storage)?; + + println!( + "old delegator rewards: {} * {} / {}", + validator_info.calculated_rewards, shares.0, validator_info.total_shares + ); + + // calculate missing rewards without updating the validator to reduce rounding errors + let missing_validator_rewards = Self::calculate_rewards( + block.time, + validator_info.last_rewards_calculation, + staking_info.apr, + validator.commission, + validator_info.stake, + ); + let validator_rewards = validator_info.calculated_rewards + missing_validator_rewards; + + // calculate the delegator's share of those + let delegator_rewards = shares.rewards(validator_info, validator_rewards); + + println!( + "new validator / delegator rewards: {} / {}", + validator_rewards, delegator_rewards + ); + + Ok(Coin { + denom: staking_info.bonded_denom, + amount: Uint128::new(1) * delegator_rewards, // multiplying by 1 to convert Decimal to Uint128 + }) + } + + /// Calculates the rewards that are due since the last calculation. + fn calculate_rewards( + current_time: Timestamp, + since: Timestamp, + interest_rate: Decimal, + validator_commission: Decimal, + stake: Uint128, + ) -> Decimal { + // calculate time since last update (in seconds) + let time_diff = current_time.minus_seconds(since.seconds()).seconds(); + + // using decimal here to reduce rounding error when calling this function a lot + let reward = Decimal::from_ratio(stake, 1u128) + * interest_rate + * Decimal::from_ratio(time_diff, 1u128) + / Decimal::from_ratio(60u128 * 60 * 24 * 365, 1u128); + let commission = reward * validator_commission; + + println!( + "calculated new: 10% * {} - 10% comm. = {}", + stake, + reward - commission + ); + + reward - commission + } + + /// Updates the staking reward for the given validator. This mutates the validator info, + /// but does not save it. + /// Always call this to update rewards before changing a validator stake. + fn update_rewards( + block: &BlockInfo, + staking_info: &StakingInfo, + validator_info: &mut ValidatorInfo, + validator: &Validator, + ) -> AnyResult<()> { + if validator_info.last_rewards_calculation >= block.time { + return Ok(()); + } + + let new_rewards = Self::calculate_rewards( + block.time, + validator_info.last_rewards_calculation, + staking_info.apr, + validator.commission, + validator_info.stake, + ); + + // update validator info, but only if there is at least 1 new token + // Less than one token would not change anything, as only full tokens are presented + // outside of the keeper. + if new_rewards >= Decimal::one() { + validator_info.last_rewards_calculation = block.time; + validator_info.calculated_rewards += new_rewards; + } + Ok(()) } /// Returns the single validator with the given address (or `None` if there is no such validator) - fn get_validator(&self, storage: &dyn Storage, address: &Addr) -> AnyResult> { - Ok(VALIDATOR_MAP.may_load(storage, address)?) + fn get_validator( + &self, + staking_storage: &dyn Storage, + address: &Addr, + ) -> AnyResult> { + Ok(VALIDATOR_MAP.may_load(staking_storage, address)?) } /// Returns all available validators - fn get_validators(&self, storage: &dyn Storage) -> AnyResult> { - Ok(VALIDATORS.may_load(storage)?.unwrap_or_default()) + fn get_validators(&self, staking_storage: &dyn Storage) -> AnyResult> { + Ok(VALIDATORS.may_load(staking_storage)?.unwrap_or_default()) } fn get_stake( &self, - storage: &dyn Storage, + staking_storage: &dyn Storage, account: &Addr, validator: &Addr, - ) -> AnyResult { - let val = STAKES.may_load(storage, (account, validator))?; - let validator_info = VALIDATOR_INFO.may_load(storage, validator)?; - Ok(val - .zip(validator_info) - .map(|(s, validator_info)| s.stake(&validator_info)) - .unwrap_or_default()) + ) -> AnyResult { + let shares = STAKES.may_load(staking_storage, (account, validator))?; + let staking_info = Self::get_staking_info(staking_storage)?; + let validator_info = VALIDATOR_INFO.may_load(staking_storage, validator)?; + Ok(Coin { + amount: shares + .zip(validator_info) + .map(|(s, validator_info)| s.stake(&validator_info)) + .unwrap_or_default(), + denom: staking_info.bonded_denom, + }) } fn add_stake( &self, - storage: &mut dyn Storage, + staking_storage: &mut dyn Storage, block: &BlockInfo, to_address: &Addr, validator: &Addr, amount: Coin, ) -> AnyResult<()> { - self.validate_denom(&amount)?; + self.validate_denom(staking_storage, &amount)?; self.validate_nonzero(&amount)?; - self.update_stake(storage, block, to_address, validator, amount.amount, false) + self.update_stake( + staking_storage, + block, + to_address, + validator, + amount.amount, + false, + ) } fn remove_stake( &self, - storage: &mut dyn Storage, + staking_storage: &mut dyn Storage, block: &BlockInfo, from_address: &Addr, validator: &Addr, amount: Coin, ) -> AnyResult<()> { - self.validate_denom(&amount)?; + self.validate_denom(staking_storage, &amount)?; self.validate_nonzero(&amount)?; - self.update_stake(storage, block, from_address, validator, amount.amount, true) + self.update_stake( + staking_storage, + block, + from_address, + validator, + amount.amount, + true, + ) } fn update_stake( &self, - storage: &mut dyn Storage, + staking_storage: &mut dyn Storage, block: &BlockInfo, delegator: &Addr, validator: &Addr, @@ -225,55 +404,57 @@ impl StakeKeeper { let amount = amount.into(); let mut validator_info = VALIDATOR_INFO - .may_load(storage, validator)? - .unwrap_or_else(|| ValidatorInfo::new(block.height)); + .may_load(staking_storage, validator)? + .unwrap_or_else(|| ValidatorInfo::new(block.time)); let mut stake_info = STAKES - .may_load(storage, (delegator, validator))? - .unwrap_or_else(|| StakeInfo { - height: block.height, - shares: Decimal::zero(), - }); - - // TODO: update rewards and validator_info.height + .may_load(staking_storage, (delegator, validator))? + .unwrap_or_else(|| Shares(Decimal::zero())); + + // update rewards for this validator + if !amount.is_zero() { + let validator_obj = VALIDATOR_MAP.load(staking_storage, validator)?; + let staking_info = Self::get_staking_info(staking_storage)?; + Self::update_rewards(block, &staking_info, &mut validator_info, &validator_obj)?; + } + // now, we can update the stake if sub { - // remove the corresponding amount of shares let shares = validator_info.shares_for(amount); - stake_info.shares -= shares; + stake_info.0 -= shares; validator_info.stake = validator_info.stake.checked_sub(amount)?; validator_info.total_shares -= shares; } else { let new_shares = validator_info.shares_for(amount); - stake_info.shares += new_shares; + stake_info.0 += new_shares; validator_info.stake = validator_info.stake.checked_add(amount)?; validator_info.total_shares += new_shares; } // save updated values - if stake_info.shares.is_zero() { + if stake_info.0.is_zero() { // no more stake, so remove - STAKES.remove(storage, (delegator, validator)); + STAKES.remove(staking_storage, (delegator, validator)); validator_info.stakers.remove(delegator); } else { - STAKES.save(storage, (delegator, validator), &stake_info)?; + STAKES.save(staking_storage, (delegator, validator), &stake_info)?; validator_info.stakers.insert(delegator.clone()); } // save updated validator info - VALIDATOR_INFO.save(storage, validator, &validator_info)?; + VALIDATOR_INFO.save(staking_storage, validator, &validator_info)?; Ok(()) } fn slash( &self, - storage: &mut dyn Storage, + staking_storage: &mut dyn Storage, validator: &Addr, percentage: Decimal, ) -> AnyResult<()> { let mut validator_info = VALIDATOR_INFO - .may_load(storage, validator)? + .may_load(staking_storage, validator)? .ok_or_else(|| anyhow!("validator not found"))?; // TODO: handle rewards? Either update them before slashing or set them to zero, depending on the slashing logic @@ -285,39 +466,30 @@ impl StakeKeeper { if validator_info.stake.is_zero() { // need to remove all stakes for delegator in validator_info.stakers.iter() { - STAKES.remove(storage, (delegator, validator)); + STAKES.remove(staking_storage, (delegator, validator)); } validator_info.stakers.clear(); validator_info.total_shares = Decimal::zero(); } - VALIDATOR_INFO.save(storage, validator, &validator_info)?; + VALIDATOR_INFO.save(staking_storage, validator, &validator_info)?; Ok(()) } - /// Filters out all 0 value coins and returns an error if the resulting Vec is empty - fn normalize_amount(&self, amount: Vec) -> AnyResult> { - let res: Vec<_> = amount.into_iter().filter(|x| !x.amount.is_zero()).collect(); - if res.is_empty() { - bail!("Cannot transfer empty coins amount") - } else { - Ok(res) - } - } - fn validate_nonzero(&self, amount: &Coin) -> AnyResult<()> { ensure!(!amount.amount.is_zero(), anyhow!("cannot delegate 0 coins")); Ok(()) } // Asserts that the given coin has the proper denominator - fn validate_denom(&self, amount: &Coin) -> AnyResult<()> { + fn validate_denom(&self, staking_storage: &dyn Storage, amount: &Coin) -> AnyResult<()> { + let staking_info = Self::get_staking_info(staking_storage)?; ensure_eq!( amount.denom, - self.bonded_denom, + staking_info.bonded_denom, anyhow!( "cannot delegate coins of denominator {}, only of {}", amount.denom, - self.bonded_denom + staking_info.bonded_denom ) ); Ok(()) @@ -379,7 +551,7 @@ impl Module for StakeKeeper { } StakingMsg::Undelegate { validator, amount } => { let validator = api.addr_validate(&validator)?; - self.validate_denom(&amount)?; + self.validate_denom(&staking_storage, &amount)?; self.validate_nonzero(&amount)?; // see https://github.com/cosmos/cosmos-sdk/blob/v0.46.1/x/staking/keeper/msg_server.go#L378-L383 @@ -395,26 +567,15 @@ impl Module for StakeKeeper { amount.clone(), )?; // add tokens to unbonding queue + let staking_info = Self::get_staking_info(&staking_storage)?; let mut queue = UNBONDING_QUEUE .may_load(&staking_storage)? .unwrap_or_default(); queue.push_back(( sender.clone(), - block.time.plus_seconds(self.unbonding_time), + block.time.plus_seconds(staking_info.unbonding_time), amount.amount.u128(), )); - - // // NB: when you need more tokens for staking rewards you can do something like: - // router.sudo( - // api, - // storage, - // block, - // BankSudo::Mint { - // to_address: self.module_addr.to_string(), - // amount: coins(123456000, "ucosm"), - // } - // .into(), - // )?; Ok(AppResponse { events, data: None }) } StakingMsg::Redelegate { @@ -467,7 +628,9 @@ impl Module for StakeKeeper { Ok(AppResponse::default()) } StakingSudo::ProcessQueue {} => { - let mut queue = UNBONDING_QUEUE.may_load(storage)?.unwrap_or_default(); + let mut queue = UNBONDING_QUEUE + .may_load(&staking_storage)? + .unwrap_or_default(); loop { match queue.front() { @@ -476,6 +639,8 @@ impl Module for StakeKeeper { // remove from queue let (delegator, _, amount) = queue.pop_front().unwrap(); + let staking_storage = prefixed_read(storage, NAMESPACE_STAKING); + let staking_info = Self::get_staking_info(&staking_storage)?; router.execute( api, storage, @@ -483,7 +648,7 @@ impl Module for StakeKeeper { self.module_addr.clone(), BankMsg::Send { to_address: delegator.into_string(), - amount: vec![coin(amount, &self.bonded_denom)], + amount: vec![coin(amount, &staking_info.bonded_denom)], } .into(), )?; @@ -501,13 +666,13 @@ impl Module for StakeKeeper { api: &dyn Api, storage: &dyn Storage, _querier: &dyn Querier, - _block: &BlockInfo, + block: &BlockInfo, request: StakingQuery, ) -> AnyResult { let staking_storage = prefixed_read(storage, NAMESPACE_STAKING); match request { StakingQuery::BondedDenom {} => Ok(to_binary(&BondedDenomResponse { - denom: self.bonded_denom.clone(), + denom: Self::get_staking_info(&staking_storage)?.bonded_denom, })?), StakingQuery::AllDelegations { delegator } => { let delegator = api.addr_validate(&delegator)?; @@ -526,7 +691,7 @@ impl Module for StakeKeeper { Ok(Delegation { delegator, validator: validator.address, - amount: coin(amount.u128(), &self.bonded_denom), + amount, }) }) .collect(); @@ -538,25 +703,34 @@ impl Module for StakeKeeper { validator, } => { let validator_addr = Addr::unchecked(&validator); - let validator_obj = self.get_validator(storage, &validator_addr)?; - if validator_obj.is_none() { - bail!("non-existent validator {}", validator); - } + let validator_obj = match self.get_validator(&staking_storage, &validator_addr)? { + Some(validator) => validator, + None => bail!("non-existent validator {}", validator), + }; let delegator = api.addr_validate(&delegator)?; - let stakes = match self.get_stake(&staking_storage, &delegator, &validator_addr) { + // calculate rewards using fixed ratio + let shares = match STAKES.load(&staking_storage, (&delegator, &validator_addr)) { Ok(stakes) => stakes, Err(_) => { let response = DelegationResponse { delegation: None }; return Ok(to_binary(&response)?); } }; - // calculate rewards using fixed ratio - let reward = self.calculate_rewards(&staking_storage, stakes)?; + let validator_info = VALIDATOR_INFO.load(&staking_storage, &validator_addr)?; + let stakes = shares.stake(&validator_info); + let reward = Self::get_rewards_internal( + &staking_storage, + block, + &shares, + &validator_obj, + &validator_info, + )?; + let staking_info = Self::get_staking_info(&staking_storage)?; let full_delegation_response = DelegationResponse { delegation: Some(FullDelegation { delegator, validator, - amount: coin(stakes.u128(), &self.bonded_denom), + amount: coin(stakes.u128(), staking_info.bonded_denom), can_redelegate: coin(0, "testcoin"), accumulated_rewards: vec![reward], }), @@ -601,26 +775,54 @@ impl Module for DistributionKeeper { sender: Addr, msg: DistributionMsg, ) -> AnyResult { - // let staking_storage = prefixed(storage, NAMESPACE_STAKING); + let mut staking_storage = prefixed(storage, NAMESPACE_STAKING); match msg { - // For now it ignores validator as I want to support only one DistributionMsg::WithdrawDelegatorReward { validator } => { - let response: DelegationResponse = from_slice(&router.query( + let validator_addr = api.addr_validate(&validator)?; + + let staking_info = STAKING_INFO.load(&staking_storage)?; + let mut validator_info = VALIDATOR_INFO.load(&staking_storage, &validator_addr)?; + let validator_obj = VALIDATOR_MAP.load(&staking_storage, &validator_addr)?; + + // update the validator's rewards + StakeKeeper::update_rewards( + block, + &staking_info, + &mut validator_info, + &validator_obj, + )?; + + // remove delegator's share of the rewards + let shares = STAKES.load(&staking_storage, (&sender, &validator_addr))?; + let rewards = shares.rewards(&validator_info, validator_info.calculated_rewards); + validator_info.calculated_rewards -= rewards; + let rewards = Uint128::new(1) * rewards; // convert to Uint128 + + // save updated validator_info + VALIDATOR_INFO.save(&mut staking_storage, &validator_addr, &validator_info)?; + + // directly mint rewards to delegator + router.sudo( api, storage, block, - cosmwasm_std::QueryRequest::Staking(StakingQuery::Delegation { - delegator: sender.to_string(), - validator: validator.clone(), - }), - )?)?; - let reward = &response.delegation.unwrap().accumulated_rewards[0]; + BankSudo::Mint { + to_address: sender.to_string(), + amount: vec![Coin { + amount: rewards, + denom: staking_info.bonded_denom.clone(), + }], + } + .into(), + )?; let events = vec![Event::new("withdraw_delegator_reward") .add_attribute("validator", &validator) .add_attribute("sender", &sender) - .add_attribute("amount", format!("{}{}", reward.amount, reward.denom))]; - // TODO: add balance to sender by sending BankMsg transfer + .add_attribute( + "amount", + format!("{}{}", rewards, staking_info.bonded_denom), + )]; Ok(AppResponse { events, data: None }) } m => bail!("Unsupported distribution message: {:?}", m), @@ -652,12 +854,31 @@ impl Module for DistributionKeeper { #[cfg(test)] mod test { - use crate::app::MockRouter; + use crate::{app::MockRouter, BankKeeper, FailingModule, Router, WasmKeeper}; use super::*; use cosmwasm_std::testing::{mock_env, MockApi, MockStorage}; + /// Type alias for default build `Router` to make its reference in typical scenario + type BasicRouter = Router< + BankKeeper, + FailingModule, + WasmKeeper, + StakeKeeper, + DistributionKeeper, + >; + + fn mock_router() -> BasicRouter { + Router { + wasm: WasmKeeper::new(), + bank: BankKeeper::new(), + custom: FailingModule::new(), + staking: StakeKeeper::new(), + distribution: DistributionKeeper::new(), + } + } + #[test] fn add_get_validators() { let api = MockApi::default(); @@ -763,7 +984,7 @@ mod test { let stake_left = stake .get_stake(&staking_storage, &delegator, &validator) .unwrap(); - assert_eq!(stake_left.u128(), 50, "should have slashed 50%"); + assert_eq!(stake_left.amount.u128(), 50, "should have slashed 50%"); // slash all stake @@ -784,6 +1005,188 @@ mod test { let stake_left = stake .get_stake(&staking_storage, &delegator, &validator) .unwrap(); - assert_eq!(stake_left.u128(), 0, "should have slashed whole stake"); + assert_eq!( + stake_left.amount.u128(), + 0, + "should have slashed whole stake" + ); + } + + fn setup_test( + apr: Decimal, + validator_commission: Decimal, + ) -> (MockApi, MockStorage, BasicRouter, BlockInfo, Addr) { + let api = MockApi::default(); + let router = mock_router(); + let mut store = MockStorage::new(); + let block = mock_env().block; + + let validator = api.addr_validate("testvaloper1").unwrap(); + + // setup 10% APR + router + .staking + .setup( + &mut store, + StakingInfo { + bonded_denom: "TOKEN".to_string(), + unbonding_time: 60, + apr, + }, + ) + .unwrap(); + + // add validator + let valoper1 = Validator { + address: "testvaloper1".to_string(), + commission: validator_commission, + max_commission: Decimal::percent(100), + max_change_rate: Decimal::percent(1), + }; + router + .staking + .add_validator(&api, &mut store, &block, valoper1) + .unwrap(); + + (api, store, router, block, validator) + } + + #[test] + fn rewards_work_for_single_delegator() { + let (api, mut store, router, mut block, validator) = + setup_test(Decimal::percent(10), Decimal::percent(10)); + let stake = &router.staking; + let distr = &router.distribution; + let delegator = Addr::unchecked("delegator"); + + let mut staking_storage = prefixed(&mut store, NAMESPACE_STAKING); + // stake 200 tokens + stake + .add_stake( + &mut staking_storage, + &block, + &delegator, + &validator, + coin(200, "TOKEN"), + ) + .unwrap(); + + // wait 1/2 year + block.time = block.time.plus_seconds(60 * 60 * 24 * 365 / 2); + + // should now have 200 * 10% / 2 - 10% commission = 9 tokens reward + let rewards = stake + .get_rewards(&store, &block, &delegator, &validator) + .unwrap() + .unwrap(); + assert_eq!(rewards.amount.u128(), 9, "should have 9 tokens reward"); + + // withdraw rewards + distr + .execute( + &api, + &mut store, + &router, + &block, + delegator.clone(), + DistributionMsg::WithdrawDelegatorReward { + validator: validator.to_string(), + }, + ) + .unwrap(); + + // should have no rewards left + let rewards = stake + .get_rewards(&store, &block, &delegator, &validator) + .unwrap() + .unwrap(); + assert_eq!(rewards.amount.u128(), 0); + + // wait another 1/2 year + block.time = block.time.plus_seconds(60 * 60 * 24 * 365 / 2); + // should now have 9 tokens again + let rewards = stake + .get_rewards(&store, &block, &delegator, &validator) + .unwrap() + .unwrap(); + assert_eq!(rewards.amount.u128(), 9); + } + + #[test] + fn rewards_work_for_multiple_delegators() { + let (api, mut store, router, mut block, validator) = + setup_test(Decimal::percent(10), Decimal::percent(10)); + let stake = &router.staking; + let distr = &router.distribution; + let delegator1 = Addr::unchecked("delegator1"); + let delegator2 = Addr::unchecked("delegator2"); + + let mut staking_storage = prefixed(&mut store, NAMESPACE_STAKING); + + // add 100 stake to delegator1 and 200 to delegator2 + stake + .add_stake( + &mut staking_storage, + &block, + &delegator1, + &validator, + coin(100, "TOKEN"), + ) + .unwrap(); + stake + .add_stake( + &mut staking_storage, + &block, + &delegator2, + &validator, + coin(200, "TOKEN"), + ) + .unwrap(); + + // wait 1 year + block.time = block.time.plus_seconds(60 * 60 * 24 * 365); + + // delegator1 should now have 100 * 10% - 10% commission = 9 tokens + let rewards = stake + .get_rewards(&store, &block, &delegator1, &validator) + .unwrap() + .unwrap(); + assert_eq!(rewards.amount.u128(), 9); + + // delegator1 should now have 200 * 10% - 10% commission = 18 tokens + let rewards = stake + .get_rewards(&store, &block, &delegator2, &validator) + .unwrap() + .unwrap(); + assert_eq!(rewards.amount.u128(), 18); + + // delegator1 stakes 100 more + let mut staking_storage = prefixed(&mut store, NAMESPACE_STAKING); + stake + .add_stake( + &mut staking_storage, + &block, + &delegator1, + &validator, + coin(100, "TOKEN"), + ) + .unwrap(); + + // wait another year + block.time = block.time.plus_seconds(60 * 60 * 24 * 365); + + // delegator1 should now have 9 + 200 * 10% - 10% commission = 27 tokens + let rewards = stake + .get_rewards(&store, &block, &delegator1, &validator) + .unwrap() + .unwrap(); + assert_eq!(rewards.amount.u128(), 27); + + // delegator1 should now have 18 + 200 * 10% - 10% commission = 36 tokens + let rewards = stake + .get_rewards(&store, &block, &delegator2, &validator) + .unwrap() + .unwrap(); + assert_eq!(rewards.amount.u128(), 36); } } From 735b38cafd2fa8ea57aefd78e57570455802cc02 Mon Sep 17 00:00:00 2001 From: Christoph Otter Date: Thu, 6 Oct 2022 16:41:04 +0200 Subject: [PATCH 14/25] Fix multi-test staking rewards --- packages/multi-test/src/staking.rs | 286 +++++++++++++++++++---------- 1 file changed, 192 insertions(+), 94 deletions(-) diff --git a/packages/multi-test/src/staking.rs b/packages/multi-test/src/staking.rs index 4cc97628c..d304cf20a 100644 --- a/packages/multi-test/src/staking.rs +++ b/packages/multi-test/src/staking.rs @@ -28,37 +28,30 @@ pub struct StakingInfo { apr: Decimal, } -/// The number of (conceptual) shares of this validator the staker has. These can be fractional shares -/// Used to calculate the stake. If the validator is slashed, this might not be the same as the stake. +/// The number of stake and rewards of this validator the staker has. These can be fractional in case of slashing. #[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, JsonSchema)] -struct Shares(Decimal); +struct Shares { + stake: Decimal, + rewards: Decimal, +} impl Shares { - /// The stake of this delegator. Make sure to pass the correct validator in - pub fn stake(&self, validator: &ValidatorInfo) -> Uint128 { - self.0 / validator.total_shares * validator.stake - } - - pub fn rewards(&self, validator: &ValidatorInfo, rewards: Decimal) -> Decimal { - self.0 * rewards / validator.total_shares + /// Calculates the share of validator rewards that should be given to this staker. + pub fn share_of_rewards(&self, validator: &ValidatorInfo, rewards: Decimal) -> Decimal { + rewards * self.stake / validator.stake } } /// Holds some operational data about a validator #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] struct ValidatorInfo { - /// The stakers that have staked with this validator + /// The stakers that have staked with this validator. + /// We need to track them for updating their rewards. stakers: BTreeSet, /// The whole stake of all stakers stake: Uint128, /// The block time when this validator's rewards were last update. This is needed for rewards calculation. last_rewards_calculation: Timestamp, - /// The total number of shares this validator has issued, only used internally for calculating rewards - total_shares: Decimal, - /// The number of available rewards. This is updated in `calculate_rewards`. - /// It is needed to save the current rewards somewhere before adding / removing stake, - /// since the new stake should only apply to future interest, not past interest. - calculated_rewards: Decimal, } impl ValidatorInfo { @@ -67,19 +60,6 @@ impl ValidatorInfo { stakers: BTreeSet::new(), stake: Uint128::zero(), last_rewards_calculation: block_time, - total_shares: Decimal::zero(), - calculated_rewards: Decimal::zero(), - } - } - /// Returns the amount of shares a delegator gets for staking the given amount of tokens (bonded_denom) at this point in time. - /// This should usually be `1:1` unless the delegator was slashed. - pub fn shares_for(&self, stake: Uint128) -> Decimal { - if self.stake.is_zero() { - // first staker always gets 1:1 - Decimal::one() - } else { - Decimal::from_ratio(stake, 1u128) * self.total_shares - / Decimal::from_ratio(self.stake, 1u128) } } } @@ -101,7 +81,7 @@ pub const NAMESPACE_STAKING: &[u8] = b"staking"; #[derive(Clone, std::fmt::Debug, PartialEq, Eq, JsonSchema)] pub enum StakingSudo { /// Slashes the given percentage of the validator's stake. - /// For now, you cannot slash after the fact in tests. + /// For now, you cannot slash retrospectively in tests. Slash { validator: String, percentage: Decimal, @@ -144,6 +124,7 @@ impl StakeKeeper { pub fn init_stake( &self, + api: &dyn Api, storage: &mut dyn Storage, block: &BlockInfo, account: &Addr, @@ -152,7 +133,7 @@ impl StakeKeeper { ) -> AnyResult<()> { let mut storage = prefixed(storage, NAMESPACE_STAKING); - self.add_stake(&mut storage, block, account, validator, amount) + self.add_stake(api, &mut storage, block, account, validator, amount) } /// Add a new validator available for staking @@ -233,28 +214,18 @@ impl StakeKeeper { ) -> AnyResult { let staking_info = Self::get_staking_info(staking_storage)?; - println!( - "old delegator rewards: {} * {} / {}", - validator_info.calculated_rewards, shares.0, validator_info.total_shares - ); - // calculate missing rewards without updating the validator to reduce rounding errors - let missing_validator_rewards = Self::calculate_rewards( + let new_validator_rewards = Self::calculate_rewards( block.time, validator_info.last_rewards_calculation, staking_info.apr, validator.commission, validator_info.stake, ); - let validator_rewards = validator_info.calculated_rewards + missing_validator_rewards; // calculate the delegator's share of those - let delegator_rewards = shares.rewards(validator_info, validator_rewards); - - println!( - "new validator / delegator rewards: {} / {}", - validator_rewards, delegator_rewards - ); + let delegator_rewards = + shares.rewards + shares.share_of_rewards(validator_info, new_validator_rewards); Ok(Coin { denom: staking_info.bonded_denom, @@ -280,12 +251,6 @@ impl StakeKeeper { / Decimal::from_ratio(60u128 * 60 * 24 * 365, 1u128); let commission = reward * validator_commission; - println!( - "calculated new: 10% * {} - 10% comm. = {}", - stake, - reward - commission - ); - reward - commission } @@ -293,6 +258,8 @@ impl StakeKeeper { /// but does not save it. /// Always call this to update rewards before changing a validator stake. fn update_rewards( + api: &dyn Api, + staking_storage: &mut dyn Storage, block: &BlockInfo, staking_info: &StakingInfo, validator_info: &mut ValidatorInfo, @@ -310,12 +277,24 @@ impl StakeKeeper { validator_info.stake, ); - // update validator info, but only if there is at least 1 new token - // Less than one token would not change anything, as only full tokens are presented - // outside of the keeper. - if new_rewards >= Decimal::one() { + // update validator info and delegators + if !new_rewards.is_zero() { validator_info.last_rewards_calculation = block.time; - validator_info.calculated_rewards += new_rewards; + + let validator_addr = api.addr_validate(&validator.address)?; + // update all delegators + for staker in validator_info.stakers.iter() { + STAKES.update( + staking_storage, + (staker, &validator_addr), + |shares| -> AnyResult<_> { + let mut shares = + shares.expect("all stakers in validator_info should exist"); + shares.rewards += shares.share_of_rewards(validator_info, new_rewards); + Ok(shares) + }, + )?; + } } Ok(()) } @@ -342,11 +321,9 @@ impl StakeKeeper { ) -> AnyResult { let shares = STAKES.may_load(staking_storage, (account, validator))?; let staking_info = Self::get_staking_info(staking_storage)?; - let validator_info = VALIDATOR_INFO.may_load(staking_storage, validator)?; Ok(Coin { amount: shares - .zip(validator_info) - .map(|(s, validator_info)| s.stake(&validator_info)) + .map(|s| s.stake * Uint128::new(1)) .unwrap_or_default(), denom: staking_info.bonded_denom, }) @@ -354,6 +331,7 @@ impl StakeKeeper { fn add_stake( &self, + api: &dyn Api, staking_storage: &mut dyn Storage, block: &BlockInfo, to_address: &Addr, @@ -363,6 +341,7 @@ impl StakeKeeper { self.validate_denom(staking_storage, &amount)?; self.validate_nonzero(&amount)?; self.update_stake( + api, staking_storage, block, to_address, @@ -374,6 +353,7 @@ impl StakeKeeper { fn remove_stake( &self, + api: &dyn Api, staking_storage: &mut dyn Storage, block: &BlockInfo, from_address: &Addr, @@ -383,6 +363,7 @@ impl StakeKeeper { self.validate_denom(staking_storage, &amount)?; self.validate_nonzero(&amount)?; self.update_stake( + api, staking_storage, block, from_address, @@ -394,6 +375,7 @@ impl StakeKeeper { fn update_stake( &self, + api: &dyn Api, staking_storage: &mut dyn Storage, block: &BlockInfo, delegator: &Addr, @@ -403,42 +385,46 @@ impl StakeKeeper { ) -> AnyResult<()> { let amount = amount.into(); + if amount.is_zero() { + return Ok(()); + } + let mut validator_info = VALIDATOR_INFO .may_load(staking_storage, validator)? .unwrap_or_else(|| ValidatorInfo::new(block.time)); - let mut stake_info = STAKES - .may_load(staking_storage, (delegator, validator))? - .unwrap_or_else(|| Shares(Decimal::zero())); // update rewards for this validator - if !amount.is_zero() { - let validator_obj = VALIDATOR_MAP.load(staking_storage, validator)?; - let staking_info = Self::get_staking_info(staking_storage)?; - Self::update_rewards(block, &staking_info, &mut validator_info, &validator_obj)?; - } + let validator_obj = VALIDATOR_MAP.load(staking_storage, validator)?; + let staking_info = Self::get_staking_info(staking_storage)?; + Self::update_rewards( + api, + staking_storage, + block, + &staking_info, + &mut validator_info, + &validator_obj, + )?; - // now, we can update the stake + // now, we can update the stake of the delegator and validator + let mut shares = STAKES + .may_load(staking_storage, (delegator, validator))? + .unwrap_or_default(); + let amount_dec = Decimal::from_ratio(amount, 1u128); if sub { - let shares = validator_info.shares_for(amount); - stake_info.0 -= shares; - + shares.stake -= amount_dec; validator_info.stake = validator_info.stake.checked_sub(amount)?; - validator_info.total_shares -= shares; } else { - let new_shares = validator_info.shares_for(amount); - stake_info.0 += new_shares; - + shares.stake += amount_dec; validator_info.stake = validator_info.stake.checked_add(amount)?; - validator_info.total_shares += new_shares; } // save updated values - if stake_info.0.is_zero() { + if shares.stake.is_zero() { // no more stake, so remove STAKES.remove(staking_storage, (delegator, validator)); validator_info.stakers.remove(delegator); } else { - STAKES.save(staking_storage, (delegator, validator), &stake_info)?; + STAKES.save(staking_storage, (delegator, validator), &shares)?; validator_info.stakers.insert(delegator.clone()); } // save updated validator info @@ -469,7 +455,20 @@ impl StakeKeeper { STAKES.remove(staking_storage, (delegator, validator)); } validator_info.stakers.clear(); - validator_info.total_shares = Decimal::zero(); + } else { + // otherwise we update all stakers + for delegator in validator_info.stakers.iter() { + STAKES.update( + staking_storage, + (delegator, validator), + |stake| -> AnyResult<_> { + let mut stake = stake.expect("all stakers in validator_info should exist"); + stake.stake *= remaining_percentage; + + Ok(stake) + }, + )?; + } } VALIDATOR_INFO.save(staking_storage, validator, &validator_info)?; Ok(()) @@ -529,6 +528,7 @@ impl Module for StakeKeeper { .add_attribute("amount", format!("{}{}", amount.amount, amount.denom)) .add_attribute("new_shares", amount.amount.to_string())]; // TODO: calculate shares? self.add_stake( + api, &mut staking_storage, block, &sender, @@ -560,6 +560,7 @@ impl Module for StakeKeeper { .add_attribute("amount", format!("{}{}", amount.amount, amount.denom)) .add_attribute("completion_time", "2022-09-27T14:00:00+00:00")]; // TODO: actual date? self.remove_stake( + api, &mut staking_storage, block, &sender, @@ -592,13 +593,21 @@ impl Module for StakeKeeper { .add_attribute("amount", format!("{}{}", amount.amount, amount.denom))]; self.remove_stake( + api, &mut staking_storage, block, &sender, &src_validator, amount.clone(), )?; - self.add_stake(&mut staking_storage, block, &sender, &dst_validator, amount)?; + self.add_stake( + api, + &mut staking_storage, + block, + &sender, + &dst_validator, + amount, + )?; Ok(AppResponse { events, data: None }) } @@ -717,7 +726,6 @@ impl Module for StakeKeeper { } }; let validator_info = VALIDATOR_INFO.load(&staking_storage, &validator_addr)?; - let stakes = shares.stake(&validator_info); let reward = Self::get_rewards_internal( &staking_storage, block, @@ -730,8 +738,11 @@ impl Module for StakeKeeper { delegation: Some(FullDelegation { delegator, validator, - amount: coin(stakes.u128(), staking_info.bonded_denom), - can_redelegate: coin(0, "testcoin"), + amount: coin( + (shares.stake * Uint128::new(1)).u128(), + staking_info.bonded_denom, + ), + can_redelegate: coin(0, "testcoin"), // TODO: not implemented right now accumulated_rewards: vec![reward], }), }; @@ -784,23 +795,26 @@ impl Module for DistributionKeeper { let mut validator_info = VALIDATOR_INFO.load(&staking_storage, &validator_addr)?; let validator_obj = VALIDATOR_MAP.load(&staking_storage, &validator_addr)?; - // update the validator's rewards + // update the validator and staker rewards StakeKeeper::update_rewards( + api, + &mut staking_storage, block, &staking_info, &mut validator_info, &validator_obj, )?; - - // remove delegator's share of the rewards - let shares = STAKES.load(&staking_storage, (&sender, &validator_addr))?; - let rewards = shares.rewards(&validator_info, validator_info.calculated_rewards); - validator_info.calculated_rewards -= rewards; - let rewards = Uint128::new(1) * rewards; // convert to Uint128 - // save updated validator_info VALIDATOR_INFO.save(&mut staking_storage, &validator_addr, &validator_info)?; + // load updated rewards for delegator + let mut shares = STAKES.load(&staking_storage, (&sender, &validator_addr))?; + let rewards = Uint128::new(1) * shares.rewards; // convert to Uint128 + + // remove rewards from delegator + shares.rewards = Decimal::zero(); + STAKES.save(&mut staking_storage, (&sender, &validator_addr), &shares)?; + // directly mint rewards to delegator router.sudo( api, @@ -858,7 +872,11 @@ mod test { use super::*; - use cosmwasm_std::testing::{mock_env, MockApi, MockStorage}; + use cosmwasm_std::{ + from_slice, + testing::{mock_env, MockApi, MockStorage}, + BalanceResponse, BankQuery, + }; /// Type alias for default build `Router` to make its reference in typical scenario type BasicRouter = Router< @@ -957,6 +975,7 @@ mod test { let mut staking_storage = prefixed(&mut store, NAMESPACE_STAKING); stake .add_stake( + &api, &mut staking_storage, &block, &delegator, @@ -1063,6 +1082,7 @@ mod test { // stake 200 tokens stake .add_stake( + &api, &mut staking_storage, &block, &delegator, @@ -1118,6 +1138,7 @@ mod test { setup_test(Decimal::percent(10), Decimal::percent(10)); let stake = &router.staking; let distr = &router.distribution; + let bank = &router.bank; let delegator1 = Addr::unchecked("delegator1"); let delegator2 = Addr::unchecked("delegator2"); @@ -1126,6 +1147,7 @@ mod test { // add 100 stake to delegator1 and 200 to delegator2 stake .add_stake( + &api, &mut staking_storage, &block, &delegator1, @@ -1135,6 +1157,7 @@ mod test { .unwrap(); stake .add_stake( + &api, &mut staking_storage, &block, &delegator2, @@ -1153,7 +1176,7 @@ mod test { .unwrap(); assert_eq!(rewards.amount.u128(), 9); - // delegator1 should now have 200 * 10% - 10% commission = 18 tokens + // delegator2 should now have 200 * 10% - 10% commission = 18 tokens let rewards = stake .get_rewards(&store, &block, &delegator2, &validator) .unwrap() @@ -1164,6 +1187,7 @@ mod test { let mut staking_storage = prefixed(&mut store, NAMESPACE_STAKING); stake .add_stake( + &api, &mut staking_storage, &block, &delegator1, @@ -1182,11 +1206,85 @@ mod test { .unwrap(); assert_eq!(rewards.amount.u128(), 27); - // delegator1 should now have 18 + 200 * 10% - 10% commission = 36 tokens + // delegator2 should now have 18 + 200 * 10% - 10% commission = 36 tokens let rewards = stake .get_rewards(&store, &block, &delegator2, &validator) .unwrap() .unwrap(); assert_eq!(rewards.amount.u128(), 36); + + // delegator2 unstakes 100 (has 100 left after that) + let mut staking_storage = prefixed(&mut store, NAMESPACE_STAKING); + stake + .remove_stake( + &api, + &mut staking_storage, + &block, + &delegator2, + &validator, + coin(100, "TOKEN"), + ) + .unwrap(); + + // and delegator1 withdraws rewards + distr + .execute( + &api, + &mut store, + &router, + &block, + delegator1.clone(), + DistributionMsg::WithdrawDelegatorReward { + validator: validator.to_string(), + }, + ) + .unwrap(); + + let balance: BalanceResponse = from_slice( + &bank + .query( + &api, + &store, + &router.querier(&api, &store, &block), + &block, + BankQuery::Balance { + address: delegator1.to_string(), + denom: "TOKEN".to_string(), + }, + ) + .unwrap(), + ) + .unwrap(); + assert_eq!( + balance.amount.amount.u128(), + 27, + "withdraw should change bank balance" + ); + let rewards = stake + .get_rewards(&store, &block, &delegator1, &validator) + .unwrap() + .unwrap(); + assert_eq!( + rewards.amount.u128(), + 0, + "withdraw should reduce rewards to 0" + ); + + // wait another year + block.time = block.time.plus_seconds(60 * 60 * 24 * 365); + + // delegator1 should now have 0 + 200 * 10% - 10% commission = 18 tokens + let rewards = stake + .get_rewards(&store, &block, &delegator1, &validator) + .unwrap() + .unwrap(); + assert_eq!(rewards.amount.u128(), 18); + + // delegator2 should now have 36 + 100 * 10% - 10% commission = 45 tokens + let rewards = stake + .get_rewards(&store, &block, &delegator2, &validator) + .unwrap() + .unwrap(); + assert_eq!(rewards.amount.u128(), 45); } } From cf3c94b97670c3d4162b1e0fce19fb7210697ebb Mon Sep 17 00:00:00 2001 From: Christoph Otter Date: Thu, 6 Oct 2022 17:00:40 +0200 Subject: [PATCH 15/25] Cleanup --- packages/multi-test/src/staking.rs | 85 ++++++++++++++---------------- 1 file changed, 40 insertions(+), 45 deletions(-) diff --git a/packages/multi-test/src/staking.rs b/packages/multi-test/src/staking.rs index d304cf20a..1b468756b 100644 --- a/packages/multi-test/src/staking.rs +++ b/packages/multi-test/src/staking.rs @@ -254,17 +254,23 @@ impl StakeKeeper { reward - commission } - /// Updates the staking reward for the given validator. This mutates the validator info, - /// but does not save it. - /// Always call this to update rewards before changing a validator stake. + /// Updates the staking reward for the given validator and their stakers + /// It saves the validator info and it's stakers, so make sure not to overwrite that. + /// Always call this to update rewards before changing anything that influences future rewards. fn update_rewards( api: &dyn Api, staking_storage: &mut dyn Storage, block: &BlockInfo, - staking_info: &StakingInfo, - validator_info: &mut ValidatorInfo, - validator: &Validator, + validator: &Addr, ) -> AnyResult<()> { + let staking_info = Self::get_staking_info(staking_storage)?; + + let mut validator_info = VALIDATOR_INFO + .may_load(staking_storage, validator)? + .ok_or_else(|| anyhow!("validator not found"))?; + + let validator_obj = VALIDATOR_MAP.load(staking_storage, validator)?; + if validator_info.last_rewards_calculation >= block.time { return Ok(()); } @@ -273,7 +279,7 @@ impl StakeKeeper { block.time, validator_info.last_rewards_calculation, staking_info.apr, - validator.commission, + validator_obj.commission, validator_info.stake, ); @@ -281,7 +287,10 @@ impl StakeKeeper { if !new_rewards.is_zero() { validator_info.last_rewards_calculation = block.time; - let validator_addr = api.addr_validate(&validator.address)?; + // save updated validator + VALIDATOR_INFO.save(staking_storage, validator, &validator_info)?; + + let validator_addr = api.addr_validate(&validator_obj.address)?; // update all delegators for staker in validator_info.stakers.iter() { STAKES.update( @@ -290,7 +299,7 @@ impl StakeKeeper { |shares| -> AnyResult<_> { let mut shares = shares.expect("all stakers in validator_info should exist"); - shares.rewards += shares.share_of_rewards(validator_info, new_rewards); + shares.rewards += shares.share_of_rewards(&validator_info, new_rewards); Ok(shares) }, )?; @@ -389,23 +398,15 @@ impl StakeKeeper { return Ok(()); } - let mut validator_info = VALIDATOR_INFO - .may_load(staking_storage, validator)? - .unwrap_or_else(|| ValidatorInfo::new(block.time)); - // update rewards for this validator - let validator_obj = VALIDATOR_MAP.load(staking_storage, validator)?; - let staking_info = Self::get_staking_info(staking_storage)?; - Self::update_rewards( - api, - staking_storage, - block, - &staking_info, - &mut validator_info, - &validator_obj, - )?; + // let validator_obj = VALIDATOR_MAP.load(staking_storage, validator)?; + // let staking_info = Self::get_staking_info(staking_storage)?; + Self::update_rewards(api, staking_storage, block, validator)?; // now, we can update the stake of the delegator and validator + let mut validator_info = VALIDATOR_INFO + .may_load(staking_storage, validator)? + .unwrap_or_else(|| ValidatorInfo::new(block.time)); let mut shares = STAKES .may_load(staking_storage, (delegator, validator))? .unwrap_or_default(); @@ -435,16 +436,20 @@ impl StakeKeeper { fn slash( &self, + api: &dyn Api, staking_storage: &mut dyn Storage, + block: &BlockInfo, validator: &Addr, percentage: Decimal, ) -> AnyResult<()> { + // calculate rewards before slashing + Self::update_rewards(api, staking_storage, block, validator)?; + + // update stake of validator and stakers let mut validator_info = VALIDATOR_INFO .may_load(staking_storage, validator)? .ok_or_else(|| anyhow!("validator not found"))?; - // TODO: handle rewards? Either update them before slashing or set them to zero, depending on the slashing logic - let remaining_percentage = Decimal::one() - percentage; validator_info.stake = validator_info.stake * remaining_percentage; @@ -632,7 +637,7 @@ impl Module for StakeKeeper { let validator = api.addr_validate(&validator)?; self.validate_percentage(percentage)?; - self.slash(&mut staking_storage, &validator, percentage)?; + self.slash(api, &mut staking_storage, block, &validator, percentage)?; Ok(AppResponse::default()) } @@ -734,15 +739,16 @@ impl Module for StakeKeeper { &validator_info, )?; let staking_info = Self::get_staking_info(&staking_storage)?; + let amount = coin( + (shares.stake * Uint128::new(1)).u128(), + staking_info.bonded_denom, + ); let full_delegation_response = DelegationResponse { delegation: Some(FullDelegation { delegator, validator, - amount: coin( - (shares.stake * Uint128::new(1)).u128(), - staking_info.bonded_denom, - ), - can_redelegate: coin(0, "testcoin"), // TODO: not implemented right now + amount: amount.clone(), + can_redelegate: amount, // TODO: not implemented right now accumulated_rewards: vec![reward], }), }; @@ -791,21 +797,10 @@ impl Module for DistributionKeeper { DistributionMsg::WithdrawDelegatorReward { validator } => { let validator_addr = api.addr_validate(&validator)?; - let staking_info = STAKING_INFO.load(&staking_storage)?; - let mut validator_info = VALIDATOR_INFO.load(&staking_storage, &validator_addr)?; - let validator_obj = VALIDATOR_MAP.load(&staking_storage, &validator_addr)?; - // update the validator and staker rewards - StakeKeeper::update_rewards( - api, - &mut staking_storage, - block, - &staking_info, - &mut validator_info, - &validator_obj, - )?; - // save updated validator_info - VALIDATOR_INFO.save(&mut staking_storage, &validator_addr, &validator_info)?; + StakeKeeper::update_rewards(api, &mut staking_storage, block, &validator_addr)?; + + let staking_info = STAKING_INFO.load(&staking_storage)?; // load updated rewards for delegator let mut shares = STAKES.load(&staking_storage, (&sender, &validator_addr))?; From de1ae1f0727c18c62126e295f39873b6ca10af69 Mon Sep 17 00:00:00 2001 From: Christoph Otter Date: Fri, 7 Oct 2022 14:26:52 +0200 Subject: [PATCH 16/25] Add more tests and small fixes --- packages/multi-test/src/staking.rs | 633 +++++++++++++++++++++++++---- 1 file changed, 545 insertions(+), 88 deletions(-) diff --git a/packages/multi-test/src/staking.rs b/packages/multi-test/src/staking.rs index 1b468756b..a16413851 100644 --- a/packages/multi-test/src/staking.rs +++ b/packages/multi-test/src/staking.rs @@ -18,7 +18,7 @@ use crate::executor::AppResponse; use crate::{BankSudo, Module}; // Contains some general staking parameters -#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, JsonSchema)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct StakingInfo { /// The denominator of the staking token bonded_denom: String, @@ -28,6 +28,16 @@ pub struct StakingInfo { apr: Decimal, } +impl Default for StakingInfo { + fn default() -> Self { + StakingInfo { + bonded_denom: "TOKEN".to_string(), + unbonding_time: 60, + apr: Decimal::percent(10), + } + } +} + /// The number of stake and rewards of this validator the staker has. These can be fractional in case of slashing. #[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, JsonSchema)] struct Shares { @@ -163,13 +173,7 @@ impl StakeKeeper { } fn get_staking_info(staking_storage: &dyn Storage) -> AnyResult { - Ok(STAKING_INFO - .may_load(staking_storage)? - .unwrap_or_else(|| StakingInfo { - bonded_denom: "TOKEN".to_string(), - unbonding_time: 60, - apr: Decimal::percent(10), - })) + Ok(STAKING_INFO.may_load(staking_storage)?.unwrap_or_default()) } /// Returns the rewards of the given delegator at the given validator @@ -327,15 +331,16 @@ impl StakeKeeper { staking_storage: &dyn Storage, account: &Addr, validator: &Addr, - ) -> AnyResult { + ) -> AnyResult> { let shares = STAKES.may_load(staking_storage, (account, validator))?; let staking_info = Self::get_staking_info(staking_storage)?; - Ok(Coin { - amount: shares - .map(|s| s.stake * Uint128::new(1)) - .unwrap_or_default(), - denom: staking_info.bonded_denom, - }) + + Ok(shares.map(|shares| { + Coin { + denom: staking_info.bonded_denom, + amount: Uint128::new(1) * shares.stake, // multiplying by 1 to convert Decimal to Uint128 + } + })) } fn add_stake( @@ -399,8 +404,6 @@ impl StakeKeeper { } // update rewards for this validator - // let validator_obj = VALIDATOR_MAP.load(staking_storage, validator)?; - // let staking_info = Self::get_staking_info(staking_storage)?; Self::update_rewards(api, staking_storage, block, validator)?; // now, we can update the stake of the delegator and validator @@ -412,6 +415,9 @@ impl StakeKeeper { .unwrap_or_default(); let amount_dec = Decimal::from_ratio(amount, 1u128); if sub { + if amount_dec > shares.stake { + bail!("insufficient stake"); + } shares.stake -= amount_dec; validator_info.stake = validator_info.stake.checked_sub(amount)?; } else { @@ -582,6 +588,7 @@ impl Module for StakeKeeper { block.time.plus_seconds(staking_info.unbonding_time), amount.amount.u128(), )); + UNBONDING_QUEUE.save(&mut staking_storage, &queue)?; Ok(AppResponse { events, data: None }) } StakingMsg::Redelegate { @@ -694,19 +701,21 @@ impl Module for StakeKeeper { let res: AnyResult> = validators .into_iter() - .map(|validator| { + .filter_map(|validator| { let delegator = delegator.clone(); - let amount = self.get_stake( - &staking_storage, - &delegator, - &Addr::unchecked(&validator.address), - )?; - - Ok(Delegation { + let amount = self + .get_stake( + &staking_storage, + &delegator, + &Addr::unchecked(&validator.address), + ) + .transpose()?; + + Some(amount.map(|amount| Delegation { delegator, validator: validator.address, amount, - }) + })) }) .collect(); @@ -722,7 +731,7 @@ impl Module for StakeKeeper { None => bail!("non-existent validator {}", validator), }; let delegator = api.addr_validate(&delegator)?; - // calculate rewards using fixed ratio + let shares = match STAKES.load(&staking_storage, (&delegator, &validator_addr)) { Ok(stakes) => stakes, Err(_) => { @@ -749,7 +758,11 @@ impl Module for StakeKeeper { validator, amount: amount.clone(), can_redelegate: amount, // TODO: not implemented right now - accumulated_rewards: vec![reward], + accumulated_rewards: if reward.amount.is_zero() { + vec![] + } else { + vec![reward] + }, }), }; @@ -774,6 +787,30 @@ impl DistributionKeeper { pub fn new() -> Self { DistributionKeeper {} } + + /// Removes all rewards from the given (delegator, validator) pair and returns the amount + pub fn remove_rewards( + &self, + api: &dyn Api, + storage: &mut dyn Storage, + block: &BlockInfo, + delegator: &Addr, + validator: &Addr, + ) -> AnyResult { + let mut staking_storage = prefixed(storage, NAMESPACE_STAKING); + // update the validator and staker rewards + StakeKeeper::update_rewards(api, &mut staking_storage, block, validator)?; + + // load updated rewards for delegator + let mut shares = STAKES.load(&staking_storage, (delegator, validator))?; + let rewards = Uint128::new(1) * shares.rewards; // convert to Uint128 + + // remove rewards from delegator + shares.rewards = Decimal::zero(); + STAKES.save(&mut staking_storage, (delegator, validator), &shares)?; + + Ok(rewards) + } } impl Distribution for DistributionKeeper {} @@ -792,24 +829,14 @@ impl Module for DistributionKeeper { sender: Addr, msg: DistributionMsg, ) -> AnyResult { - let mut staking_storage = prefixed(storage, NAMESPACE_STAKING); match msg { DistributionMsg::WithdrawDelegatorReward { validator } => { let validator_addr = api.addr_validate(&validator)?; - // update the validator and staker rewards - StakeKeeper::update_rewards(api, &mut staking_storage, block, &validator_addr)?; - - let staking_info = STAKING_INFO.load(&staking_storage)?; - - // load updated rewards for delegator - let mut shares = STAKES.load(&staking_storage, (&sender, &validator_addr))?; - let rewards = Uint128::new(1) * shares.rewards; // convert to Uint128 - - // remove rewards from delegator - shares.rewards = Decimal::zero(); - STAKES.save(&mut staking_storage, (&sender, &validator_addr), &shares)?; + let rewards = self.remove_rewards(api, storage, block, &sender, &validator_addr)?; + let staking_storage = prefixed_read(storage, NAMESPACE_STAKING); + let staking_info = StakeKeeper::get_staking_info(&staking_storage)?; // directly mint rewards to delegator router.sudo( api, @@ -892,6 +919,44 @@ mod test { } } + fn setup_test_env( + apr: Decimal, + validator_commission: Decimal, + ) -> (MockApi, MockStorage, BasicRouter, BlockInfo, Addr) { + let api = MockApi::default(); + let router = mock_router(); + let mut store = MockStorage::new(); + let block = mock_env().block; + + let validator = api.addr_validate("testvaloper1").unwrap(); + + router + .staking + .setup( + &mut store, + StakingInfo { + bonded_denom: "TOKEN".to_string(), + unbonding_time: 60, + apr, + }, + ) + .unwrap(); + + // add validator + let valoper1 = Validator { + address: "testvaloper1".to_string(), + commission: validator_commission, + max_commission: Decimal::percent(100), + max_change_rate: Decimal::percent(1), + }; + router + .staking + .add_validator(&api, &mut store, &block, valoper1) + .unwrap(); + + (api, store, router, block, validator) + } + #[test] fn add_get_validators() { let api = MockApi::default(); @@ -998,7 +1063,11 @@ mod test { let stake_left = stake .get_stake(&staking_storage, &delegator, &validator) .unwrap(); - assert_eq!(stake_left.amount.u128(), 50, "should have slashed 50%"); + assert_eq!( + stake_left.unwrap().amount.u128(), + 50, + "should have slashed 50%" + ); // slash all stake @@ -1019,56 +1088,13 @@ mod test { let stake_left = stake .get_stake(&staking_storage, &delegator, &validator) .unwrap(); - assert_eq!( - stake_left.amount.u128(), - 0, - "should have slashed whole stake" - ); - } - - fn setup_test( - apr: Decimal, - validator_commission: Decimal, - ) -> (MockApi, MockStorage, BasicRouter, BlockInfo, Addr) { - let api = MockApi::default(); - let router = mock_router(); - let mut store = MockStorage::new(); - let block = mock_env().block; - - let validator = api.addr_validate("testvaloper1").unwrap(); - - // setup 10% APR - router - .staking - .setup( - &mut store, - StakingInfo { - bonded_denom: "TOKEN".to_string(), - unbonding_time: 60, - apr, - }, - ) - .unwrap(); - - // add validator - let valoper1 = Validator { - address: "testvaloper1".to_string(), - commission: validator_commission, - max_commission: Decimal::percent(100), - max_change_rate: Decimal::percent(1), - }; - router - .staking - .add_validator(&api, &mut store, &block, valoper1) - .unwrap(); - - (api, store, router, block, validator) + assert_eq!(stake_left, None, "should have slashed whole stake"); } #[test] fn rewards_work_for_single_delegator() { let (api, mut store, router, mut block, validator) = - setup_test(Decimal::percent(10), Decimal::percent(10)); + setup_test_env(Decimal::percent(10), Decimal::percent(10)); let stake = &router.staking; let distr = &router.distribution; let delegator = Addr::unchecked("delegator"); @@ -1130,7 +1156,7 @@ mod test { #[test] fn rewards_work_for_multiple_delegators() { let (api, mut store, router, mut block, validator) = - setup_test(Decimal::percent(10), Decimal::percent(10)); + setup_test_env(Decimal::percent(10), Decimal::percent(10)); let stake = &router.staking; let distr = &router.distribution; let bank = &router.bank; @@ -1282,4 +1308,435 @@ mod test { .unwrap(); assert_eq!(rewards.amount.u128(), 45); } + + mod msg { + use cosmwasm_std::{from_slice, Addr, BondedDenomResponse, Decimal, StakingQuery}; + use serde::de::DeserializeOwned; + + use super::*; + + // shortens tests a bit + struct TestEnv { + api: MockApi, + store: MockStorage, + router: BasicRouter, + block: BlockInfo, + } + + impl TestEnv { + fn wrap(tuple: (MockApi, MockStorage, BasicRouter, BlockInfo, Addr)) -> (Self, Addr) { + ( + Self { + api: tuple.0, + store: tuple.1, + router: tuple.2, + block: tuple.3, + }, + tuple.4, + ) + } + } + + fn execute_stake( + env: &mut TestEnv, + sender: Addr, + msg: StakingMsg, + ) -> AnyResult { + env.router.staking.execute( + &env.api, + &mut env.store, + &env.router, + &env.block, + sender, + msg, + ) + } + + fn query_stake(env: &TestEnv, msg: StakingQuery) -> AnyResult { + Ok(from_slice(&env.router.staking.query( + &env.api, + &env.store, + &env.router.querier(&env.api, &env.store, &env.block), + &env.block, + msg, + )?)?) + } + + fn execute_distr( + env: &mut TestEnv, + sender: Addr, + msg: DistributionMsg, + ) -> AnyResult { + env.router.distribution.execute( + &env.api, + &mut env.store, + &env.router, + &env.block, + sender, + msg, + ) + } + + fn query_bank(env: &TestEnv, msg: BankQuery) -> AnyResult { + Ok(from_slice(&env.router.bank.query( + &env.api, + &env.store, + &env.router.querier(&env.api, &env.store, &env.block), + &env.block, + msg, + )?)?) + } + + fn assert_balances(env: &TestEnv, balances: impl IntoIterator) { + for (addr, amount) in balances { + let balance: BalanceResponse = query_bank( + env, + BankQuery::Balance { + address: addr.to_string(), + denom: "TOKEN".to_string(), + }, + ) + .unwrap(); + assert_eq!(balance.amount.amount.u128(), amount); + } + } + + #[test] + fn execute() { + // test all execute msgs + let (mut test_env, validator1) = + TestEnv::wrap(setup_test_env(Decimal::percent(10), Decimal::percent(10))); + + let delegator1 = Addr::unchecked("delegator1"); + + // fund delegator1 account + test_env + .router + .bank + .init_balance(&mut test_env.store, &delegator1, vec![coin(1000, "TOKEN")]) + .unwrap(); + + // add second validator + let validator2 = Addr::unchecked("validator2"); + test_env + .router + .staking + .add_validator( + &test_env.api, + &mut test_env.store, + &test_env.block, + Validator { + address: validator2.to_string(), + commission: Decimal::zero(), + max_commission: Decimal::percent(20), + max_change_rate: Decimal::percent(1), + }, + ) + .unwrap(); + + // delegate 100 tokens to validator1 + execute_stake( + &mut test_env, + delegator1.clone(), + StakingMsg::Delegate { + validator: validator1.to_string(), + amount: coin(100, "TOKEN"), + }, + ) + .unwrap(); + + // should now have 100 tokens less + assert_balances(&test_env, vec![(delegator1.clone(), 900)]); + + // wait a year + test_env.block.time = test_env.block.time.plus_seconds(60 * 60 * 24 * 365); + + // withdraw rewards + execute_distr( + &mut test_env, + delegator1.clone(), + DistributionMsg::WithdrawDelegatorReward { + validator: validator1.to_string(), + }, + ) + .unwrap(); + + // redelegate to validator2 + execute_stake( + &mut test_env, + delegator1.clone(), + StakingMsg::Redelegate { + src_validator: validator1.to_string(), + dst_validator: validator2.to_string(), + amount: coin(100, "TOKEN"), + }, + ) + .unwrap(); + + // should have same amount as before + assert_balances( + &test_env, + vec![(delegator1.clone(), 900 + 100 / 10 * 9 / 10)], + ); + + let delegations: AllDelegationsResponse = query_stake( + &test_env, + StakingQuery::AllDelegations { + delegator: delegator1.to_string(), + }, + ) + .unwrap(); + assert_eq!( + delegations.delegations, + [Delegation { + delegator: delegator1.clone(), + validator: validator2.to_string(), + amount: coin(100, "TOKEN"), + }] + ); + + // undelegate all tokens + execute_stake( + &mut test_env, + delegator1.clone(), + StakingMsg::Undelegate { + validator: validator2.to_string(), + amount: coin(100, "TOKEN"), + }, + ) + .unwrap(); + + // wait for unbonding period (60 seconds in default config) + test_env.block.time = test_env.block.time.plus_seconds(60); + + // need to manually cause queue to get processed + test_env + .router + .staking + .sudo( + &test_env.api, + &mut test_env.store, + &test_env.router, + &test_env.block, + StakingSudo::ProcessQueue {}, + ) + .unwrap(); + + // check bank balance + assert_balances( + &test_env, + vec![(delegator1.clone(), 1000 + 100 / 10 * 9 / 10)], + ); + } + + #[test] + fn cannot_steal() { + let (mut test_env, validator1) = + TestEnv::wrap(setup_test_env(Decimal::percent(10), Decimal::percent(10))); + + let delegator1 = Addr::unchecked("delegator1"); + + // fund delegator1 account + test_env + .router + .bank + .init_balance(&mut test_env.store, &delegator1, vec![coin(100, "TOKEN")]) + .unwrap(); + + // delegate 100 tokens to validator1 + execute_stake( + &mut test_env, + delegator1.clone(), + StakingMsg::Delegate { + validator: validator1.to_string(), + amount: coin(100, "TOKEN"), + }, + ) + .unwrap(); + + // undelegate more tokens than we have + let e = execute_stake( + &mut test_env, + delegator1.clone(), + StakingMsg::Undelegate { + validator: validator1.to_string(), + amount: coin(200, "TOKEN"), + }, + ) + .unwrap_err(); + + assert_eq!(e.to_string(), "insufficient stake"); + + // add second validator + let validator2 = Addr::unchecked("validator2"); + test_env + .router + .staking + .add_validator( + &test_env.api, + &mut test_env.store, + &test_env.block, + Validator { + address: validator2.to_string(), + commission: Decimal::zero(), + max_commission: Decimal::percent(20), + max_change_rate: Decimal::percent(1), + }, + ) + .unwrap(); + + // redelegate more tokens than we have + let e = execute_stake( + &mut test_env, + delegator1.clone(), + StakingMsg::Redelegate { + src_validator: validator1.to_string(), + dst_validator: validator2.to_string(), + amount: coin(200, "TOKEN"), + }, + ) + .unwrap_err(); + assert_eq!(e.to_string(), "insufficient stake"); + } + + #[test] + fn query_staking() { + // run all staking queries + let (mut test_env, validator1) = + TestEnv::wrap(setup_test_env(Decimal::percent(10), Decimal::percent(10))); + let delegator1 = Addr::unchecked("delegator1"); + let delegator2 = Addr::unchecked("delegator2"); + + // init balances + test_env + .router + .bank + .init_balance(&mut test_env.store, &delegator1, vec![coin(260, "TOKEN")]) + .unwrap(); + test_env + .router + .bank + .init_balance(&mut test_env.store, &delegator2, vec![coin(150, "TOKEN")]) + .unwrap(); + + // add another validator + let validator2 = test_env.api.addr_validate("testvaloper2").unwrap(); + let valoper2 = Validator { + address: "testvaloper2".to_string(), + commission: Decimal::percent(0), + max_commission: Decimal::percent(1), + max_change_rate: Decimal::percent(1), + }; + test_env + .router + .staking + .add_validator( + &test_env.api, + &mut test_env.store, + &test_env.block, + valoper2.clone(), + ) + .unwrap(); + + // query validators + let valoper1: ValidatorResponse = query_stake( + &test_env, + StakingQuery::Validator { + address: validator1.to_string(), + }, + ) + .unwrap(); + let validators: AllValidatorsResponse = + query_stake(&test_env, StakingQuery::AllValidators {}).unwrap(); + assert_eq!( + validators.validators, + [valoper1.validator.unwrap(), valoper2] + ); + // query non-existent validator + let response = query_stake::( + &test_env, + StakingQuery::Validator { + address: "notvaloper".to_string(), + }, + ) + .unwrap(); + assert_eq!(response.validator, None); + + // query bonded denom + let response: BondedDenomResponse = + query_stake(&test_env, StakingQuery::BondedDenom {}).unwrap(); + assert_eq!(response.denom, "TOKEN"); + + // delegate some tokens with delegator1 and delegator2 + execute_stake( + &mut test_env, + delegator1.clone(), + StakingMsg::Delegate { + validator: validator1.to_string(), + amount: coin(100, "TOKEN"), + }, + ) + .unwrap(); + execute_stake( + &mut test_env, + delegator1.clone(), + StakingMsg::Delegate { + validator: validator2.to_string(), + amount: coin(160, "TOKEN"), + }, + ) + .unwrap(); + execute_stake( + &mut test_env, + delegator2.clone(), + StakingMsg::Delegate { + validator: validator1.to_string(), + amount: coin(150, "TOKEN"), + }, + ) + .unwrap(); + + // query all delegations + let response1: AllDelegationsResponse = query_stake( + &test_env, + StakingQuery::AllDelegations { + delegator: delegator1.to_string(), + }, + ) + .unwrap(); + assert_eq!( + response1.delegations, + vec![ + Delegation { + delegator: delegator1.clone(), + validator: validator1.to_string(), + amount: coin(100, "TOKEN"), + }, + Delegation { + delegator: delegator1.clone(), + validator: validator2.to_string(), + amount: coin(160, "TOKEN"), + }, + ] + ); + let response2: DelegationResponse = query_stake( + &test_env, + StakingQuery::Delegation { + delegator: delegator2.to_string(), + validator: validator1.to_string(), + }, + ) + .unwrap(); + assert_eq!( + response2.delegation.unwrap(), + FullDelegation { + delegator: delegator2.clone(), + validator: validator1.to_string(), + amount: coin(150, "TOKEN"), + accumulated_rewards: vec![], + can_redelegate: coin(150, "TOKEN"), + }, + ); + } + } } From b4882f28ae0aa2b14a5dbfc7e06ef0f8067f70ba Mon Sep 17 00:00:00 2001 From: Christoph Otter Date: Fri, 7 Oct 2022 14:29:27 +0200 Subject: [PATCH 17/25] Add stake and distribution keeper to BasicApp --- packages/multi-test/src/app.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/multi-test/src/app.rs b/packages/multi-test/src/app.rs index 18e498418..9087cebf1 100644 --- a/packages/multi-test/src/app.rs +++ b/packages/multi-test/src/app.rs @@ -33,6 +33,8 @@ pub type BasicApp = App< MockStorage, FailingModule, WasmKeeper, + StakeKeeper, + DistributionKeeper, >; /// Router is a persisted state. You can query this. From 31aaa5e893c310496e0a1edef657bbd54f23380a Mon Sep 17 00:00:00 2001 From: Christoph Otter Date: Fri, 7 Oct 2022 14:53:07 +0200 Subject: [PATCH 18/25] Use Deque in staking test module --- packages/multi-test/src/staking.rs | 49 ++++++++++++++---------------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/packages/multi-test/src/staking.rs b/packages/multi-test/src/staking.rs index a16413851..d9b8e1026 100644 --- a/packages/multi-test/src/staking.rs +++ b/packages/multi-test/src/staking.rs @@ -1,4 +1,4 @@ -use std::collections::{BTreeSet, VecDeque}; +use std::collections::BTreeSet; use anyhow::{anyhow, bail, Result as AnyResult}; use schemars::JsonSchema; @@ -10,7 +10,7 @@ use cosmwasm_std::{ StakingQuery, Storage, Timestamp, Uint128, Validator, ValidatorResponse, }; use cosmwasm_storage::{prefixed, prefixed_read}; -use cw_storage_plus::{Item, Map}; +use cw_storage_plus::{Deque, Item, Map}; use serde::{Deserialize, Serialize}; use crate::app::CosmosRouter; @@ -78,12 +78,11 @@ const STAKING_INFO: Item = Item::new("staking_info"); const STAKES: Map<(&Addr, &Addr), Shares> = Map::new("stakes"); const VALIDATOR_MAP: Map<&Addr, Validator> = Map::new("validator_map"); /// Additional vec of validators, in case the `iterator` feature is disabled -const VALIDATORS: Item> = Item::new("validators"); +const VALIDATORS: Deque = Deque::new("validators"); /// Contains additional info for each validator const VALIDATOR_INFO: Map<&Addr, ValidatorInfo> = Map::new("validator_info"); /// The queue of unbonding operations. This is needed because unbonding has a waiting time. See [`StakeKeeper`] -/// TODO: replace with `Deque` -const UNBONDING_QUEUE: Item> = Item::new("unbonding_queue"); +const UNBONDING_QUEUE: Deque<(Addr, Timestamp, u128)> = Deque::new("unbonding_queue"); pub const NAMESPACE_STAKING: &[u8] = b"staking"; @@ -165,9 +164,7 @@ impl StakeKeeper { } VALIDATOR_MAP.save(&mut storage, &val_addr, &validator)?; - let mut vec = VALIDATORS.may_load(&storage)?.unwrap_or_default(); - vec.push(validator); - VALIDATORS.save(&mut storage, &vec)?; + VALIDATORS.push_back(&mut storage, &validator)?; VALIDATOR_INFO.save(&mut storage, &val_addr, &ValidatorInfo::new(block.time))?; Ok(()) } @@ -323,7 +320,8 @@ impl StakeKeeper { /// Returns all available validators fn get_validators(&self, staking_storage: &dyn Storage) -> AnyResult> { - Ok(VALIDATORS.may_load(staking_storage)?.unwrap_or_default()) + let res: Result<_, _> = VALIDATORS.iter(staking_storage)?.collect(); + Ok(res?) } fn get_stake( @@ -580,15 +578,14 @@ impl Module for StakeKeeper { )?; // add tokens to unbonding queue let staking_info = Self::get_staking_info(&staking_storage)?; - let mut queue = UNBONDING_QUEUE - .may_load(&staking_storage)? - .unwrap_or_default(); - queue.push_back(( - sender.clone(), - block.time.plus_seconds(staking_info.unbonding_time), - amount.amount.u128(), - )); - UNBONDING_QUEUE.save(&mut staking_storage, &queue)?; + UNBONDING_QUEUE.push_back( + &mut staking_storage, + &( + sender.clone(), + block.time.plus_seconds(staking_info.unbonding_time), + amount.amount.u128(), + ), + )?; Ok(AppResponse { events, data: None }) } StakingMsg::Redelegate { @@ -635,12 +632,12 @@ impl Module for StakeKeeper { block: &BlockInfo, msg: StakingSudo, ) -> AnyResult { - let mut staking_storage = prefixed(storage, NAMESPACE_STAKING); match msg { StakingSudo::Slash { validator, percentage, } => { + let mut staking_storage = prefixed(storage, NAMESPACE_STAKING); let validator = api.addr_validate(&validator)?; self.validate_percentage(percentage)?; @@ -649,18 +646,16 @@ impl Module for StakeKeeper { Ok(AppResponse::default()) } StakingSudo::ProcessQueue {} => { - let mut queue = UNBONDING_QUEUE - .may_load(&staking_storage)? - .unwrap_or_default(); - loop { - match queue.front() { + let mut staking_storage = prefixed(storage, NAMESPACE_STAKING); + let front = UNBONDING_QUEUE.front(&staking_storage)?; + match front { // assuming the queue is sorted by payout_at - Some((_, payout_at, _)) if payout_at <= &block.time => { + Some((_, payout_at, _)) if payout_at <= block.time => { // remove from queue - let (delegator, _, amount) = queue.pop_front().unwrap(); + let (delegator, _, amount) = + UNBONDING_QUEUE.pop_front(&mut staking_storage)?.unwrap(); - let staking_storage = prefixed_read(storage, NAMESPACE_STAKING); let staking_info = Self::get_staking_info(&staking_storage)?; router.execute( api, From 17d469c39a6556a177790640dda891d6d49922e4 Mon Sep 17 00:00:00 2001 From: Christoph Otter Date: Fri, 7 Oct 2022 15:30:37 +0200 Subject: [PATCH 19/25] Improve coverage --- packages/multi-test/src/staking.rs | 83 ++++++++++++++++++++++++------ 1 file changed, 66 insertions(+), 17 deletions(-) diff --git a/packages/multi-test/src/staking.rs b/packages/multi-test/src/staking.rs index d9b8e1026..270cd421b 100644 --- a/packages/multi-test/src/staking.rs +++ b/packages/multi-test/src/staking.rs @@ -131,20 +131,6 @@ impl StakeKeeper { Ok(()) } - pub fn init_stake( - &self, - api: &dyn Api, - storage: &mut dyn Storage, - block: &BlockInfo, - account: &Addr, - validator: &Addr, - amount: Coin, - ) -> AnyResult<()> { - let mut storage = prefixed(storage, NAMESPACE_STAKING); - - self.add_stake(api, &mut storage, block, account, validator, amount) - } - /// Add a new validator available for staking pub fn add_validator( &self, @@ -185,7 +171,7 @@ impl StakeKeeper { let validator_obj = match self.get_validator(&staking_storage, validator)? { Some(validator) => validator, - None => bail!("non-existent validator {}", validator), + None => bail!("validator {} not found", validator), }; // calculate rewards using fixed ratio let shares = match STAKES.load(&staking_storage, (delegator, validator)) { @@ -268,7 +254,7 @@ impl StakeKeeper { let mut validator_info = VALIDATOR_INFO .may_load(staking_storage, validator)? - .ok_or_else(|| anyhow!("validator not found"))?; + .ok_or_else(|| anyhow!("validator {} not found", validator))?; let validator_obj = VALIDATOR_MAP.load(staking_storage, validator)?; @@ -452,7 +438,7 @@ impl StakeKeeper { // update stake of validator and stakers let mut validator_info = VALIDATOR_INFO .may_load(staking_storage, validator)? - .ok_or_else(|| anyhow!("validator not found"))?; + .ok_or_else(|| anyhow!("validator {} not found", validator))?; let remaining_percentage = Decimal::one() - percentage; validator_info.stake = validator_info.stake * remaining_percentage; @@ -1594,6 +1580,69 @@ mod test { assert_eq!(e.to_string(), "insufficient stake"); } + #[test] + fn denom_validation() { + let (mut test_env, validator) = + TestEnv::wrap(setup_test_env(Decimal::percent(10), Decimal::percent(10))); + + let delegator1 = Addr::unchecked("delegator1"); + + // fund delegator1 account + test_env + .router + .bank + .init_balance(&mut test_env.store, &delegator1, vec![coin(100, "FAKE")]) + .unwrap(); + + // try to delegate 100 to validator1 + let e = execute_stake( + &mut test_env, + delegator1.clone(), + StakingMsg::Delegate { + validator: validator.to_string(), + amount: coin(100, "FAKE"), + }, + ) + .unwrap_err(); + + assert_eq!( + e.to_string(), + "cannot delegate coins of denominator FAKE, only of TOKEN", + ); + } + + #[test] + fn cannot_slash_nonexistent() { + let (mut test_env, _) = + TestEnv::wrap(setup_test_env(Decimal::percent(10), Decimal::percent(10))); + + let delegator1 = Addr::unchecked("delegator1"); + + // fund delegator1 account + test_env + .router + .bank + .init_balance(&mut test_env.store, &delegator1, vec![coin(100, "FAKE")]) + .unwrap(); + + // try to delegate 100 to validator1 + let e = test_env + .router + .staking + .sudo( + &test_env.api, + &mut test_env.store, + &test_env.router, + &test_env.block, + StakingSudo::Slash { + validator: "nonexistingvaloper".to_string(), + percentage: Decimal::percent(50), + }, + ) + .unwrap_err(); + assert_eq!(e.to_string(), "validator nonexistingvaloper not found"); + } + #[test] fn query_staking() { // run all staking queries From 41b6432cfa46ad9636d21f46007608bd23ee2100 Mon Sep 17 00:00:00 2001 From: Christoph Otter Date: Fri, 7 Oct 2022 16:16:14 +0200 Subject: [PATCH 20/25] Cleanup --- packages/multi-test/src/staking.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/multi-test/src/staking.rs b/packages/multi-test/src/staking.rs index 270cd421b..7ad5fe6ad 100644 --- a/packages/multi-test/src/staking.rs +++ b/packages/multi-test/src/staking.rs @@ -383,10 +383,6 @@ impl StakeKeeper { ) -> AnyResult<()> { let amount = amount.into(); - if amount.is_zero() { - return Ok(()); - } - // update rewards for this validator Self::update_rewards(api, staking_storage, block, validator)?; @@ -942,7 +938,7 @@ mod test { fn add_get_validators() { let api = MockApi::default(); let mut store = MockStorage::new(); - let stake = StakeKeeper::new(); + let stake = StakeKeeper::default(); let block = mock_env().block; // add validator From 31a872e8bdeb6d4e6528b1d545c02dee1d8e0ed8 Mon Sep 17 00:00:00 2001 From: Christoph Otter Date: Mon, 10 Oct 2022 10:27:50 +0200 Subject: [PATCH 21/25] Export StakingInfo in multi-test --- packages/multi-test/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/multi-test/src/lib.rs b/packages/multi-test/src/lib.rs index 572c72c87..7d9ae4ff9 100644 --- a/packages/multi-test/src/lib.rs +++ b/packages/multi-test/src/lib.rs @@ -28,5 +28,5 @@ pub use crate::bank::{Bank, BankKeeper, BankSudo}; pub use crate::contracts::{Contract, ContractWrapper}; pub use crate::executor::{AppResponse, Executor}; pub use crate::module::{FailingModule, Module}; -pub use crate::staking::{DistributionKeeper, StakeKeeper, Staking, StakingSudo}; +pub use crate::staking::{DistributionKeeper, StakeKeeper, Staking, StakingInfo, StakingSudo}; pub use crate::wasm::{Wasm, WasmKeeper, WasmSudo}; From 6e7380945a3eb8561f00b819790ea46ec6513981 Mon Sep 17 00:00:00 2001 From: Christoph Otter Date: Mon, 10 Oct 2022 10:42:58 +0200 Subject: [PATCH 22/25] Make StakingInfo fields public --- packages/multi-test/src/staking.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/multi-test/src/staking.rs b/packages/multi-test/src/staking.rs index 7ad5fe6ad..cd8b8432d 100644 --- a/packages/multi-test/src/staking.rs +++ b/packages/multi-test/src/staking.rs @@ -21,11 +21,11 @@ use crate::{BankSudo, Module}; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct StakingInfo { /// The denominator of the staking token - bonded_denom: String, + pub bonded_denom: String, /// Time between unbonding and receiving tokens in seconds - unbonding_time: u64, + pub unbonding_time: u64, /// Interest rate per year (60 * 60 * 24 * 365 seconds) - apr: Decimal, + pub apr: Decimal, } impl Default for StakingInfo { From fc956b056daad110bab87ef60c9e363191a21657 Mon Sep 17 00:00:00 2001 From: Christoph Otter Date: Mon, 10 Oct 2022 13:59:29 +0200 Subject: [PATCH 23/25] Allow delegating / undelegating zero coins --- packages/multi-test/src/staking.rs | 86 +++++++++++++++++++----------- 1 file changed, 56 insertions(+), 30 deletions(-) diff --git a/packages/multi-test/src/staking.rs b/packages/multi-test/src/staking.rs index cd8b8432d..08a8291bc 100644 --- a/packages/multi-test/src/staking.rs +++ b/packages/multi-test/src/staking.rs @@ -337,7 +337,6 @@ impl StakeKeeper { amount: Coin, ) -> AnyResult<()> { self.validate_denom(staking_storage, &amount)?; - self.validate_nonzero(&amount)?; self.update_stake( api, staking_storage, @@ -359,7 +358,6 @@ impl StakeKeeper { amount: Coin, ) -> AnyResult<()> { self.validate_denom(staking_storage, &amount)?; - self.validate_nonzero(&amount)?; self.update_stake( api, staking_storage, @@ -465,11 +463,6 @@ impl StakeKeeper { Ok(()) } - fn validate_nonzero(&self, amount: &Coin) -> AnyResult<()> { - ensure!(!amount.amount.is_zero(), anyhow!("cannot delegate 0 coins")); - Ok(()) - } - // Asserts that the given coin has the proper denominator fn validate_denom(&self, staking_storage: &dyn Storage, amount: &Coin) -> AnyResult<()> { let staking_info = Self::get_staking_info(staking_storage)?; @@ -527,23 +520,24 @@ impl Module for StakeKeeper { amount.clone(), )?; // move money from sender account to this module (note we can controller sender here) - router.execute( - api, - storage, - block, - sender, - BankMsg::Send { - to_address: self.module_addr.to_string(), - amount: vec![amount], - } - .into(), - )?; + if !amount.amount.is_zero() { + router.execute( + api, + storage, + block, + sender, + BankMsg::Send { + to_address: self.module_addr.to_string(), + amount: vec![amount], + } + .into(), + )?; + } Ok(AppResponse { events, data: None }) } StakingMsg::Undelegate { validator, amount } => { let validator = api.addr_validate(&validator)?; self.validate_denom(&staking_storage, &amount)?; - self.validate_nonzero(&amount)?; // see https://github.com/cosmos/cosmos-sdk/blob/v0.46.1/x/staking/keeper/msg_server.go#L378-L383 let events = vec![Event::new("unbond") @@ -639,17 +633,19 @@ impl Module for StakeKeeper { UNBONDING_QUEUE.pop_front(&mut staking_storage)?.unwrap(); let staking_info = Self::get_staking_info(&staking_storage)?; - router.execute( - api, - storage, - block, - self.module_addr.clone(), - BankMsg::Send { - to_address: delegator.into_string(), - amount: vec![coin(amount, &staking_info.bonded_denom)], - } - .into(), - )?; + if amount > 0 { + router.execute( + api, + storage, + block, + self.module_addr.clone(), + BankMsg::Send { + to_address: delegator.into_string(), + amount: vec![coin(amount, &staking_info.bonded_denom)], + } + .into(), + )?; + } } _ => break, } @@ -1639,6 +1635,36 @@ mod test { assert_eq!(e.to_string(), "validator nonexistingvaloper not found"); } + #[test] + fn zero_staking_allowed() { + let (mut test_env, validator) = + TestEnv::wrap(setup_test_env(Decimal::percent(10), Decimal::percent(10))); + + let delegator = Addr::unchecked("delegator1"); + + // delegate 0 + execute_stake( + &mut test_env, + delegator.clone(), + StakingMsg::Delegate { + validator: validator.to_string(), + amount: coin(0, "TOKEN"), + }, + ) + .unwrap(); + + // undelegate 0 + execute_stake( + &mut test_env, + delegator, + StakingMsg::Undelegate { + validator: validator.to_string(), + amount: coin(0, "TOKEN"), + }, + ) + .unwrap(); + } + #[test] fn query_staking() { // run all staking queries From f02f3458dab573c2347743565d400709dbae91b3 Mon Sep 17 00:00:00 2001 From: Christoph Otter Date: Tue, 11 Oct 2022 09:04:18 +0200 Subject: [PATCH 24/25] Apply suggestions from code review Co-authored-by: Tomasz Kurcz --- packages/multi-test/src/staking.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/multi-test/src/staking.rs b/packages/multi-test/src/staking.rs index 08a8291bc..1dea0d279 100644 --- a/packages/multi-test/src/staking.rs +++ b/packages/multi-test/src/staking.rs @@ -75,6 +75,7 @@ impl ValidatorInfo { } const STAKING_INFO: Item = Item::new("staking_info"); +/// (staker_addr, validator_addr) -> shares const STAKES: Map<(&Addr, &Addr), Shares> = Map::new("stakes"); const VALIDATOR_MAP: Map<&Addr, Validator> = Map::new("validator_map"); /// Additional vec of validators, in case the `iterator` feature is disabled @@ -519,7 +520,7 @@ impl Module for StakeKeeper { &validator, amount.clone(), )?; - // move money from sender account to this module (note we can controller sender here) + // move money from sender account to this module (note we can control sender here) if !amount.amount.is_zero() { router.execute( api, From f1e2ee28db59caa5e49ee6d2021f5b58336bd28a Mon Sep 17 00:00:00 2001 From: Christoph Otter Date: Thu, 13 Oct 2022 12:48:21 +0200 Subject: [PATCH 25/25] Fix staking module --- packages/multi-test/src/staking.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/multi-test/src/staking.rs b/packages/multi-test/src/staking.rs index 1dea0d279..043838f32 100644 --- a/packages/multi-test/src/staking.rs +++ b/packages/multi-test/src/staking.rs @@ -9,12 +9,12 @@ use cosmwasm_std::{ DelegationResponse, DistributionMsg, Empty, Event, FullDelegation, Querier, StakingMsg, StakingQuery, Storage, Timestamp, Uint128, Validator, ValidatorResponse, }; -use cosmwasm_storage::{prefixed, prefixed_read}; use cw_storage_plus::{Deque, Item, Map}; use serde::{Deserialize, Serialize}; use crate::app::CosmosRouter; use crate::executor::AppResponse; +use crate::prefixed_storage::{prefixed, prefixed_read}; use crate::{BankSudo, Module}; // Contains some general staking parameters