use crate::app::CosmosRouter;
use crate::error::std_error_bail;
use crate::executor::AppResponse;
use crate::prefixed_storage::typed_prefixed_storage::{
StoragePrefix, TypedPrefixedStorage, TypedPrefixedStorageMut,
};
use crate::{BankSudo, Module};
use cosmwasm_std::{
ensure, ensure_eq, to_json_binary, Addr, AllDelegationsResponse, AllValidatorsResponse, Api,
BankMsg, Binary, BlockInfo, BondedDenomResponse, Coin, CustomMsg, CustomQuery, Decimal256,
Delegation, DelegationResponse, DelegatorWithdrawAddressResponse, DistributionMsg,
DistributionQuery, Empty, Event, FullDelegation, Order, Querier, StakingMsg, StakingQuery,
StdError, StdResult, Storage, Timestamp, Uint256, Validator, ValidatorResponse,
};
#[cfg(feature = "cosmwasm_1_4")]
use cosmwasm_std::{
DecCoin, DelegationRewardsResponse, DelegationTotalRewardsResponse, DelegatorReward,
DelegatorValidatorsResponse,
};
use cw_storage_plus::{Deque, Item, Map};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeSet, VecDeque};
const BONDED_DENOM: &str = "TOKEN";
const YEAR: u64 = 60 * 60 * 24 * 365;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
pub struct StakingInfo {
pub bonded_denom: String,
pub unbonding_time: u64,
pub apr: Decimal256,
}
impl Default for StakingInfo {
fn default() -> Self {
StakingInfo {
bonded_denom: BONDED_DENOM.to_string(),
unbonding_time: 60,
apr: Decimal256::percent(10),
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, JsonSchema)]
struct Shares {
stake: Decimal256,
rewards: Decimal256,
}
impl Shares {
pub fn share_of_rewards(
&self,
validator_info: &ValidatorInfo,
rewards: Decimal256,
) -> Decimal256 {
if validator_info.stake.is_zero() {
return Decimal256::zero();
}
rewards * self.stake / validator_info.stake
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
struct ValidatorInfo {
stakers: BTreeSet<Addr>,
stake: Uint256,
last_rewards_calculation: Timestamp,
}
impl ValidatorInfo {
pub fn new(block_time: Timestamp) -> Self {
Self {
stakers: BTreeSet::new(),
stake: Uint256::zero(),
last_rewards_calculation: block_time,
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
struct Unbonding {
pub delegator: Addr,
pub validator: String,
pub amount: Uint256,
pub payout_at: Timestamp,
}
const STAKING_INFO: Item<StakingInfo> = Item::new("staking_info");
const STAKES: Map<(&Addr, &str), Shares> = Map::new("stakes");
const VALIDATOR_MAP: Map<&str, Validator> = Map::new("validator_map");
const VALIDATORS: Deque<Validator> = Deque::new("validators");
const VALIDATOR_INFO: Map<&str, ValidatorInfo> = Map::new("validator_info");
const UNBONDING_QUEUE: Item<VecDeque<Unbonding>> = Item::new("unbonding_queue");
const WITHDRAW_ADDRESS: Map<&Addr, Addr> = Map::new("withdraw_address");
#[derive(Clone, Debug, PartialEq, Eq, JsonSchema)]
pub enum StakingSudo {
Slash {
validator: String,
percentage: Decimal256,
},
}
pub trait Staking: Module<ExecT = StakingMsg, QueryT = StakingQuery, SudoT = StakingSudo> {
fn process_queue<ExecC: CustomMsg, QueryC: CustomQuery>(
&self,
_api: &dyn Api,
_storage: &mut dyn Storage,
_router: &dyn CosmosRouter<ExecC = ExecC, QueryC = QueryC>,
_block: &BlockInfo,
) -> StdResult<AppResponse> {
Ok(AppResponse::default())
}
}
pub trait Distribution:
Module<ExecT = DistributionMsg, QueryT = DistributionQuery, SudoT = Empty>
{
}
pub struct StakeKeeper {
module_addr: Addr,
}
impl Default for StakeKeeper {
fn default() -> Self {
StakeKeeper {
module_addr: Addr::unchecked("staking_module"),
}
}
}
impl StakeKeeper {
pub fn new() -> Self {
Self::default()
}
pub fn setup(&self, storage: &mut dyn Storage, staking_info: StakingInfo) -> StdResult<()> {
STAKING_INFO.save(&mut StakingStorageMut::new(storage), &staking_info)?;
Ok(())
}
pub fn add_validator(
&self,
_api: &dyn Api,
storage: &mut dyn Storage,
block: &BlockInfo,
validator: Validator,
) -> StdResult<()> {
let mut storage = StakingStorageMut::new(storage);
if VALIDATOR_MAP
.may_load(&storage, &validator.address)?
.is_some()
{
std_error_bail!(
"Cannot add validator {}, since a validator with that address already exists",
validator.address
);
}
VALIDATOR_MAP.save(&mut storage, &validator.address, &validator)?;
VALIDATORS.push_back(&mut storage, &validator)?;
VALIDATOR_INFO.save(
&mut storage,
&validator.address,
&ValidatorInfo::new(block.time),
)?;
Ok(())
}
fn get_staking_info(storage: &StakingStorage) -> StdResult<StakingInfo> {
Ok(STAKING_INFO.may_load(storage)?.unwrap_or_default())
}
pub fn get_rewards(
&self,
storage: &dyn Storage,
block: &BlockInfo,
delegator: &Addr,
validator: &str,
) -> StdResult<Option<Coin>> {
Self::get_rewards_internal(storage, block, delegator, validator)
}
fn get_rewards_internal(
storage: &dyn Storage,
block: &BlockInfo,
delegator: &Addr,
validator: &str,
) -> StdResult<Option<Coin>> {
let staking_storage = StakingStorage::new(storage);
let validator_obj = match Self::get_validator(&staking_storage, validator)? {
Some(validator) => validator,
None => std_error_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_from_validator(
&staking_storage,
block,
&shares,
&validator_obj,
&validator_info,
)
.map(Some)
}
fn get_rewards_from_validator(
storage: &StakingStorage,
block: &BlockInfo,
shares: &Shares,
validator: &Validator,
validator_info: &ValidatorInfo,
) -> StdResult<Coin> {
let staking_info = Self::get_staking_info(storage)?;
let new_validator_rewards = Self::calculate_rewards(
block.time,
validator_info.last_rewards_calculation,
staking_info.apr,
validator.commission.into(),
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: Uint256::new(1).mul_floor(delegator_rewards), })
}
fn calculate_rewards(
current_time: Timestamp,
since: Timestamp,
interest_rate: Decimal256,
validator_commission: Decimal256,
stake: Uint256,
) -> Decimal256 {
let time_diff = current_time.minus_seconds(since.seconds()).seconds();
let reward = Decimal256::from_ratio(stake, 1u128)
* interest_rate
* Decimal256::from_ratio(time_diff, 1u128)
/ Decimal256::from_ratio(YEAR, 1u128);
let commission = reward * validator_commission;
reward - commission
}
fn update_rewards(
_api: &dyn Api,
storage: &mut StakingStorageMut,
block: &BlockInfo,
validator: &str,
) -> StdResult<()> {
let staking_info = Self::get_staking_info(&storage.borrow())?;
let mut validator_info = VALIDATOR_INFO
.may_load(storage, validator)?
.ok_or_else(|| StdError::msg("validator does not exist"))?;
let validator_obj = VALIDATOR_MAP.load(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.into(),
validator_info.stake,
);
validator_info.last_rewards_calculation = block.time;
VALIDATOR_INFO.save(storage, validator, &validator_info)?;
if !new_rewards.is_zero() {
for staker in validator_info.stakers.iter() {
STAKES.update(
storage,
(staker, &validator_obj.address),
|shares| -> StdResult<_> {
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(storage: &StakingStorage, address: &str) -> StdResult<Option<Validator>> {
VALIDATOR_MAP.may_load(storage, address)
}
fn get_validators(&self, storage: &StakingStorage) -> StdResult<Vec<Validator>> {
VALIDATORS.iter(storage)?.collect()
}
fn get_stake(
&self,
storage: &StakingStorage,
account: &Addr,
validator: &str,
) -> StdResult<Option<Coin>> {
let shares = STAKES.may_load(storage, (account, validator))?;
let staking_info = Self::get_staking_info(storage)?;
Ok(shares.map(|shares| {
Coin {
denom: staking_info.bonded_denom,
amount: Uint256::new(1).mul_floor(shares.stake), }
}))
}
fn add_stake(
&self,
api: &dyn Api,
storage: &mut StakingStorageMut,
block: &BlockInfo,
to_address: &Addr,
validator: &str,
amount: Coin,
) -> StdResult<()> {
self.validate_denom(&storage.borrow(), &amount)?;
self.update_stake(
api,
storage,
block,
to_address,
validator,
amount.amount,
false,
)
}
fn remove_stake(
&self,
api: &dyn Api,
storage: &mut StakingStorageMut,
block: &BlockInfo,
from_address: &Addr,
validator: &str,
amount: Coin,
) -> StdResult<()> {
self.validate_denom(&storage.borrow(), &amount)?;
self.update_stake(
api,
storage,
block,
from_address,
validator,
amount.amount,
true,
)
}
fn update_stake(
&self,
api: &dyn Api,
storage: &mut StakingStorageMut,
block: &BlockInfo,
delegator: &Addr,
validator: &str,
amount: impl Into<Uint256>,
sub: bool,
) -> StdResult<()> {
let amount = amount.into();
Self::update_rewards(api, storage, block, validator)?;
let mut validator_info = VALIDATOR_INFO
.may_load(storage, validator)?
.unwrap_or_else(|| ValidatorInfo::new(block.time));
let shares = STAKES.may_load(storage, (delegator, validator))?;
let mut shares = if sub {
shares.ok_or_else(|| StdError::msg("no delegation for (address, validator) tuple"))?
} else {
shares.unwrap_or_default()
};
let amount_dec = Decimal256::from_ratio(amount, 1u128);
if sub {
if amount_dec > shares.stake {
std_error_bail!("invalid shares amount");
}
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(storage, (delegator, validator));
validator_info.stakers.remove(delegator);
} else {
STAKES.save(storage, (delegator, validator), &shares)?;
validator_info.stakers.insert(delegator.clone());
}
VALIDATOR_INFO.save(storage, validator, &validator_info)?;
Ok(())
}
fn slash(
&self,
api: &dyn Api,
storage: &mut StakingStorageMut,
block: &BlockInfo,
validator: &str,
percentage: Decimal256,
) -> StdResult<()> {
Self::update_rewards(api, storage, block, validator)?;
let mut validator_info = VALIDATOR_INFO.may_load(storage, validator)?.unwrap();
let remaining_percentage = Decimal256::one() - percentage;
validator_info.stake = validator_info.stake.mul_floor(remaining_percentage);
if validator_info.stake.is_zero() {
for delegator in validator_info.stakers.iter() {
STAKES.remove(storage, (delegator, validator));
}
validator_info.stakers.clear();
} else {
for delegator in validator_info.stakers.iter() {
STAKES.update(storage, (delegator, validator), |stake| -> StdResult<_> {
let mut stake = stake.expect("all stakers in validator_info should exist");
stake.stake *= remaining_percentage;
Ok(stake)
})?;
}
}
let mut unbonding_queue = UNBONDING_QUEUE.may_load(storage)?.unwrap_or_default();
unbonding_queue
.iter_mut()
.filter(|ub| ub.validator == validator)
.for_each(|ub| {
ub.amount = ub.amount.mul_floor(remaining_percentage);
});
UNBONDING_QUEUE.save(storage, &unbonding_queue)?;
VALIDATOR_INFO.save(storage, validator, &validator_info)?;
Ok(())
}
fn validate_denom(&self, storage: &StakingStorage, amount: &Coin) -> StdResult<()> {
let staking_info = Self::get_staking_info(storage)?;
ensure_eq!(
amount.denom,
staking_info.bonded_denom,
StdError::msg(format!(
"cannot delegate coins of denominator {}, only of {}",
amount.denom, staking_info.bonded_denom
))
);
Ok(())
}
fn validate_percentage(&self, percentage: Decimal256) -> StdResult<()> {
ensure!(
percentage <= Decimal256::one(),
StdError::msg("expected percentage")
);
Ok(())
}
fn process_queue<ExecC: CustomMsg, QueryC: CustomQuery>(
&self,
api: &dyn Api,
storage: &mut dyn Storage,
router: &dyn CosmosRouter<ExecC = ExecC, QueryC = QueryC>,
block: &BlockInfo,
) -> StdResult<AppResponse> {
let mut unbonding_queue = UNBONDING_QUEUE
.may_load(&StakingStorage::new(storage))?
.unwrap_or_default();
loop {
match unbonding_queue.front() {
Some(Unbonding { payout_at, .. }) if payout_at <= &block.time => {
let mut staking_storage_mut = StakingStorageMut::new(storage);
let Unbonding {
delegator,
validator,
amount,
..
} = unbonding_queue.pop_front().unwrap();
let delegation = self
.get_stake(&staking_storage_mut.borrow(), &delegator, &validator)?
.map(|mut stake| {
stake.amount += unbonding_queue
.iter()
.filter(|u| u.delegator == delegator && u.validator == validator)
.map(|u| u.amount)
.sum::<Uint256>();
stake
});
match delegation {
Some(delegation) if delegation.amount.is_zero() => {
STAKES.remove(&mut staking_storage_mut, (&delegator, &validator));
}
None => {
STAKES.remove(&mut staking_storage_mut, (&delegator, &validator));
}
_ => {}
}
let staking_info = Self::get_staking_info(&staking_storage_mut.borrow())?;
if !amount.is_zero() {
router.execute(
api,
storage,
block,
self.module_addr.clone(),
BankMsg::Send {
to_address: delegator.into_string(),
amount: vec![Coin::new(amount, &staking_info.bonded_denom)],
}
.into(),
)?;
}
}
_ => break,
}
}
UNBONDING_QUEUE.save(&mut StakingStorageMut::new(storage), &unbonding_queue)?;
Ok(AppResponse::default())
}
}
impl Staking for StakeKeeper {
fn process_queue<ExecC: CustomMsg, QueryC: CustomQuery>(
&self,
api: &dyn Api,
storage: &mut dyn Storage,
router: &dyn CosmosRouter<ExecC = ExecC, QueryC = QueryC>,
block: &BlockInfo,
) -> StdResult<AppResponse> {
self.process_queue(api, storage, router, block)
}
}
impl StoragePrefix for StakeKeeper {
const NAMESPACE: &'static [u8] = b"staking";
}
type StakingStorage<'a> = TypedPrefixedStorage<'a, StakeKeeper>;
type StakingStorageMut<'a> = TypedPrefixedStorageMut<'a, StakeKeeper>;
impl Module for StakeKeeper {
type ExecT = StakingMsg;
type QueryT = StakingQuery;
type SudoT = StakingSudo;
fn execute<ExecC: CustomMsg, QueryC: CustomQuery>(
&self,
api: &dyn Api,
storage: &mut dyn Storage,
router: &dyn CosmosRouter<ExecC = ExecC, QueryC = QueryC>,
block: &BlockInfo,
sender: Addr,
msg: StakingMsg,
) -> StdResult<AppResponse> {
let mut staking_storage_mut = StakingStorageMut::new(storage);
match msg {
StakingMsg::Delegate { validator, amount } => {
if amount.amount.is_zero() {
std_error_bail!("invalid delegation amount");
}
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_mut,
block,
&sender,
&validator,
amount.clone(),
)?;
router.execute(
api,
storage,
block,
sender,
BankMsg::Send {
to_address: self.module_addr.to_string(),
amount: vec![amount],
}
.into(),
)?;
Ok(AppResponse {
events,
..Default::default()
})
}
StakingMsg::Undelegate { validator, amount } => {
self.validate_denom(&staking_storage_mut.borrow(), &amount)?;
if amount.amount.is_zero() {
std_error_bail!("invalid shares 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_mut,
block,
&sender,
&validator,
amount.clone(),
)?;
let staking_info = Self::get_staking_info(&staking_storage_mut.borrow())?;
let mut unbonding_queue = UNBONDING_QUEUE
.may_load(&staking_storage_mut)?
.unwrap_or_default();
unbonding_queue.push_back(Unbonding {
delegator: sender.clone(),
validator,
amount: amount.amount,
payout_at: block.time.plus_seconds(staking_info.unbonding_time),
});
UNBONDING_QUEUE.save(&mut staking_storage_mut, &unbonding_queue)?;
Ok(AppResponse {
events,
..Default::default()
})
}
StakingMsg::Redelegate {
src_validator,
dst_validator,
amount,
} => {
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_mut,
block,
&sender,
&src_validator,
amount.clone(),
)?;
self.add_stake(
api,
&mut staking_storage_mut,
block,
&sender,
&dst_validator,
amount,
)?;
Ok(AppResponse {
events,
..Default::default()
})
}
m => std_error_bail!("Unsupported staking message: {:?}", m),
}
}
fn query(
&self,
api: &dyn Api,
storage: &dyn Storage,
_querier: &dyn Querier,
block: &BlockInfo,
request: StakingQuery,
) -> StdResult<Binary> {
let staking_storage = StakingStorage::new(storage);
match request {
StakingQuery::BondedDenom {} => Ok(to_json_binary(&BondedDenomResponse::new(
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: StdResult<Vec<Delegation>> =
validators
.into_iter()
.filter_map(|validator| {
let delegator = delegator.clone();
let amount = self
.get_stake(&staking_storage, &delegator, &validator.address)
.transpose()?;
Some(amount.map(|amount| {
Delegation::new(delegator, validator.address, amount)
}))
})
.collect();
Ok(to_json_binary(&AllDelegationsResponse::new(res?))?)
}
StakingQuery::Delegation {
delegator,
validator,
} => {
let validator_obj = match Self::get_validator(&staking_storage, &validator)? {
Some(validator) => validator,
None => std_error_bail!("non-existent validator {}", validator),
};
let delegator = api.addr_validate(&delegator)?;
let shares = STAKES
.may_load(&staking_storage, (&delegator, &validator))?
.unwrap_or_default();
let validator_info = VALIDATOR_INFO.load(&staking_storage, &validator)?;
let reward = Self::get_rewards_from_validator(
&staking_storage,
block,
&shares,
&validator_obj,
&validator_info,
)?;
let staking_info = Self::get_staking_info(&staking_storage)?;
let amount = Coin::new(
Uint256::new(1).mul_floor(shares.stake),
staking_info.bonded_denom,
);
let full_delegation_response = if amount.amount.is_zero() {
DelegationResponse::new(None)
} else {
DelegationResponse::new(Some(FullDelegation::new(
delegator,
validator,
amount.clone(),
amount, if reward.amount.is_zero() {
vec![]
} else {
vec![reward]
},
)))
};
let res = to_json_binary(&full_delegation_response)?;
Ok(res)
}
StakingQuery::AllValidators {} => {
let validators: Vec<Validator> = self.get_validators(&staking_storage)?;
Ok(to_json_binary(&AllValidatorsResponse::new(
validators.into_iter().map(Into::into).collect(),
))?)
}
StakingQuery::Validator { address } => Ok(to_json_binary(&ValidatorResponse::new(
Self::get_validator(&staking_storage, &address)?,
))?),
q => std_error_bail!("Unsupported staking sudo message: {:?}", q),
}
}
fn sudo<ExecC: CustomMsg, QueryC: CustomQuery>(
&self,
api: &dyn Api,
storage: &mut dyn Storage,
_router: &dyn CosmosRouter<ExecC = ExecC, QueryC = QueryC>,
block: &BlockInfo,
msg: StakingSudo,
) -> StdResult<AppResponse> {
match msg {
StakingSudo::Slash {
validator,
percentage,
} => {
let mut staking_storage = StakingStorageMut::new(storage);
self.validate_percentage(percentage)?;
self.slash(api, &mut staking_storage, block, &validator, percentage)?;
Ok(AppResponse::default())
}
}
}
}
#[derive(Default)]
pub struct DistributionKeeper {}
impl DistributionKeeper {
pub fn new() -> Self {
Self::default()
}
pub fn remove_rewards(
&self,
api: &dyn Api,
storage: &mut dyn Storage,
block: &BlockInfo,
delegator: &Addr,
validator: &str,
) -> StdResult<Uint256> {
let mut staking_storage_mut = StakingStorageMut::new(storage);
StakeKeeper::update_rewards(api, &mut staking_storage_mut, block, validator)?;
let mut shares = STAKES.load(&staking_storage_mut, (delegator, validator))?;
let rewards = Uint256::new(1).mul_floor(shares.rewards);
shares.rewards = Decimal256::zero();
STAKES.save(&mut staking_storage_mut, (delegator, validator), &shares)?;
Ok(rewards)
}
pub fn get_withdraw_address(storage: &dyn Storage, delegator_addr: &Addr) -> StdResult<Addr> {
let storage = DistributionStorage::new(storage);
Ok(match WITHDRAW_ADDRESS.may_load(&storage, delegator_addr)? {
Some(withdraw_addr) => withdraw_addr,
None => delegator_addr.clone(),
})
}
pub fn set_withdraw_address(
storage: &mut dyn Storage,
delegator_addr: &Addr,
withdraw_addr: &Addr,
) -> StdResult<()> {
let mut storage = DistributionStorageMut::new(storage);
if delegator_addr == withdraw_addr {
WITHDRAW_ADDRESS.remove(&mut storage, delegator_addr);
Ok(())
} else {
WITHDRAW_ADDRESS.save(&mut storage, delegator_addr, withdraw_addr)
}
}
pub fn get_delegator_validators(
&self,
storage: &dyn Storage,
delegator_addr: &Addr,
) -> StdResult<Vec<String>> {
let storage = StakingStorage::new(storage);
STAKES
.prefix(delegator_addr)
.keys(&storage, None, None, Order::Ascending)
.collect::<Result<Vec<String>, StdError>>()
}
#[cfg(feature = "cosmwasm_1_4")]
pub fn get_rewards(
&self,
storage: &dyn Storage,
block: &BlockInfo,
delegator_address: &Addr,
validator_address: &str,
) -> StdResult<Option<DecCoin>> {
Ok(
if let Some(coin) = StakeKeeper::get_rewards_internal(
storage,
block,
delegator_address,
validator_address,
)? {
Some(DecCoin::new(
Decimal256::from_atomics(coin.amount, 0)?,
coin.denom,
))
} else {
None
},
)
}
}
impl Distribution for DistributionKeeper {}
impl StoragePrefix for DistributionKeeper {
const NAMESPACE: &'static [u8] = b"distribution";
}
type DistributionStorage<'a> = TypedPrefixedStorage<'a, DistributionKeeper>;
type DistributionStorageMut<'a> = TypedPrefixedStorageMut<'a, DistributionKeeper>;
impl Module for DistributionKeeper {
type ExecT = DistributionMsg;
type QueryT = DistributionQuery;
type SudoT = Empty;
fn execute<ExecC: CustomMsg, QueryC: CustomQuery>(
&self,
api: &dyn Api,
storage: &mut dyn Storage,
router: &dyn CosmosRouter<ExecC = ExecC, QueryC = QueryC>,
block: &BlockInfo,
sender: Addr,
msg: DistributionMsg,
) -> StdResult<AppResponse> {
match msg {
DistributionMsg::WithdrawDelegatorReward { validator } => {
let rewards = self.remove_rewards(api, storage, block, &sender, &validator)?;
let staking_storage = StakingStorage::new(storage);
let staking_info = StakeKeeper::get_staking_info(&staking_storage)?;
let receiver = Self::get_withdraw_address(storage, &sender)?;
router.sudo(
api,
storage,
block,
BankSudo::Mint {
to_address: receiver.into_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,
..Default::default()
})
}
DistributionMsg::SetWithdrawAddress { address } => {
let address = api.addr_validate(&address)?;
Self::set_withdraw_address(storage, &sender, &address)?;
Ok(AppResponse {
events: vec![Event::new("set_withdraw_address")
.add_attribute("withdraw_address", address)],
..Default::default()
})
}
other => std_error_bail!("Unsupported distribution message: {:?}", other),
}
}
fn query(
&self,
api: &dyn Api,
storage: &dyn Storage,
_querier: &dyn Querier,
block: &BlockInfo,
request: DistributionQuery,
) -> StdResult<Binary> {
match request {
#[cfg(feature = "cosmwasm_1_4")]
DistributionQuery::DelegatorValidators { delegator_address } => {
let delegator_address = api.addr_validate(&delegator_address)?;
let validators = self.get_delegator_validators(storage, &delegator_address)?;
Ok(to_json_binary(&DelegatorValidatorsResponse::new(
validators,
))?)
}
DistributionQuery::DelegatorWithdrawAddress { delegator_address } => {
let delegator_address = api.addr_validate(&delegator_address)?;
let withdraw_address = Self::get_withdraw_address(storage, &delegator_address)?;
Ok(to_json_binary(&DelegatorWithdrawAddressResponse::new(
withdraw_address,
))?)
}
#[cfg(feature = "cosmwasm_1_4")]
DistributionQuery::DelegationRewards {
delegator_address,
validator_address,
} => {
let delegator_address = api.addr_validate(&delegator_address)?;
let rewards = if let Some(dec_coin) =
self.get_rewards(storage, block, &delegator_address, &validator_address)?
{
vec![dec_coin]
} else {
vec![]
};
Ok(to_json_binary(&DelegationRewardsResponse::new(rewards))?)
}
#[cfg(feature = "cosmwasm_1_4")]
DistributionQuery::DelegationTotalRewards { delegator_address } => {
let delegator_address = api.addr_validate(&delegator_address)?;
let mut delegator_rewards = vec![];
let mut total_rewards = std::collections::BTreeMap::new();
for validator_address in
self.get_delegator_validators(storage, &delegator_address)?
{
if let Some(dec_coin) =
self.get_rewards(storage, block, &delegator_address, &validator_address)?
{
delegator_rewards.push(DelegatorReward::new(
validator_address.clone(),
vec![dec_coin.clone()],
));
total_rewards
.entry(dec_coin.denom)
.and_modify(|value| *value += dec_coin.amount)
.or_insert(dec_coin.amount);
}
}
let total_rewards = total_rewards
.iter()
.map(|(denom, amount)| DecCoin::new(*amount, denom))
.collect();
Ok(to_json_binary(&DelegationTotalRewardsResponse::new(
delegator_rewards,
total_rewards,
))?)
}
other => {
let _ = block; std_error_bail!("Unsupported distribution query: {:?}", other)
}
}
}
fn sudo<ExecC, QueryC>(
&self,
_api: &dyn Api,
_storage: &mut dyn Storage,
_router: &dyn CosmosRouter<ExecC = ExecC, QueryC = QueryC>,
_block: &BlockInfo,
_msg: Empty,
) -> StdResult<AppResponse> {
std_error_bail!("Something went wrong - distribution doesn't have sudo messages")
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::{
BankKeeper, FailingModule, GovFailingModule, IbcFailingModule, IntoBech32, Router,
StargateFailing, WasmKeeper,
};
use cosmwasm_std::{
coin, coins, from_json,
testing::{mock_env, MockApi, MockStorage},
BalanceResponse, BankQuery, Decimal, QuerierWrapper, Uint256,
};
use serde::de::DeserializeOwned;
struct ValidatorProperties {
commission: Decimal,
max_commission: Decimal,
max_change_rate: Decimal,
}
fn vp(commission: u64, max_commission: u64, max_change_rate: u64) -> ValidatorProperties {
ValidatorProperties {
commission: Decimal::percent(commission),
max_commission: Decimal::percent(max_commission),
max_change_rate: Decimal::percent(max_change_rate),
}
}
type BasicRouter<ExecC = Empty, QueryC = Empty> = Router<
BankKeeper,
FailingModule<ExecC, QueryC, Empty>,
WasmKeeper<ExecC, QueryC>,
StakeKeeper,
DistributionKeeper,
IbcFailingModule,
GovFailingModule,
StargateFailing,
>;
struct TestEnv {
api: MockApi,
storage: MockStorage,
router: BasicRouter,
block: BlockInfo,
validator_addr_1: String,
validator_addr_2: String,
validator_addr_3: String,
delegator_addr_1: Addr,
delegator_addr_2: Addr,
user_addr_1: Addr,
}
impl TestEnv {
fn new(validator1: ValidatorProperties, validator2: ValidatorProperties) -> Self {
fn validator_address(value: &str) -> String {
value.into_bech32_with_prefix("cosmwasmvaloper").to_string()
}
fn user_address(api: &MockApi, value: &str) -> Addr {
api.addr_make(value)
}
let api = MockApi::default();
let router = Router {
wasm: WasmKeeper::new(),
bank: BankKeeper::new(),
custom: FailingModule::new(),
staking: StakeKeeper::new(),
distribution: DistributionKeeper::new(),
ibc: IbcFailingModule::new(),
gov: GovFailingModule::new(),
stargate: StargateFailing,
};
let mut storage = MockStorage::new();
let block = mock_env().block;
let validator_addr_1 = validator_address("validator1");
let validator_addr_2 = validator_address("validator2");
let validator_addr_3 = validator_address("validator3");
router
.staking
.setup(&mut storage, StakingInfo::default())
.unwrap();
let valoper1 = Validator::new(
validator_addr_1.to_string(),
validator1.commission,
validator1.max_commission,
validator1.max_change_rate,
);
router
.staking
.add_validator(&api, &mut storage, &block, valoper1)
.unwrap();
let valoper2 = Validator::new(
validator_addr_2.to_string(),
validator2.commission,
validator2.max_commission,
validator2.max_change_rate,
);
router
.staking
.add_validator(&api, &mut storage, &block, valoper2)
.unwrap();
Self {
api,
storage,
router,
block,
validator_addr_1,
validator_addr_2,
validator_addr_3,
delegator_addr_1: user_address(&api, "delegator1"),
delegator_addr_2: user_address(&api, "delegator2"),
user_addr_1: user_address(&api, "user1"),
}
}
#[inline(always)]
fn validator_addr_1(&self) -> String {
self.validator_addr_1.clone()
}
#[inline(always)]
fn validator_addr_2(&self) -> String {
self.validator_addr_2.clone()
}
#[inline(always)]
fn validator_addr_3(&self) -> String {
self.validator_addr_3.clone()
}
#[inline(always)]
fn delegator_addr_1(&self) -> Addr {
self.delegator_addr_1.clone()
}
#[inline(always)]
fn delegator_addr_2(&self) -> Addr {
self.delegator_addr_2.clone()
}
#[inline(always)]
fn user_addr_1(&self) -> Addr {
self.user_addr_1.clone()
}
}
fn execute_stake(env: &mut TestEnv, sender: Addr, msg: StakingMsg) -> StdResult<AppResponse> {
env.router.staking.execute(
&env.api,
&mut env.storage,
&env.router,
&env.block,
sender,
msg,
)
}
fn query_stake<T: DeserializeOwned>(env: &TestEnv, msg: StakingQuery) -> StdResult<T> {
from_json(env.router.staking.query(
&env.api,
&env.storage,
&env.router.querier(&env.api, &env.storage, &env.block),
&env.block,
msg,
)?)
}
fn execute_distr(
env: &mut TestEnv,
sender: Addr,
msg: DistributionMsg,
) -> StdResult<AppResponse> {
env.router.distribution.execute(
&env.api,
&mut env.storage,
&env.router,
&env.block,
sender,
msg,
)
}
fn query_bank<T: DeserializeOwned>(env: &TestEnv, msg: BankQuery) -> StdResult<T> {
from_json(env.router.bank.query(
&env.api,
&env.storage,
&env.router.querier(&env.api, &env.storage, &env.block),
&env.block,
msg,
)?)
}
fn init_balance(env: &mut TestEnv, address: &Addr, amount: u128) {
init_balance_denom(env, address, amount, BONDED_DENOM);
}
fn init_balance_denom(env: &mut TestEnv, address: &Addr, amount: u128, denom: &str) {
env.router
.bank
.init_balance(&mut env.storage, address, coins(amount, denom))
.unwrap();
}
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: BONDED_DENOM.to_string(),
},
)
.unwrap();
assert_eq!(balance.amount.amount, Uint256::new(amount));
}
}
#[test]
fn add_get_validators() {
let mut env = TestEnv::new(vp(10, 100, 1), vp(0, 20, 1));
let validator_addr_3 = env.validator_addr_3();
let validator = Validator::new(
validator_addr_3.to_string(),
Decimal::percent(1),
Decimal::percent(10),
Decimal::percent(1),
);
env.router
.staking
.add_validator(&env.api, &mut env.storage, &env.block, validator.clone())
.unwrap();
let val = StakeKeeper::get_validator(&StakingStorage::new(&env.storage), &validator_addr_3)
.unwrap()
.unwrap();
assert_eq!(val, validator);
let validator_fake = Validator::new(
validator_addr_3.to_string(),
Decimal::percent(2),
Decimal::percent(20),
Decimal::percent(2),
);
env.router
.staking
.add_validator(&env.api, &mut env.storage, &env.block, validator_fake)
.unwrap_err();
let val = StakeKeeper::get_validator(&StakingStorage::new(&env.storage), &validator_addr_3)
.unwrap()
.unwrap();
assert_eq!(val, validator);
}
#[test]
fn validator_slashing() {
let mut env = TestEnv::new(vp(10, 20, 1), vp(10, 20, 1));
let validator_addr_1 = env.validator_addr_1();
let delegator_addr_1 = env.delegator_addr_1();
let mut staking_storage_mut = StakingStorageMut::new(&mut env.storage);
env.router
.staking
.add_stake(
&env.api,
&mut staking_storage_mut,
&env.block,
&delegator_addr_1,
&validator_addr_1,
coin(100, BONDED_DENOM),
)
.unwrap();
env.router
.staking
.sudo(
&env.api,
&mut env.storage,
&env.router,
&env.block,
StakingSudo::Slash {
validator: validator_addr_1.to_string(),
percentage: Decimal256::percent(50),
},
)
.unwrap();
let stake_left = env
.router
.staking
.get_stake(
&StakingStorage::new(&env.storage),
&delegator_addr_1,
&validator_addr_1,
)
.unwrap()
.unwrap();
assert_eq!(Uint256::new(50), stake_left.amount);
env.router
.staking
.sudo(
&env.api,
&mut env.storage,
&env.router,
&env.block,
StakingSudo::Slash {
validator: validator_addr_1.to_string(),
percentage: Decimal256::percent(100),
},
)
.unwrap();
let stake_left = env
.router
.staking
.get_stake(
&StakingStorage::new(&env.storage),
&delegator_addr_1,
&validator_addr_1,
)
.unwrap();
assert_eq!(None, stake_left);
}
#[test]
fn rewards_work_for_single_delegator() {
let mut env = TestEnv::new(vp(10, 20, 1), vp(10, 20, 1));
let validator_addr_1 = env.validator_addr_1();
let delegator_addr_1 = env.delegator_addr_1();
let mut staking_storage_mut = StakingStorageMut::new(&mut env.storage);
env.router
.staking
.add_stake(
&env.api,
&mut staking_storage_mut,
&env.block,
&delegator_addr_1,
&validator_addr_1,
coin(200, BONDED_DENOM),
)
.unwrap();
env.block.time = env.block.time.plus_seconds(YEAR / 2);
let rewards = env
.router
.staking
.get_rewards(
&env.storage,
&env.block,
&delegator_addr_1,
&validator_addr_1,
)
.unwrap()
.unwrap();
assert_eq!(Uint256::new(9), rewards.amount);
env.router
.distribution
.execute(
&env.api,
&mut env.storage,
&env.router,
&env.block,
delegator_addr_1.clone(),
DistributionMsg::WithdrawDelegatorReward {
validator: validator_addr_1.to_string(),
},
)
.unwrap();
let rewards = env
.router
.staking
.get_rewards(
&env.storage,
&env.block,
&delegator_addr_1,
&validator_addr_1,
)
.unwrap()
.unwrap();
assert_eq!(Uint256::zero(), rewards.amount);
env.block.time = env.block.time.plus_seconds(YEAR / 2);
let rewards = env
.router
.staking
.get_rewards(
&env.storage,
&env.block,
&delegator_addr_1,
&validator_addr_1,
)
.unwrap()
.unwrap();
assert_eq!(Uint256::new(9), rewards.amount);
}
#[test]
fn rewards_work_for_multiple_delegators() {
let mut env = TestEnv::new(vp(10, 100, 1), vp(10, 100, 1));
let validator_addr_1 = env.validator_addr_1();
let delegator_addr_1 = env.delegator_addr_1();
let delegator_addr_2 = env.delegator_addr_2();
env.router
.staking
.add_stake(
&env.api,
&mut StakingStorageMut::new(&mut env.storage),
&env.block,
&delegator_addr_1,
&validator_addr_1,
coin(100, BONDED_DENOM),
)
.unwrap();
env.router
.staking
.add_stake(
&env.api,
&mut StakingStorageMut::new(&mut env.storage),
&env.block,
&delegator_addr_2,
&validator_addr_1,
coin(200, BONDED_DENOM),
)
.unwrap();
env.block.time = env.block.time.plus_seconds(YEAR);
let rewards = env
.router
.staking
.get_rewards(
&env.storage,
&env.block,
&delegator_addr_1,
&validator_addr_1,
)
.unwrap()
.unwrap();
assert_eq!(Uint256::new(9), rewards.amount);
let rewards = env
.router
.staking
.get_rewards(
&env.storage,
&env.block,
&delegator_addr_2,
&validator_addr_1,
)
.unwrap()
.unwrap();
assert_eq!(Uint256::new(18), rewards.amount);
env.router
.staking
.add_stake(
&env.api,
&mut StakingStorageMut::new(&mut env.storage),
&env.block,
&delegator_addr_1,
&validator_addr_1,
coin(100, BONDED_DENOM),
)
.unwrap();
env.block.time = env.block.time.plus_seconds(YEAR);
let rewards = env
.router
.staking
.get_rewards(
&env.storage,
&env.block,
&delegator_addr_1,
&validator_addr_1,
)
.unwrap()
.unwrap();
assert_eq!(Uint256::new(27), rewards.amount);
let rewards = env
.router
.staking
.get_rewards(
&env.storage,
&env.block,
&delegator_addr_2,
&validator_addr_1,
)
.unwrap()
.unwrap();
assert_eq!(Uint256::new(36), rewards.amount);
env.router
.staking
.remove_stake(
&env.api,
&mut StakingStorageMut::new(&mut env.storage),
&env.block,
&delegator_addr_2,
&validator_addr_1,
coin(100, BONDED_DENOM),
)
.unwrap();
env.router
.distribution
.execute(
&env.api,
&mut env.storage,
&env.router,
&env.block,
delegator_addr_1.clone(),
DistributionMsg::WithdrawDelegatorReward {
validator: validator_addr_1.to_string(),
},
)
.unwrap();
let balance: BalanceResponse = from_json(
env.router
.bank
.query(
&env.api,
&env.storage,
&env.router.querier(&env.api, &env.storage, &env.block),
&env.block,
BankQuery::Balance {
address: delegator_addr_1.to_string(),
denom: BONDED_DENOM.to_string(),
},
)
.unwrap(),
)
.unwrap();
assert_eq!(Uint256::new(27), balance.amount.amount);
let rewards = env
.router
.staking
.get_rewards(
&env.storage,
&env.block,
&delegator_addr_1,
&validator_addr_1,
)
.unwrap()
.unwrap();
assert_eq!(Uint256::zero(), rewards.amount);
env.block.time = env.block.time.plus_seconds(YEAR);
let rewards = env
.router
.staking
.get_rewards(
&env.storage,
&env.block,
&delegator_addr_1,
&validator_addr_1,
)
.unwrap()
.unwrap();
assert_eq!(Uint256::new(18), rewards.amount);
let rewards = env
.router
.staking
.get_rewards(
&env.storage,
&env.block,
&delegator_addr_2,
&validator_addr_1,
)
.unwrap()
.unwrap();
assert_eq!(Uint256::new(45), rewards.amount);
}
#[test]
fn execute() {
let mut env = TestEnv::new(vp(10, 100, 1), vp(0, 20, 1));
let validator_addr_1 = env.validator_addr_1();
let validator_addr_2 = env.validator_addr_2();
let delegator_addr_1 = env.delegator_addr_2();
let reward_receiver_addr = env.user_addr_1();
init_balance(&mut env, &delegator_addr_1, 1000);
execute_stake(
&mut env,
delegator_addr_1.clone(),
StakingMsg::Delegate {
validator: validator_addr_1.clone(),
amount: coin(100, BONDED_DENOM),
},
)
.unwrap();
assert_balances(&env, vec![(delegator_addr_1.clone(), 900)]);
env.block.time = env.block.time.plus_seconds(YEAR);
execute_distr(
&mut env,
delegator_addr_1.clone(),
DistributionMsg::SetWithdrawAddress {
address: reward_receiver_addr.to_string(),
},
)
.unwrap();
execute_distr(
&mut env,
delegator_addr_1.clone(),
DistributionMsg::WithdrawDelegatorReward {
validator: validator_addr_1.clone(),
},
)
.unwrap();
assert_balances(
&env,
vec![(reward_receiver_addr, 100 / 10 * 9 / 10)],
);
execute_stake(
&mut env,
delegator_addr_1.clone(),
StakingMsg::Redelegate {
src_validator: validator_addr_1,
dst_validator: validator_addr_2.clone(),
amount: coin(100, BONDED_DENOM),
},
)
.unwrap();
assert_balances(&env, vec![(delegator_addr_1.clone(), 900)]);
let delegations: AllDelegationsResponse = query_stake(
&env,
StakingQuery::AllDelegations {
delegator: delegator_addr_1.to_string(),
},
)
.unwrap();
assert_eq!(
delegations.delegations,
[Delegation::new(
delegator_addr_1.clone(),
validator_addr_2.clone(),
coin(100, BONDED_DENOM),
)]
);
execute_stake(
&mut env,
delegator_addr_1.clone(),
StakingMsg::Undelegate {
validator: validator_addr_2,
amount: coin(100, BONDED_DENOM),
},
)
.unwrap();
env.block.time = env.block.time.plus_seconds(60);
env.router
.staking
.process_queue(&env.api, &mut env.storage, &env.router, &env.block)
.unwrap();
assert_balances(&env, vec![(delegator_addr_1.clone(), 1000)]);
}
#[test]
fn can_set_withdraw_address() {
let mut env = TestEnv::new(vp(10, 100, 1), vp(10, 100, 1));
let validator_addr_1 = env.validator_addr_1();
let delegator_addr_1 = env.delegator_addr_1();
let reward_receiver_addr = env.user_addr_1();
init_balance(&mut env, &delegator_addr_1, 100);
execute_stake(
&mut env,
delegator_addr_1.clone(),
StakingMsg::Delegate {
validator: validator_addr_1.clone(),
amount: coin(100, BONDED_DENOM),
},
)
.unwrap();
execute_distr(
&mut env,
delegator_addr_1.clone(),
DistributionMsg::SetWithdrawAddress {
address: reward_receiver_addr.to_string(),
},
)
.unwrap();
env.block.time = env.block.time.plus_seconds(YEAR);
execute_distr(
&mut env,
delegator_addr_1.clone(),
DistributionMsg::WithdrawDelegatorReward {
validator: validator_addr_1.clone(),
},
)
.unwrap();
execute_distr(
&mut env,
delegator_addr_1.clone(),
DistributionMsg::SetWithdrawAddress {
address: delegator_addr_1.to_string(),
},
)
.unwrap();
env.block.time = env.block.time.plus_seconds(YEAR);
execute_distr(
&mut env,
delegator_addr_1.clone(),
DistributionMsg::WithdrawDelegatorReward {
validator: validator_addr_1,
},
)
.unwrap();
let rewards_yr = 100 / 10 * 9 / 10;
assert_balances(
&env,
vec![
(reward_receiver_addr, rewards_yr),
(delegator_addr_1, rewards_yr),
],
);
}
#[test]
fn cannot_steal() {
let mut env = TestEnv::new(vp(10, 100, 1), vp(0, 20, 1));
let validator_addr_1 = env.validator_addr_1();
let validator_addr_2 = env.validator_addr_2();
let delegator_addr_1 = env.delegator_addr_1();
init_balance(&mut env, &delegator_addr_1, 100);
execute_stake(
&mut env,
delegator_addr_1.clone(),
StakingMsg::Delegate {
validator: validator_addr_1.clone(),
amount: coin(100, BONDED_DENOM),
},
)
.unwrap();
let error_result = execute_stake(
&mut env,
delegator_addr_1.clone(),
StakingMsg::Undelegate {
validator: validator_addr_1.clone(),
amount: coin(200, BONDED_DENOM),
},
)
.unwrap_err();
assert_eq!(
"kind: Other, error: invalid shares amount",
error_result.to_string()
);
let error_result = execute_stake(
&mut env,
delegator_addr_1.clone(),
StakingMsg::Redelegate {
src_validator: validator_addr_1,
dst_validator: validator_addr_2.clone(),
amount: coin(200, BONDED_DENOM),
},
)
.unwrap_err();
assert_eq!(
"kind: Other, error: invalid shares amount",
error_result.to_string()
);
let error_result = execute_stake(
&mut env,
delegator_addr_1.clone(),
StakingMsg::Undelegate {
validator: validator_addr_2,
amount: coin(100, BONDED_DENOM),
},
)
.unwrap_err();
assert_eq!(
"kind: Other, error: no delegation for (address, validator) tuple",
error_result.to_string()
);
}
#[test]
fn denom_validation() {
let mut env = TestEnv::new(vp(10, 100, 1), vp(10, 100, 1));
let validator_addr_1 = env.validator_addr_1();
let delegator_addr_1 = env.delegator_addr_1();
init_balance_denom(&mut env, &delegator_addr_1, 100, "FAKE");
let error_result = execute_stake(
&mut env,
delegator_addr_1.clone(),
StakingMsg::Delegate {
validator: validator_addr_1,
amount: coin(100, "FAKE"),
},
)
.unwrap_err();
assert_eq!(
"kind: Other, error: cannot delegate coins of denominator FAKE, only of TOKEN",
error_result.to_string()
);
}
#[test]
fn cannot_slash_nonexistent() {
let mut env = TestEnv::new(vp(10, 100, 1), vp(10, 100, 1));
let validator_addr_3 = env.validator_addr_3();
let delegator_addr_1 = env.delegator_addr_1();
init_balance_denom(&mut env, &delegator_addr_1, 100, "FAKE");
let error_result = env
.router
.staking
.sudo(
&env.api,
&mut env.storage,
&env.router,
&env.block,
StakingSudo::Slash {
validator: validator_addr_3,
percentage: Decimal256::percent(50),
},
)
.unwrap_err();
assert_eq!(
error_result.to_string(),
"kind: Other, error: validator does not exist"
);
}
#[test]
fn non_existent_validator() {
let mut env = TestEnv::new(vp(10, 100, 1), vp(10, 100, 1));
let validator_addr_3 = env.validator_addr_3();
let delegator_addr_1 = env.delegator_addr_1();
init_balance(&mut env, &delegator_addr_1, 100);
let error_result = execute_stake(
&mut env,
delegator_addr_1.clone(),
StakingMsg::Delegate {
validator: validator_addr_3.clone(),
amount: coin(100, BONDED_DENOM),
},
)
.unwrap_err();
assert_eq!(
"kind: Other, error: validator does not exist",
error_result.to_string()
);
let error_result = execute_stake(
&mut env,
delegator_addr_1.clone(),
StakingMsg::Undelegate {
validator: validator_addr_3,
amount: coin(100, BONDED_DENOM),
},
)
.unwrap_err();
assert_eq!(
"kind: Other, error: validator does not exist",
error_result.to_string()
);
}
#[test]
fn zero_staking_forbidden() {
let mut env = TestEnv::new(vp(10, 100, 1), vp(10, 100, 1));
let validator_addr_1 = env.validator_addr_1();
let delegator_addr_1 = env.delegator_addr_1();
let error_result = execute_stake(
&mut env,
delegator_addr_1.clone(),
StakingMsg::Delegate {
validator: validator_addr_1.clone(),
amount: coin(0, BONDED_DENOM),
},
)
.unwrap_err();
assert_eq!(
"kind: Other, error: invalid delegation amount",
error_result.to_string()
);
let error_result = execute_stake(
&mut env,
delegator_addr_1,
StakingMsg::Undelegate {
validator: validator_addr_1,
amount: coin(0, BONDED_DENOM),
},
)
.unwrap_err();
assert_eq!(
"kind: Other, error: invalid shares amount",
error_result.to_string()
);
}
#[test]
fn query_staking() {
let mut env = TestEnv::new(vp(10, 100, 1), vp(0, 1, 1));
let validator_addr_1 = env.validator_addr_1();
let validator_addr_2 = env.validator_addr_2();
let delegator_addr_1 = env.delegator_addr_1();
let delegator_addr_2 = env.delegator_addr_2();
let user_addr_1 = env.user_addr_1();
init_balance(&mut env, &delegator_addr_1, 260);
init_balance(&mut env, &delegator_addr_2, 150);
let valoper1: ValidatorResponse = query_stake(
&env,
StakingQuery::Validator {
address: validator_addr_1.to_string(),
},
)
.unwrap();
let valoper2: ValidatorResponse = query_stake(
&env,
StakingQuery::Validator {
address: validator_addr_2.to_string(),
},
)
.unwrap();
let validators: AllValidatorsResponse =
query_stake(&env, StakingQuery::AllValidators {}).unwrap();
assert_eq!(
validators.validators,
[
valoper1.validator.unwrap().into(),
valoper2.validator.unwrap().into()
]
);
let response = query_stake::<ValidatorResponse>(
&env,
StakingQuery::Validator {
address: user_addr_1.to_string(),
},
)
.unwrap();
assert_eq!(response.validator, None);
let response: BondedDenomResponse =
query_stake(&env, StakingQuery::BondedDenom {}).unwrap();
assert_eq!(response.denom, BONDED_DENOM);
execute_stake(
&mut env,
delegator_addr_1.clone(),
StakingMsg::Delegate {
validator: validator_addr_1.to_string(),
amount: coin(100, BONDED_DENOM),
},
)
.unwrap();
execute_stake(
&mut env,
delegator_addr_1.clone(),
StakingMsg::Delegate {
validator: validator_addr_2.to_string(),
amount: coin(160, BONDED_DENOM),
},
)
.unwrap();
execute_stake(
&mut env,
delegator_addr_2.clone(),
StakingMsg::Delegate {
validator: validator_addr_1.to_string(),
amount: coin(150, BONDED_DENOM),
},
)
.unwrap();
execute_stake(
&mut env,
delegator_addr_1.clone(),
StakingMsg::Undelegate {
validator: validator_addr_1.to_string(),
amount: coin(50, BONDED_DENOM),
},
)
.unwrap();
execute_stake(
&mut env,
delegator_addr_2.clone(),
StakingMsg::Undelegate {
validator: validator_addr_1.to_string(),
amount: coin(50, BONDED_DENOM),
},
)
.unwrap();
let response1: AllDelegationsResponse = query_stake(
&env,
StakingQuery::AllDelegations {
delegator: delegator_addr_1.to_string(),
},
)
.unwrap();
assert_eq!(
response1.delegations,
vec![
Delegation::new(
delegator_addr_1.clone(),
validator_addr_1.to_string(),
coin(50, BONDED_DENOM),
),
Delegation::new(
delegator_addr_1.clone(),
validator_addr_2,
coin(160, BONDED_DENOM),
),
]
);
let response2: DelegationResponse = query_stake(
&env,
StakingQuery::Delegation {
delegator: delegator_addr_2.to_string(),
validator: validator_addr_1.clone(),
},
)
.unwrap();
assert_eq!(
response2.delegation.unwrap(),
FullDelegation::new(
delegator_addr_2.clone(),
validator_addr_1,
coin(100, BONDED_DENOM),
coin(100, BONDED_DENOM),
vec![],
),
);
}
#[test]
fn delegation_queries_unbonding() {
let mut env = TestEnv::new(vp(10, 100, 1), vp(10, 100, 1));
let validator_addr_1 = env.validator_addr_1();
let delegator_addr_1 = env.delegator_addr_1();
let delegator_addr_2 = env.delegator_addr_2();
init_balance(&mut env, &delegator_addr_1, 100);
init_balance(&mut env, &delegator_addr_2, 150);
execute_stake(
&mut env,
delegator_addr_1.clone(),
StakingMsg::Delegate {
validator: validator_addr_1.to_string(),
amount: coin(100, BONDED_DENOM),
},
)
.unwrap();
execute_stake(
&mut env,
delegator_addr_2.clone(),
StakingMsg::Delegate {
validator: validator_addr_1.to_string(),
amount: coin(150, BONDED_DENOM),
},
)
.unwrap();
execute_stake(
&mut env,
delegator_addr_1.clone(),
StakingMsg::Undelegate {
validator: validator_addr_1.to_string(),
amount: coin(50, BONDED_DENOM),
},
)
.unwrap();
execute_stake(
&mut env,
delegator_addr_2.clone(),
StakingMsg::Undelegate {
validator: validator_addr_1.to_string(),
amount: coin(150, BONDED_DENOM),
},
)
.unwrap();
let response1: AllDelegationsResponse = query_stake(
&env,
StakingQuery::AllDelegations {
delegator: delegator_addr_1.to_string(),
},
)
.unwrap();
assert_eq!(
response1.delegations,
vec![Delegation::new(
delegator_addr_1.clone(),
validator_addr_1.to_string(),
coin(50, BONDED_DENOM),
)]
);
let response2: DelegationResponse = query_stake(
&env,
StakingQuery::Delegation {
delegator: delegator_addr_2.to_string(),
validator: validator_addr_1.to_string(),
},
)
.unwrap();
assert_eq!(response2.delegation, None);
execute_stake(
&mut env,
delegator_addr_1.clone(),
StakingMsg::Undelegate {
validator: validator_addr_1.to_string(),
amount: coin(25, BONDED_DENOM),
},
)
.unwrap();
env.block.time = env.block.time.plus_seconds(10);
execute_stake(
&mut env,
delegator_addr_1.clone(),
StakingMsg::Undelegate {
validator: validator_addr_1.to_string(),
amount: coin(25, BONDED_DENOM),
},
)
.unwrap();
let response1: DelegationResponse = query_stake(
&env,
StakingQuery::Delegation {
delegator: delegator_addr_1.to_string(),
validator: validator_addr_1,
},
)
.unwrap();
let response2: AllDelegationsResponse = query_stake(
&env,
StakingQuery::AllDelegations {
delegator: delegator_addr_1.to_string(),
},
)
.unwrap();
assert_eq!(
response1.delegation, None,
"delegator1 should have no delegations left"
);
assert_eq!(response2.delegations, vec![]);
}
#[test]
fn partial_unbonding_reduces_stake() {
let mut env = TestEnv::new(vp(10, 100, 1), vp(10, 100, 1));
let validator_addr_1 = env.validator_addr_1();
let delegator_addr_1 = env.delegator_addr_1();
init_balance(&mut env, &delegator_addr_1, 100);
execute_stake(
&mut env,
delegator_addr_1.clone(),
StakingMsg::Delegate {
validator: validator_addr_1.to_string(),
amount: coin(100, BONDED_DENOM),
},
)
.unwrap();
execute_stake(
&mut env,
delegator_addr_1.clone(),
StakingMsg::Undelegate {
validator: validator_addr_1.to_string(),
amount: coin(50, BONDED_DENOM),
},
)
.unwrap();
env.block.time = env.block.time.plus_seconds(10);
execute_stake(
&mut env,
delegator_addr_1.clone(),
StakingMsg::Undelegate {
validator: validator_addr_1.to_string(),
amount: coin(30, BONDED_DENOM),
},
)
.unwrap();
env.block.time = env.block.time.plus_seconds(10);
execute_stake(
&mut env,
delegator_addr_1.clone(),
StakingMsg::Undelegate {
validator: validator_addr_1.to_string(),
amount: coin(20, BONDED_DENOM),
},
)
.unwrap();
env.block.time = env.block.time.plus_seconds(40);
env.router
.staking
.process_queue(&env.api, &mut env.storage, &env.router, &env.block)
.unwrap();
let response1: DelegationResponse = query_stake(
&env,
StakingQuery::Delegation {
delegator: delegator_addr_1.to_string(),
validator: validator_addr_1.to_string(),
},
)
.unwrap();
let response2: AllDelegationsResponse = query_stake(
&env,
StakingQuery::AllDelegations {
delegator: delegator_addr_1.to_string(),
},
)
.unwrap();
assert_eq!(response1.delegation, None);
assert_eq!(response2.delegations, vec![]);
env.block.time = env.block.time.plus_seconds(20);
env.router
.staking
.process_queue(&env.api, &mut env.storage, &env.router, &env.block)
.unwrap();
let response1: DelegationResponse = query_stake(
&env,
StakingQuery::Delegation {
delegator: delegator_addr_1.to_string(),
validator: validator_addr_1,
},
)
.unwrap();
let response2: AllDelegationsResponse = query_stake(
&env,
StakingQuery::AllDelegations {
delegator: delegator_addr_1.to_string(),
},
)
.unwrap();
assert_eq!(
response1.delegation, None,
"delegator should have nothing left"
);
assert!(response2.delegations.is_empty());
}
#[test]
fn delegations_slashed() {
let mut env = TestEnv::new(vp(10, 100, 1), vp(10, 100, 1));
let validator_addr_1 = env.validator_addr_1();
let delegator_addr_1 = env.delegator_addr_1();
init_balance(&mut env, &delegator_addr_1, 333);
execute_stake(
&mut env,
delegator_addr_1.clone(),
StakingMsg::Delegate {
validator: validator_addr_1.to_string(),
amount: coin(333, BONDED_DENOM),
},
)
.unwrap();
execute_stake(
&mut env,
delegator_addr_1.clone(),
StakingMsg::Undelegate {
validator: validator_addr_1.to_string(),
amount: coin(111, BONDED_DENOM),
},
)
.unwrap();
env.router
.staking
.sudo(
&env.api,
&mut env.storage,
&env.router,
&env.block,
StakingSudo::Slash {
validator: validator_addr_1.to_string(),
percentage: Decimal256::percent(50),
},
)
.unwrap();
let response1: AllDelegationsResponse = query_stake(
&env,
StakingQuery::AllDelegations {
delegator: delegator_addr_1.to_string(),
},
)
.unwrap();
assert_eq!(
response1.delegations[0],
Delegation::new(
delegator_addr_1.clone(),
validator_addr_1,
coin(111, BONDED_DENOM),
)
);
env.block.time = env.block.time.plus_seconds(60);
env.router
.staking
.process_queue(&env.api, &mut env.storage, &env.router, &env.block)
.unwrap();
let balance =
QuerierWrapper::<Empty>::new(&env.router.querier(&env.api, &env.storage, &env.block))
.query_balance(delegator_addr_1, BONDED_DENOM)
.unwrap();
assert_eq!(Uint256::new(55), balance.amount);
}
#[test]
fn rewards_initial_wait() {
let mut env = TestEnv::new(vp(0, 100, 1), vp(0, 100, 1));
let validator_addr_1 = env.validator_addr_1();
let delegator_addr_1 = env.delegator_addr_1();
init_balance(&mut env, &delegator_addr_1, 100);
env.block.time = env.block.time.plus_seconds(YEAR);
execute_stake(
&mut env,
delegator_addr_1.clone(),
StakingMsg::Delegate {
validator: validator_addr_1.to_string(),
amount: coin(100, BONDED_DENOM),
},
)
.unwrap();
env.block.time = env.block.time.plus_seconds(YEAR);
let response: DelegationResponse = query_stake(
&env,
StakingQuery::Delegation {
delegator: delegator_addr_1.to_string(),
validator: validator_addr_1,
},
)
.unwrap();
assert_eq!(
response.delegation.unwrap().accumulated_rewards,
vec![coin(10, BONDED_DENOM)] );
}
}