use std::collections::BTreeSet;
use anyhow::{anyhow, bail, Result as AnyResult};
use schemars::JsonSchema;
use cosmwasm_std::{
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 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};
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct StakingInfo {
pub bonded_denom: String,
pub unbonding_time: u64,
pub apr: Decimal,
}
impl Default for StakingInfo {
fn default() -> Self {
StakingInfo {
bonded_denom: "TOKEN".to_string(),
unbonding_time: 60,
apr: Decimal::percent(10),
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, JsonSchema)]
struct Shares {
stake: Decimal,
rewards: Decimal,
}
impl Shares {
pub fn share_of_rewards(&self, validator: &ValidatorInfo, rewards: Decimal) -> Decimal {
rewards * self.stake / validator.stake
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
struct ValidatorInfo {
stakers: BTreeSet<Addr>,
stake: Uint128,
last_rewards_calculation: Timestamp,
}
impl ValidatorInfo {
pub fn new(block_time: Timestamp) -> Self {
Self {
stakers: BTreeSet::new(),
stake: Uint128::zero(),
last_rewards_calculation: block_time,
}
}
}
const STAKING_INFO: Item<StakingInfo> = Item::new("staking_info");
const STAKES: Map<(&Addr, &Addr), Shares> = Map::new("stakes");
const VALIDATOR_MAP: Map<&Addr, Validator> = Map::new("validator_map");
const VALIDATORS: Deque<Validator> = Deque::new("validators");
const VALIDATOR_INFO: Map<&Addr, ValidatorInfo> = Map::new("validator_info");
const UNBONDING_QUEUE: Deque<(Addr, Timestamp, u128)> = Deque::new("unbonding_queue");
pub const NAMESPACE_STAKING: &[u8] = b"staking";
#[derive(Clone, std::fmt::Debug, PartialEq, Eq, JsonSchema)]
pub enum StakingSudo {
Slash {
validator: String,
percentage: Decimal,
},
ProcessQueue {},
}
pub trait Staking: Module<ExecT = StakingMsg, QueryT = StakingQuery, SudoT = StakingSudo> {}
pub trait Distribution: Module<ExecT = DistributionMsg, QueryT = Empty, SudoT = Empty> {}
pub struct StakeKeeper {
module_addr: Addr,
}
impl Default for StakeKeeper {
fn default() -> Self {
Self::new()
}
}
impl StakeKeeper {
pub fn new() -> Self {
StakeKeeper {
module_addr: Addr::unchecked("staking_module"),
}
}
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 add_validator(
&self,
api: &dyn Api,
storage: &mut dyn Storage,
block: &BlockInfo,
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!(
"Cannot add validator {}, since a validator with that address already exists",
val_addr
);
}
VALIDATOR_MAP.save(&mut storage, &val_addr, &validator)?;
VALIDATORS.push_back(&mut storage, &validator)?;
VALIDATOR_INFO.save(&mut storage, &val_addr, &ValidatorInfo::new(block.time))?;
Ok(())
}
fn get_staking_info(staking_storage: &dyn Storage) -> AnyResult<StakingInfo> {
Ok(STAKING_INFO.may_load(staking_storage)?.unwrap_or_default())
}
pub fn get_rewards(
&self,
storage: &dyn Storage,
block: &BlockInfo,
delegator: &Addr,
validator: &Addr,
) -> AnyResult<Option<Coin>> {
let staking_storage = prefixed_read(storage, NAMESPACE_STAKING);
let validator_obj = match self.get_validator(&staking_storage, validator)? {
Some(validator) => validator,
None => bail!("validator {} not found", validator),
};
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<Coin> {
let staking_info = Self::get_staking_info(staking_storage)?;
let new_validator_rewards = Self::calculate_rewards(
block.time,
validator_info.last_rewards_calculation,
staking_info.apr,
validator.commission,
validator_info.stake,
);
let delegator_rewards =
shares.rewards + shares.share_of_rewards(validator_info, new_validator_rewards);
Ok(Coin {
denom: staking_info.bonded_denom,
amount: Uint128::new(1) * delegator_rewards, })
}
fn calculate_rewards(
current_time: Timestamp,
since: Timestamp,
interest_rate: Decimal,
validator_commission: Decimal,
stake: Uint128,
) -> Decimal {
let time_diff = current_time.minus_seconds(since.seconds()).seconds();
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;
reward - commission
}
fn update_rewards(
api: &dyn Api,
staking_storage: &mut dyn Storage,
block: &BlockInfo,
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", validator))?;
let validator_obj = VALIDATOR_MAP.load(staking_storage, validator)?;
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_obj.commission,
validator_info.stake,
);
if !new_rewards.is_zero() {
validator_info.last_rewards_calculation = block.time;
VALIDATOR_INFO.save(staking_storage, validator, &validator_info)?;
let validator_addr = api.addr_validate(&validator_obj.address)?;
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(())
}
fn get_validator(
&self,
staking_storage: &dyn Storage,
address: &Addr,
) -> AnyResult<Option<Validator>> {
Ok(VALIDATOR_MAP.may_load(staking_storage, address)?)
}
fn get_validators(&self, staking_storage: &dyn Storage) -> AnyResult<Vec<Validator>> {
let res: Result<_, _> = VALIDATORS.iter(staking_storage)?.collect();
Ok(res?)
}
fn get_stake(
&self,
staking_storage: &dyn Storage,
account: &Addr,
validator: &Addr,
) -> AnyResult<Option<Coin>> {
let shares = STAKES.may_load(staking_storage, (account, validator))?;
let staking_info = Self::get_staking_info(staking_storage)?;
Ok(shares.map(|shares| {
Coin {
denom: staking_info.bonded_denom,
amount: Uint128::new(1) * shares.stake, }
}))
}
fn add_stake(
&self,
api: &dyn Api,
staking_storage: &mut dyn Storage,
block: &BlockInfo,
to_address: &Addr,
validator: &Addr,
amount: Coin,
) -> AnyResult<()> {
self.validate_denom(staking_storage, &amount)?;
self.update_stake(
api,
staking_storage,
block,
to_address,
validator,
amount.amount,
false,
)
}
fn remove_stake(
&self,
api: &dyn Api,
staking_storage: &mut dyn Storage,
block: &BlockInfo,
from_address: &Addr,
validator: &Addr,
amount: Coin,
) -> AnyResult<()> {
self.validate_denom(staking_storage, &amount)?;
self.update_stake(
api,
staking_storage,
block,
from_address,
validator,
amount.amount,
true,
)
}
fn update_stake(
&self,
api: &dyn Api,
staking_storage: &mut dyn Storage,
block: &BlockInfo,
delegator: &Addr,
validator: &Addr,
amount: impl Into<Uint128>,
sub: bool,
) -> AnyResult<()> {
let amount = amount.into();
Self::update_rewards(api, staking_storage, block, 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();
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 {
shares.stake += amount_dec;
validator_info.stake = validator_info.stake.checked_add(amount)?;
}
if shares.stake.is_zero() {
STAKES.remove(staking_storage, (delegator, validator));
validator_info.stakers.remove(delegator);
} else {
STAKES.save(staking_storage, (delegator, validator), &shares)?;
validator_info.stakers.insert(delegator.clone());
}
VALIDATOR_INFO.save(staking_storage, validator, &validator_info)?;
Ok(())
}
fn slash(
&self,
api: &dyn Api,
staking_storage: &mut dyn Storage,
block: &BlockInfo,
validator: &Addr,
percentage: Decimal,
) -> AnyResult<()> {
Self::update_rewards(api, staking_storage, block, validator)?;
let mut validator_info = VALIDATOR_INFO
.may_load(staking_storage, validator)?
.ok_or_else(|| anyhow!("validator {} not found", validator))?;
let remaining_percentage = Decimal::one() - percentage;
validator_info.stake = validator_info.stake * remaining_percentage;
if validator_info.stake.is_zero() {
for delegator in validator_info.stakers.iter() {
STAKES.remove(staking_storage, (delegator, validator));
}
validator_info.stakers.clear();
} else {
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(())
}
fn validate_denom(&self, staking_storage: &dyn Storage, amount: &Coin) -> AnyResult<()> {
let staking_info = Self::get_staking_info(staking_storage)?;
ensure_eq!(
amount.denom,
staking_info.bonded_denom,
anyhow!(
"cannot delegate coins of denominator {}, only of {}",
amount.denom,
staking_info.bonded_denom
)
);
Ok(())
}
fn validate_percentage(&self, percentage: Decimal) -> AnyResult<()> {
ensure!(percentage <= Decimal::one(), anyhow!("expected percentage"));
Ok(())
}
}
impl Staking for StakeKeeper {}
impl Module for StakeKeeper {
type ExecT = StakingMsg;
type QueryT = StakingQuery;
type SudoT = StakingSudo;
fn execute<ExecC, QueryC: CustomQuery>(
&self,
api: &dyn Api,
storage: &mut dyn Storage,
router: &dyn CosmosRouter<ExecC = ExecC, QueryC = QueryC>,
block: &BlockInfo,
sender: Addr,
msg: StakingMsg,
) -> AnyResult<AppResponse> {
let mut staking_storage = prefixed(storage, NAMESPACE_STAKING);
match msg {
StakingMsg::Delegate { validator, amount } => {
let validator = api.addr_validate(&validator)?;
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())]; self.add_stake(
api,
&mut staking_storage,
block,
&sender,
&validator,
amount.clone(),
)?;
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)?;
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")]; self.remove_stake(
api,
&mut staking_storage,
block,
&sender,
&validator,
amount.clone(),
)?;
let staking_info = Self::get_staking_info(&staking_storage)?;
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 {
src_validator,
dst_validator,
amount,
} => {
let src_validator = api.addr_validate(&src_validator)?;
let dst_validator = api.addr_validate(&dst_validator)?;
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))];
self.remove_stake(
api,
&mut staking_storage,
block,
&sender,
&src_validator,
amount.clone(),
)?;
self.add_stake(
api,
&mut staking_storage,
block,
&sender,
&dst_validator,
amount,
)?;
Ok(AppResponse { events, data: None })
}
m => bail!("Unsupported staking message: {:?}", m),
}
}
fn sudo<ExecC, QueryC: CustomQuery>(
&self,
api: &dyn Api,
storage: &mut dyn Storage,
router: &dyn CosmosRouter<ExecC = ExecC, QueryC = QueryC>,
block: &BlockInfo,
msg: StakingSudo,
) -> AnyResult<AppResponse> {
match msg {
StakingSudo::Slash {
validator,
percentage,
} => {
let mut staking_storage = prefixed(storage, NAMESPACE_STAKING);
let validator = api.addr_validate(&validator)?;
self.validate_percentage(percentage)?;
self.slash(api, &mut staking_storage, block, &validator, percentage)?;
Ok(AppResponse::default())
}
StakingSudo::ProcessQueue {} => {
loop {
let mut staking_storage = prefixed(storage, NAMESPACE_STAKING);
let front = UNBONDING_QUEUE.front(&staking_storage)?;
match front {
Some((_, payout_at, _)) if payout_at <= block.time => {
let (delegator, _, amount) =
UNBONDING_QUEUE.pop_front(&mut staking_storage)?.unwrap();
let staking_info = Self::get_staking_info(&staking_storage)?;
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,
}
}
Ok(AppResponse::default())
}
}
}
fn query(
&self,
api: &dyn Api,
storage: &dyn Storage,
_querier: &dyn Querier,
block: &BlockInfo,
request: StakingQuery,
) -> AnyResult<Binary> {
let staking_storage = prefixed_read(storage, NAMESPACE_STAKING);
match request {
StakingQuery::BondedDenom {} => Ok(to_binary(&BondedDenomResponse {
denom: Self::get_staking_info(&staking_storage)?.bonded_denom,
})?),
StakingQuery::AllDelegations { delegator } => {
let delegator = api.addr_validate(&delegator)?;
let validators = self.get_validators(&staking_storage)?;
let res: AnyResult<Vec<Delegation>> = validators
.into_iter()
.filter_map(|validator| {
let delegator = delegator.clone();
let amount = self
.get_stake(
&staking_storage,
&delegator,
&Addr::unchecked(&validator.address),
)
.transpose()?;
Some(amount.map(|amount| Delegation {
delegator,
validator: validator.address,
amount,
}))
})
.collect();
Ok(to_binary(&AllDelegationsResponse { delegations: res? })?)
}
StakingQuery::Delegation {
delegator,
validator,
} => {
let validator_addr = Addr::unchecked(&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 shares = match STAKES.load(&staking_storage, (&delegator, &validator_addr)) {
Ok(stakes) => stakes,
Err(_) => {
let response = DelegationResponse { delegation: None };
return Ok(to_binary(&response)?);
}
};
let validator_info = VALIDATOR_INFO.load(&staking_storage, &validator_addr)?;
let reward = Self::get_rewards_internal(
&staking_storage,
block,
&shares,
&validator_obj,
&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: amount.clone(),
can_redelegate: amount, accumulated_rewards: if reward.amount.is_zero() {
vec![]
} else {
vec![reward]
},
}),
};
let res = to_binary(&full_delegation_response)?;
Ok(res)
}
StakingQuery::AllValidators {} => Ok(to_binary(&AllValidatorsResponse {
validators: self.get_validators(&staking_storage)?,
})?),
StakingQuery::Validator { address } => Ok(to_binary(&ValidatorResponse {
validator: self.get_validator(&staking_storage, &Addr::unchecked(address))?,
})?),
q => bail!("Unsupported staking sudo message: {:?}", q),
}
}
}
#[derive(Default)]
pub struct DistributionKeeper {}
impl DistributionKeeper {
pub fn new() -> Self {
DistributionKeeper {}
}
pub fn remove_rewards(
&self,
api: &dyn Api,
storage: &mut dyn Storage,
block: &BlockInfo,
delegator: &Addr,
validator: &Addr,
) -> AnyResult<Uint128> {
let mut staking_storage = prefixed(storage, NAMESPACE_STAKING);
StakeKeeper::update_rewards(api, &mut staking_storage, block, validator)?;
let mut shares = STAKES.load(&staking_storage, (delegator, validator))?;
let rewards = Uint128::new(1) * shares.rewards;
shares.rewards = Decimal::zero();
STAKES.save(&mut staking_storage, (delegator, validator), &shares)?;
Ok(rewards)
}
}
impl Distribution for DistributionKeeper {}
impl Module for DistributionKeeper {
type ExecT = DistributionMsg;
type QueryT = Empty;
type SudoT = Empty;
fn execute<ExecC, QueryC: CustomQuery>(
&self,
api: &dyn Api,
storage: &mut dyn Storage,
router: &dyn CosmosRouter<ExecC = ExecC, QueryC = QueryC>,
block: &BlockInfo,
sender: Addr,
msg: DistributionMsg,
) -> AnyResult<AppResponse> {
match msg {
DistributionMsg::WithdrawDelegatorReward { validator } => {
let validator_addr = api.addr_validate(&validator)?;
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)?;
router.sudo(
api,
storage,
block,
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!("{}{}", rewards, staking_info.bonded_denom),
)];
Ok(AppResponse { events, data: None })
}
m => bail!("Unsupported distribution message: {:?}", m),
}
}
fn sudo<ExecC, QueryC>(
&self,
_api: &dyn Api,
_storage: &mut dyn Storage,
_router: &dyn CosmosRouter<ExecC = ExecC, QueryC = QueryC>,
_block: &BlockInfo,
_msg: Empty,
) -> AnyResult<AppResponse> {
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<Binary> {
bail!("Something went wrong - Distribution doesn't have query messages")
}
}
#[cfg(test)]
mod test {
use crate::{app::MockRouter, BankKeeper, FailingModule, Router, WasmKeeper};
use super::*;
use cosmwasm_std::{
from_slice,
testing::{mock_env, MockApi, MockStorage},
BalanceResponse, BankQuery,
};
type BasicRouter<ExecC = Empty, QueryC = Empty> = Router<
BankKeeper,
FailingModule<ExecC, QueryC, Empty>,
WasmKeeper<ExecC, QueryC>,
StakeKeeper,
DistributionKeeper,
>;
fn mock_router() -> BasicRouter {
Router {
wasm: WasmKeeper::new(),
bank: BankKeeper::new(),
custom: FailingModule::new(),
staking: StakeKeeper::new(),
distribution: DistributionKeeper::new(),
}
}
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();
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();
let mut store = MockStorage::new();
let stake = StakeKeeper::default();
let block = mock_env().block;
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();
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);
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();
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();
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();
let mut staking_storage = prefixed(&mut store, NAMESPACE_STAKING);
stake
.add_stake(
&api,
&mut staking_storage,
&block,
&delegator,
&validator,
coin(100, "TOKEN"),
)
.unwrap();
stake
.sudo(
&api,
&mut store,
&router,
&block,
StakingSudo::Slash {
validator: "testvaloper1".to_string(),
percentage: Decimal::percent(50),
},
)
.unwrap();
let staking_storage = prefixed(&mut store, NAMESPACE_STAKING);
let stake_left = stake
.get_stake(&staking_storage, &delegator, &validator)
.unwrap();
assert_eq!(
stake_left.unwrap().amount.u128(),
50,
"should have slashed 50%"
);
stake
.sudo(
&api,
&mut store,
&router,
&block,
StakingSudo::Slash {
validator: "testvaloper1".to_string(),
percentage: Decimal::percent(100),
},
)
.unwrap();
let staking_storage = prefixed(&mut store, NAMESPACE_STAKING);
let stake_left = stake
.get_stake(&staking_storage, &delegator, &validator)
.unwrap();
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_env(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
.add_stake(
&api,
&mut staking_storage,
&block,
&delegator,
&validator,
coin(200, "TOKEN"),
)
.unwrap();
block.time = block.time.plus_seconds(60 * 60 * 24 * 365 / 2);
let rewards = stake
.get_rewards(&store, &block, &delegator, &validator)
.unwrap()
.unwrap();
assert_eq!(rewards.amount.u128(), 9, "should have 9 tokens reward");
distr
.execute(
&api,
&mut store,
&router,
&block,
delegator.clone(),
DistributionMsg::WithdrawDelegatorReward {
validator: validator.to_string(),
},
)
.unwrap();
let rewards = stake
.get_rewards(&store, &block, &delegator, &validator)
.unwrap()
.unwrap();
assert_eq!(rewards.amount.u128(), 0);
block.time = block.time.plus_seconds(60 * 60 * 24 * 365 / 2);
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_env(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");
let mut staking_storage = prefixed(&mut store, NAMESPACE_STAKING);
stake
.add_stake(
&api,
&mut staking_storage,
&block,
&delegator1,
&validator,
coin(100, "TOKEN"),
)
.unwrap();
stake
.add_stake(
&api,
&mut staking_storage,
&block,
&delegator2,
&validator,
coin(200, "TOKEN"),
)
.unwrap();
block.time = block.time.plus_seconds(60 * 60 * 24 * 365);
let rewards = stake
.get_rewards(&store, &block, &delegator1, &validator)
.unwrap()
.unwrap();
assert_eq!(rewards.amount.u128(), 9);
let rewards = stake
.get_rewards(&store, &block, &delegator2, &validator)
.unwrap()
.unwrap();
assert_eq!(rewards.amount.u128(), 18);
let mut staking_storage = prefixed(&mut store, NAMESPACE_STAKING);
stake
.add_stake(
&api,
&mut staking_storage,
&block,
&delegator1,
&validator,
coin(100, "TOKEN"),
)
.unwrap();
block.time = block.time.plus_seconds(60 * 60 * 24 * 365);
let rewards = stake
.get_rewards(&store, &block, &delegator1, &validator)
.unwrap()
.unwrap();
assert_eq!(rewards.amount.u128(), 27);
let rewards = stake
.get_rewards(&store, &block, &delegator2, &validator)
.unwrap()
.unwrap();
assert_eq!(rewards.amount.u128(), 36);
let mut staking_storage = prefixed(&mut store, NAMESPACE_STAKING);
stake
.remove_stake(
&api,
&mut staking_storage,
&block,
&delegator2,
&validator,
coin(100, "TOKEN"),
)
.unwrap();
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"
);
block.time = block.time.plus_seconds(60 * 60 * 24 * 365);
let rewards = stake
.get_rewards(&store, &block, &delegator1, &validator)
.unwrap()
.unwrap();
assert_eq!(rewards.amount.u128(), 18);
let rewards = stake
.get_rewards(&store, &block, &delegator2, &validator)
.unwrap()
.unwrap();
assert_eq!(rewards.amount.u128(), 45);
}
mod msg {
use cosmwasm_std::{from_slice, Addr, BondedDenomResponse, Decimal, StakingQuery};
use serde::de::DeserializeOwned;
use super::*;
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<AppResponse> {
env.router.staking.execute(
&env.api,
&mut env.store,
&env.router,
&env.block,
sender,
msg,
)
}
fn query_stake<T: DeserializeOwned>(env: &TestEnv, msg: StakingQuery) -> AnyResult<T> {
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<AppResponse> {
env.router.distribution.execute(
&env.api,
&mut env.store,
&env.router,
&env.block,
sender,
msg,
)
}
fn query_bank<T: DeserializeOwned>(env: &TestEnv, msg: BankQuery) -> AnyResult<T> {
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<Item = (Addr, u128)>) {
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() {
let (mut test_env, validator1) =
TestEnv::wrap(setup_test_env(Decimal::percent(10), Decimal::percent(10)));
let delegator1 = Addr::unchecked("delegator1");
test_env
.router
.bank
.init_balance(&mut test_env.store, &delegator1, vec![coin(1000, "TOKEN")])
.unwrap();
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();
execute_stake(
&mut test_env,
delegator1.clone(),
StakingMsg::Delegate {
validator: validator1.to_string(),
amount: coin(100, "TOKEN"),
},
)
.unwrap();
assert_balances(&test_env, vec![(delegator1.clone(), 900)]);
test_env.block.time = test_env.block.time.plus_seconds(60 * 60 * 24 * 365);
execute_distr(
&mut test_env,
delegator1.clone(),
DistributionMsg::WithdrawDelegatorReward {
validator: validator1.to_string(),
},
)
.unwrap();
execute_stake(
&mut test_env,
delegator1.clone(),
StakingMsg::Redelegate {
src_validator: validator1.to_string(),
dst_validator: validator2.to_string(),
amount: coin(100, "TOKEN"),
},
)
.unwrap();
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"),
}]
);
execute_stake(
&mut test_env,
delegator1.clone(),
StakingMsg::Undelegate {
validator: validator2.to_string(),
amount: coin(100, "TOKEN"),
},
)
.unwrap();
test_env.block.time = test_env.block.time.plus_seconds(60);
test_env
.router
.staking
.sudo(
&test_env.api,
&mut test_env.store,
&test_env.router,
&test_env.block,
StakingSudo::ProcessQueue {},
)
.unwrap();
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");
test_env
.router
.bank
.init_balance(&mut test_env.store, &delegator1, vec![coin(100, "TOKEN")])
.unwrap();
execute_stake(
&mut test_env,
delegator1.clone(),
StakingMsg::Delegate {
validator: validator1.to_string(),
amount: coin(100, "TOKEN"),
},
)
.unwrap();
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");
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();
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 denom_validation() {
let (mut test_env, validator) =
TestEnv::wrap(setup_test_env(Decimal::percent(10), Decimal::percent(10)));
let delegator1 = Addr::unchecked("delegator1");
test_env
.router
.bank
.init_balance(&mut test_env.store, &delegator1, vec![coin(100, "FAKE")])
.unwrap();
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");
test_env
.router
.bank
.init_balance(&mut test_env.store, &delegator1, vec![coin(100, "FAKE")])
.unwrap();
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 zero_staking_allowed() {
let (mut test_env, validator) =
TestEnv::wrap(setup_test_env(Decimal::percent(10), Decimal::percent(10)));
let delegator = Addr::unchecked("delegator1");
execute_stake(
&mut test_env,
delegator.clone(),
StakingMsg::Delegate {
validator: validator.to_string(),
amount: coin(0, "TOKEN"),
},
)
.unwrap();
execute_stake(
&mut test_env,
delegator,
StakingMsg::Undelegate {
validator: validator.to_string(),
amount: coin(0, "TOKEN"),
},
)
.unwrap();
}
#[test]
fn query_staking() {
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");
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();
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();
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]
);
let response = query_stake::<ValidatorResponse>(
&test_env,
StakingQuery::Validator {
address: "notvaloper".to_string(),
},
)
.unwrap();
assert_eq!(response.validator, None);
let response: BondedDenomResponse =
query_stake(&test_env, StakingQuery::BondedDenom {}).unwrap();
assert_eq!(response.denom, "TOKEN");
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();
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"),
},
);
}
}
}