use cosmwasm_std::{
attr, coin, coins, to_binary, BankMsg, Binary, CanonicalAddr, Coin, CosmosMsg, Deps, DepsMut,
Env, HandleResponse, HumanAddr, InitResponse, MessageInfo, Order, StdResult, Storage, Uint128,
};
use cw0::maybe_canonical;
use cw2::set_contract_version;
use cw4::{
Member, MemberChangedHookMsg, MemberDiff, MemberListResponse, MemberResponse,
TotalWeightResponse,
};
use cw_storage_plus::Bound;
use crate::error::ContractError;
use crate::msg::{HandleMsg, InitMsg, QueryMsg, StakedResponse};
use crate::state::{Config, ADMIN, CLAIMS, CONFIG, HOOKS, MEMBERS, STAKE, TOTAL};
const CONTRACT_NAME: &str = "crates.io:cw4-stake";
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
pub fn init(
mut deps: DepsMut,
_env: Env,
_info: MessageInfo,
msg: InitMsg,
) -> Result<InitResponse, ContractError> {
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
ADMIN.set(deps.branch(), msg.admin)?;
let min_bond = match msg.min_bond {
Uint128(0) => Uint128(1),
v => v,
};
let config = Config {
denom: msg.denom,
tokens_per_weight: msg.tokens_per_weight,
min_bond,
unbonding_period: msg.unbonding_period,
};
CONFIG.save(deps.storage, &config)?;
TOTAL.save(deps.storage, &0)?;
Ok(InitResponse::default())
}
pub fn handle(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: HandleMsg,
) -> Result<HandleResponse, ContractError> {
match msg {
HandleMsg::UpdateAdmin { admin } => Ok(ADMIN.handle_update_admin(deps, info, admin)?),
HandleMsg::AddHook { addr } => Ok(HOOKS.handle_add_hook(&ADMIN, deps, info, addr)?),
HandleMsg::RemoveHook { addr } => Ok(HOOKS.handle_remove_hook(&ADMIN, deps, info, addr)?),
HandleMsg::Bond {} => handle_bond(deps, env, info),
HandleMsg::Unbond { tokens: amount } => handle_unbond(deps, env, info, amount),
HandleMsg::Claim {} => handle_claim(deps, env, info),
}
}
pub fn handle_bond(
deps: DepsMut,
env: Env,
info: MessageInfo,
) -> Result<HandleResponse, ContractError> {
let cfg = CONFIG.load(deps.storage)?;
let sent = match info.sent_funds.len() {
0 => Err(ContractError::NoFunds {}),
1 => {
if info.sent_funds[0].denom == cfg.denom {
Ok(info.sent_funds[0].amount)
} else {
Err(ContractError::MissingDenom(cfg.denom.clone()))
}
}
_ => Err(ContractError::ExtraDenoms(cfg.denom.clone())),
}?;
if sent.is_zero() {
return Err(ContractError::NoFunds {});
}
let sender_raw = deps.api.canonical_address(&info.sender)?;
let new_stake = STAKE.update(deps.storage, &sender_raw, |stake| -> StdResult<_> {
Ok(stake.unwrap_or_default() + sent)
})?;
let messages = update_membership(
deps.storage,
info.sender.clone(),
&sender_raw,
new_stake,
&cfg,
env.block.height,
)?;
let attributes = vec![
attr("action", "bond"),
attr("amount", sent),
attr("sender", info.sender),
];
Ok(HandleResponse {
messages,
attributes,
data: None,
})
}
pub fn handle_unbond(
deps: DepsMut,
env: Env,
info: MessageInfo,
amount: Uint128,
) -> Result<HandleResponse, ContractError> {
let sender_raw = deps.api.canonical_address(&info.sender)?;
let new_stake = STAKE.update(deps.storage, &sender_raw, |stake| -> StdResult<_> {
stake.unwrap_or_default() - amount
})?;
let cfg = CONFIG.load(deps.storage)?;
CLAIMS.create_claim(
deps.storage,
&sender_raw,
amount,
cfg.unbonding_period.after(&env.block),
)?;
let messages = update_membership(
deps.storage,
info.sender.clone(),
&sender_raw,
new_stake,
&cfg,
env.block.height,
)?;
let attributes = vec![
attr("action", "unbond"),
attr("amount", amount),
attr("sender", info.sender),
];
Ok(HandleResponse {
messages,
attributes,
data: None,
})
}
fn update_membership(
storage: &mut dyn Storage,
sender: HumanAddr,
sender_raw: &CanonicalAddr,
new_stake: Uint128,
cfg: &Config,
height: u64,
) -> StdResult<Vec<CosmosMsg>> {
let new = calc_weight(new_stake, cfg);
let old = MEMBERS.may_load(storage, sender_raw)?;
if new == old {
return Ok(vec![]);
}
match new.as_ref() {
Some(w) => MEMBERS.save(storage, sender_raw, w, height),
None => MEMBERS.remove(storage, sender_raw, height),
}?;
TOTAL.update(storage, |total| -> StdResult<_> {
Ok(total + new.unwrap_or_default() - old.unwrap_or_default())
})?;
let diff = MemberDiff::new(sender, old, new);
HOOKS.prepare_hooks(storage, |h| {
MemberChangedHookMsg::one(diff.clone()).into_cosmos_msg(h)
})
}
fn calc_weight(stake: Uint128, cfg: &Config) -> Option<u64> {
if stake < cfg.min_bond {
None
} else {
let w = stake.u128() / (cfg.tokens_per_weight.u128());
Some(w as u64)
}
}
pub fn handle_claim(
deps: DepsMut,
env: Env,
info: MessageInfo,
) -> Result<HandleResponse, ContractError> {
let sender_raw = deps.api.canonical_address(&info.sender)?;
let release = CLAIMS.claim_tokens(deps.storage, &sender_raw, &env.block, None)?;
if release.is_zero() {
return Err(ContractError::NothingToClaim {});
}
let config = CONFIG.load(deps.storage)?;
let amount = coins(release.u128(), config.denom);
let amount_str = coins_to_string(&amount);
let messages = vec![BankMsg::Send {
from_address: env.contract.address,
to_address: info.sender.clone(),
amount,
}
.into()];
let attributes = vec![
attr("action", "claim"),
attr("tokens", amount_str),
attr("sender", info.sender),
];
Ok(HandleResponse {
messages,
attributes,
data: None,
})
}
fn coins_to_string(coins: &[Coin]) -> String {
let strings: Vec<_> = coins
.iter()
.map(|c| format!("{}{}", c.amount, c.denom))
.collect();
strings.join(",")
}
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::Member {
addr,
at_height: height,
} => to_binary(&query_member(deps, addr, height)?),
QueryMsg::ListMembers { start_after, limit } => {
to_binary(&list_members(deps, start_after, limit)?)
}
QueryMsg::TotalWeight {} => to_binary(&query_total_weight(deps)?),
QueryMsg::Claims { address } => to_binary(&CLAIMS.query_claims(deps, address)?),
QueryMsg::Staked { address } => to_binary(&query_staked(deps, address)?),
QueryMsg::Admin {} => to_binary(&ADMIN.query_admin(deps)?),
QueryMsg::Hooks {} => to_binary(&HOOKS.query_hooks(deps)?),
}
}
fn query_total_weight(deps: Deps) -> StdResult<TotalWeightResponse> {
let weight = TOTAL.load(deps.storage)?;
Ok(TotalWeightResponse { weight })
}
pub fn query_staked(deps: Deps, address: HumanAddr) -> StdResult<StakedResponse> {
let address_raw = deps.api.canonical_address(&address)?;
let stake = STAKE
.may_load(deps.storage, &address_raw)?
.unwrap_or_default();
let denom = CONFIG.load(deps.storage)?.denom;
Ok(StakedResponse {
stake: coin(stake.u128(), denom),
})
}
fn query_member(deps: Deps, addr: HumanAddr, height: Option<u64>) -> StdResult<MemberResponse> {
let raw = deps.api.canonical_address(&addr)?;
let weight = match height {
Some(h) => MEMBERS.may_load_at_height(deps.storage, &raw, h),
None => MEMBERS.may_load(deps.storage, &raw),
}?;
Ok(MemberResponse { weight })
}
const MAX_LIMIT: u32 = 30;
const DEFAULT_LIMIT: u32 = 10;
fn list_members(
deps: Deps,
start_after: Option<HumanAddr>,
limit: Option<u32>,
) -> StdResult<MemberListResponse> {
let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize;
let canon = maybe_canonical(deps.api, start_after)?;
let start = canon.map(Bound::exclusive);
let api = &deps.api;
let members: StdResult<Vec<_>> = MEMBERS
.range(deps.storage, start, None, Order::Ascending)
.take(limit)
.map(|item| {
let (key, weight) = item?;
Ok(Member {
addr: api.human_address(&CanonicalAddr::from(key))?,
weight,
})
})
.collect();
Ok(MemberListResponse { members: members? })
}
#[cfg(test)]
mod tests {
use super::*;
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
use cosmwasm_std::{from_slice, Api, StdError, Storage};
use cw0::Duration;
use cw4::{member_key, TOTAL_KEY};
use cw_controllers::{AdminError, Claim, HookError};
const INIT_ADMIN: &str = "juan";
const USER1: &str = "somebody";
const USER2: &str = "else";
const USER3: &str = "funny";
const DENOM: &str = "stake";
const TOKENS_PER_WEIGHT: Uint128 = Uint128(1_000);
const MIN_BOND: Uint128 = Uint128(5_000);
const UNBONDING_BLOCKS: u64 = 100;
fn default_init(deps: DepsMut) {
do_init(
deps,
TOKENS_PER_WEIGHT,
MIN_BOND,
Duration::Height(UNBONDING_BLOCKS),
)
}
fn do_init(
deps: DepsMut,
tokens_per_weight: Uint128,
min_bond: Uint128,
unbonding_period: Duration,
) {
let msg = InitMsg {
denom: DENOM.to_string(),
tokens_per_weight,
min_bond,
unbonding_period,
admin: Some(INIT_ADMIN.into()),
};
let info = mock_info("creator", &[]);
init(deps, mock_env(), info, msg).unwrap();
}
fn bond(mut deps: DepsMut, user1: u128, user2: u128, user3: u128, height_delta: u64) {
let mut env = mock_env();
env.block.height += height_delta;
for (addr, stake) in &[(USER1, user1), (USER2, user2), (USER3, user3)] {
if *stake != 0 {
let msg = HandleMsg::Bond {};
let info = mock_info(HumanAddr::from(*addr), &coins(*stake, DENOM));
handle(deps.branch(), env.clone(), info, msg).unwrap();
}
}
}
fn unbond(mut deps: DepsMut, user1: u128, user2: u128, user3: u128, height_delta: u64) {
let mut env = mock_env();
env.block.height += height_delta;
for (addr, stake) in &[(USER1, user1), (USER2, user2), (USER3, user3)] {
if *stake != 0 {
let msg = HandleMsg::Unbond {
tokens: Uint128(*stake),
};
let info = mock_info(HumanAddr::from(*addr), &[]);
handle(deps.branch(), env.clone(), info, msg).unwrap();
}
}
}
#[test]
fn proper_initialization() {
let mut deps = mock_dependencies(&[]);
default_init(deps.as_mut());
let res = ADMIN.query_admin(deps.as_ref()).unwrap();
assert_eq!(Some(HumanAddr::from(INIT_ADMIN)), res.admin);
let res = query_total_weight(deps.as_ref()).unwrap();
assert_eq!(0, res.weight);
}
fn get_member(deps: Deps, addr: HumanAddr, at_height: Option<u64>) -> Option<u64> {
let raw = query(deps, mock_env(), QueryMsg::Member { addr, at_height }).unwrap();
let res: MemberResponse = from_slice(&raw).unwrap();
res.weight
}
fn assert_users(
deps: Deps,
user1_weight: Option<u64>,
user2_weight: Option<u64>,
user3_weight: Option<u64>,
height: Option<u64>,
) {
let member1 = get_member(deps, USER1.into(), height);
assert_eq!(member1, user1_weight);
let member2 = get_member(deps, USER2.into(), height);
assert_eq!(member2, user2_weight);
let member3 = get_member(deps, USER3.into(), height);
assert_eq!(member3, user3_weight);
if height.is_none() {
let weights = vec![user1_weight, user2_weight, user3_weight];
let sum: u64 = weights.iter().map(|x| x.unwrap_or_default()).sum();
let count = weights.iter().filter(|x| x.is_some()).count();
let msg = QueryMsg::ListMembers {
start_after: None,
limit: None,
};
let raw = query(deps, mock_env(), msg).unwrap();
let members: MemberListResponse = from_slice(&raw).unwrap();
assert_eq!(count, members.members.len());
let raw = query(deps, mock_env(), QueryMsg::TotalWeight {}).unwrap();
let total: TotalWeightResponse = from_slice(&raw).unwrap();
assert_eq!(sum, total.weight); }
}
fn assert_stake(deps: Deps, user1_stake: u128, user2_stake: u128, user3_stake: u128) {
let stake1 = query_staked(deps, USER1.into()).unwrap();
assert_eq!(stake1.stake, coin(user1_stake, DENOM));
let stake2 = query_staked(deps, USER2.into()).unwrap();
assert_eq!(stake2.stake, coin(user2_stake, DENOM));
let stake3 = query_staked(deps, USER3.into()).unwrap();
assert_eq!(stake3.stake, coin(user3_stake, DENOM));
}
#[test]
fn bond_stake_adds_membership() {
let mut deps = mock_dependencies(&[]);
default_init(deps.as_mut());
let height = mock_env().block.height;
assert_users(deps.as_ref(), None, None, None, None);
bond(deps.as_mut(), 12_000, 7_500, 4_000, 1);
assert_stake(deps.as_ref(), 12_000, 7_500, 4_000);
assert_users(deps.as_ref(), Some(12), Some(7), None, None);
bond(deps.as_mut(), 0, 7_600, 1_200, 2);
assert_stake(deps.as_ref(), 12_000, 15_100, 5_200);
assert_users(deps.as_ref(), Some(12), Some(15), Some(5), None);
assert_users(deps.as_ref(), None, None, None, Some(height + 1)); assert_users(deps.as_ref(), Some(12), Some(7), None, Some(height + 2)); assert_users(deps.as_ref(), Some(12), Some(15), Some(5), Some(height + 3));
}
#[test]
fn unbond_stake_update_membership() {
let mut deps = mock_dependencies(&[]);
default_init(deps.as_mut());
let height = mock_env().block.height;
bond(deps.as_mut(), 12_000, 7_500, 4_000, 1);
unbond(deps.as_mut(), 4_500, 2_600, 1_111, 2);
assert_stake(deps.as_ref(), 7_500, 4_900, 2_889);
assert_users(deps.as_ref(), Some(7), None, None, None);
bond(deps.as_mut(), 600, 100, 2_222, 3);
assert_users(deps.as_ref(), Some(8), Some(5), Some(5), None);
assert_users(deps.as_ref(), None, None, None, Some(height + 1)); assert_users(deps.as_ref(), Some(12), Some(7), None, Some(height + 2)); assert_users(deps.as_ref(), Some(7), None, None, Some(height + 3)); assert_users(deps.as_ref(), Some(8), Some(5), Some(5), Some(height + 4));
let msg = HandleMsg::Unbond {
tokens: Uint128(5100),
};
let mut env = mock_env();
env.block.height += 5;
let info = mock_info(USER2, &[]);
let err = handle(deps.as_mut(), env, info, msg).unwrap_err();
match err {
ContractError::Std(StdError::Underflow {
minuend,
subtrahend,
}) => {
assert_eq!(minuend.as_str(), "5000");
assert_eq!(subtrahend.as_str(), "5100");
}
e => panic!("Unexpected error: {}", e),
}
}
#[test]
fn raw_queries_work() {
let mut deps = mock_dependencies(&[]);
default_init(deps.as_mut());
bond(deps.as_mut(), 11_000, 6_000, 0, 1);
let total_raw = deps.storage.get(TOTAL_KEY.as_bytes()).unwrap();
let total: u64 = from_slice(&total_raw).unwrap();
assert_eq!(17, total);
let member2_canon = deps.api.canonical_address(&USER2.into()).unwrap();
let member2_raw = deps.storage.get(&member_key(&member2_canon)).unwrap();
let member2: u64 = from_slice(&member2_raw).unwrap();
assert_eq!(6, member2);
let member3_canon = deps.api.canonical_address(&USER3.into()).unwrap();
let member3_raw = deps.storage.get(&member_key(&member3_canon));
assert_eq!(None, member3_raw);
}
fn get_claims<U: Into<HumanAddr>>(deps: Deps, addr: U) -> Vec<Claim> {
CLAIMS.query_claims(deps, addr.into()).unwrap().claims
}
#[test]
fn unbond_claim_workflow() {
let mut deps = mock_dependencies(&[]);
default_init(deps.as_mut());
bond(deps.as_mut(), 12_000, 7_500, 4_000, 1);
unbond(deps.as_mut(), 4_500, 2_600, 0, 2);
let mut env = mock_env();
env.block.height += 2;
let expires = Duration::Height(UNBONDING_BLOCKS).after(&env.block);
assert_eq!(
get_claims(deps.as_ref(), USER1),
vec![Claim::new(4_500, expires)]
);
assert_eq!(
get_claims(deps.as_ref(), USER2),
vec![Claim::new(2_600, expires)]
);
assert_eq!(get_claims(deps.as_ref(), USER3), vec![]);
let mut env2 = mock_env();
env2.block.height += 22;
unbond(deps.as_mut(), 0, 1_345, 1_500, 22);
let expires2 = Duration::Height(UNBONDING_BLOCKS).after(&env2.block);
assert_eq!(
get_claims(deps.as_ref(), USER1),
vec![Claim::new(4_500, expires)]
);
assert_eq!(
get_claims(deps.as_ref(), USER2),
vec![Claim::new(2_600, expires), Claim::new(1_345, expires2)]
);
assert_eq!(
get_claims(deps.as_ref(), USER3),
vec![Claim::new(1_500, expires2)]
);
let err = handle(
deps.as_mut(),
env2.clone(),
mock_info(USER1, &[]),
HandleMsg::Claim {},
)
.unwrap_err();
assert_eq!(err, ContractError::NothingToClaim {});
let mut env3 = mock_env();
env3.block.height += 2 + UNBONDING_BLOCKS;
let res = handle(
deps.as_mut(),
env3.clone(),
mock_info(USER1, &[]),
HandleMsg::Claim {},
)
.unwrap();
assert_eq!(
res.messages,
vec![BankMsg::Send {
from_address: env3.contract.address.clone(),
to_address: USER1.into(),
amount: coins(4_500, DENOM),
}
.into()]
);
let res = handle(
deps.as_mut(),
env3.clone(),
mock_info(USER2, &[]),
HandleMsg::Claim {},
)
.unwrap();
assert_eq!(
res.messages,
vec![BankMsg::Send {
from_address: env3.contract.address.clone(),
to_address: USER2.into(),
amount: coins(2_600, DENOM),
}
.into()]
);
let err = handle(
deps.as_mut(),
env3.clone(),
mock_info(USER3, &[]),
HandleMsg::Claim {},
)
.unwrap_err();
assert_eq!(err, ContractError::NothingToClaim {});
assert_eq!(get_claims(deps.as_ref(), USER1), vec![]);
assert_eq!(
get_claims(deps.as_ref(), USER2),
vec![Claim::new(1_345, expires2)]
);
assert_eq!(
get_claims(deps.as_ref(), USER3),
vec![Claim::new(1_500, expires2)]
);
unbond(deps.as_mut(), 0, 600, 0, 30 + UNBONDING_BLOCKS);
unbond(deps.as_mut(), 0, 1_005, 0, 50 + UNBONDING_BLOCKS);
let mut env4 = mock_env();
env4.block.height += 55 + UNBONDING_BLOCKS + UNBONDING_BLOCKS;
let res = handle(
deps.as_mut(),
env4.clone(),
mock_info(USER2, &[]),
HandleMsg::Claim {},
)
.unwrap();
assert_eq!(
res.messages,
vec![BankMsg::Send {
from_address: env4.contract.address.clone(),
to_address: USER2.into(),
amount: coins(2_950, DENOM),
}
.into()]
);
assert_eq!(get_claims(deps.as_ref(), USER2), vec![]);
}
#[test]
fn add_remove_hooks() {
let mut deps = mock_dependencies(&[]);
default_init(deps.as_mut());
let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap();
assert!(hooks.hooks.is_empty());
let contract1 = HumanAddr::from("hook1");
let contract2 = HumanAddr::from("hook2");
let add_msg = HandleMsg::AddHook {
addr: contract1.clone(),
};
let user_info = mock_info(USER1, &[]);
let err = handle(
deps.as_mut(),
mock_env(),
user_info.clone(),
add_msg.clone(),
)
.unwrap_err();
assert_eq!(err, HookError::Admin(AdminError::NotAdmin {}).into());
let admin_info = mock_info(INIT_ADMIN, &[]);
let _ = handle(
deps.as_mut(),
mock_env(),
admin_info.clone(),
add_msg.clone(),
)
.unwrap();
let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap();
assert_eq!(hooks.hooks, vec![contract1.clone()]);
let remove_msg = HandleMsg::RemoveHook {
addr: contract2.clone(),
};
let err = handle(
deps.as_mut(),
mock_env(),
admin_info.clone(),
remove_msg.clone(),
)
.unwrap_err();
assert_eq!(err, HookError::HookNotRegistered {}.into());
let add_msg2 = HandleMsg::AddHook {
addr: contract2.clone(),
};
let _ = handle(deps.as_mut(), mock_env(), admin_info.clone(), add_msg2).unwrap();
let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap();
assert_eq!(hooks.hooks, vec![contract1.clone(), contract2.clone()]);
let err = handle(
deps.as_mut(),
mock_env(),
admin_info.clone(),
add_msg.clone(),
)
.unwrap_err();
assert_eq!(err, HookError::HookAlreadyRegistered {}.into());
let remove_msg = HandleMsg::RemoveHook {
addr: contract1.clone(),
};
let err = handle(
deps.as_mut(),
mock_env(),
user_info.clone(),
remove_msg.clone(),
)
.unwrap_err();
assert_eq!(err, HookError::Admin(AdminError::NotAdmin {}).into());
let _ = handle(
deps.as_mut(),
mock_env(),
admin_info.clone(),
remove_msg.clone(),
)
.unwrap();
let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap();
assert_eq!(hooks.hooks, vec![contract2.clone()]);
}
#[test]
fn hooks_fire() {
let mut deps = mock_dependencies(&[]);
default_init(deps.as_mut());
let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap();
assert!(hooks.hooks.is_empty());
let contract1 = HumanAddr::from("hook1");
let contract2 = HumanAddr::from("hook2");
let admin_info = mock_info(INIT_ADMIN, &[]);
let add_msg = HandleMsg::AddHook {
addr: contract1.clone(),
};
let add_msg2 = HandleMsg::AddHook {
addr: contract2.clone(),
};
for msg in vec![add_msg, add_msg2] {
let _ = handle(deps.as_mut(), mock_env(), admin_info.clone(), msg).unwrap();
}
assert_users(deps.as_ref(), None, None, None, None);
let info = mock_info(USER1, &coins(13_800, DENOM));
let res = handle(deps.as_mut(), mock_env(), info, HandleMsg::Bond {}).unwrap();
assert_users(deps.as_ref(), Some(13), None, None, None);
assert_eq!(res.messages.len(), 2);
let diff = MemberDiff::new(USER1, None, Some(13));
let hook_msg = MemberChangedHookMsg::one(diff);
let msg1 = hook_msg.clone().into_cosmos_msg(contract1.clone()).unwrap();
let msg2 = hook_msg.into_cosmos_msg(contract2.clone()).unwrap();
assert_eq!(res.messages, vec![msg1, msg2]);
let msg = HandleMsg::Unbond {
tokens: Uint128(7_300),
};
let info = mock_info(USER1, &[]);
let res = handle(deps.as_mut(), mock_env(), info, msg).unwrap();
assert_users(deps.as_ref(), Some(6), None, None, None);
assert_eq!(res.messages.len(), 2);
let diff = MemberDiff::new(USER1, Some(13), Some(6));
let hook_msg = MemberChangedHookMsg::one(diff);
let msg1 = hook_msg.clone().into_cosmos_msg(contract1).unwrap();
let msg2 = hook_msg.into_cosmos_msg(contract2).unwrap();
assert_eq!(res.messages, vec![msg1, msg2]);
}
#[test]
fn only_bond_valid_coins() {
let mut deps = mock_dependencies(&[]);
default_init(deps.as_mut());
let info = mock_info(HumanAddr::from(USER1), &[]);
let err = handle(deps.as_mut(), mock_env(), info, HandleMsg::Bond {}).unwrap_err();
assert_eq!(err, ContractError::NoFunds {});
let info = mock_info(HumanAddr::from(USER1), &[coin(500, "FOO")]);
let err = handle(deps.as_mut(), mock_env(), info, HandleMsg::Bond {}).unwrap_err();
assert_eq!(err, ContractError::MissingDenom(DENOM.to_string()));
let info = mock_info(
HumanAddr::from(USER1),
&[coin(1234, DENOM), coin(5000, "BAR")],
);
let err = handle(deps.as_mut(), mock_env(), info, HandleMsg::Bond {}).unwrap_err();
assert_eq!(err, ContractError::ExtraDenoms(DENOM.to_string()));
let info = mock_info(HumanAddr::from(USER1), &[coin(500, DENOM)]);
handle(deps.as_mut(), mock_env(), info, HandleMsg::Bond {}).unwrap();
}
#[test]
fn ensure_bonding_edge_cases() {
let mut deps = mock_dependencies(&[]);
do_init(deps.as_mut(), Uint128(100), Uint128(0), Duration::Height(5));
bond(deps.as_mut(), 50, 1, 102, 1);
assert_users(deps.as_ref(), Some(0), Some(0), Some(1), None);
unbond(deps.as_mut(), 49, 1, 102, 2);
assert_users(deps.as_ref(), Some(0), None, None, None);
}
}