Skip to main content

tg4_stake/
contract.rs

1#[cfg(not(feature = "library"))]
2use cosmwasm_std::entry_point;
3use cosmwasm_std::{
4    coin, coins, to_binary, Addr, BankMsg, Binary, Coin, CustomQuery, Decimal, Deps, DepsMut, Env,
5    MessageInfo, Order, StdError, StdResult, Storage, Uint128,
6};
7use std::cmp::min;
8use std::ops::Sub;
9
10use crate::claim::process_pending_undelegations;
11use cw2::set_contract_version;
12use cw_storage_plus::Bound;
13use cw_utils::{ensure_from_older_version, maybe_addr};
14use tg4::{
15    HooksResponse, Member, MemberChangedHookMsg, MemberDiff, MemberInfo, MemberListResponse,
16    MemberResponse, TotalPointsResponse,
17};
18use tg_bindings::{
19    request_privileges, Privilege, PrivilegeChangeMsg, TgradeMsg, TgradeQuery, TgradeSudoMsg,
20};
21use tg_utils::{
22    members, validate_portion, Duration, ADMIN, HOOKS, PREAUTH_HOOKS, PREAUTH_SLASHING, SLASHERS,
23    TOTAL,
24};
25
26use crate::error::ContractError;
27use crate::msg::{
28    ClaimsResponse, ExecuteMsg, InstantiateMsg, MigrateMsg, PreauthResponse, QueryMsg,
29    StakedResponse, UnbondingPeriodResponse,
30};
31use crate::state::{claims, Config, CONFIG, STAKE, STAKE_VESTING};
32
33pub type Response = cosmwasm_std::Response<TgradeMsg>;
34pub type SubMsg = cosmwasm_std::SubMsg<TgradeMsg>;
35
36// version info for migration info
37const CONTRACT_NAME: &str = "crates.io:tg4-stake";
38const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
39
40// Note, you can use StdResult in some functions where you do not
41// make use of the custom errors
42#[cfg_attr(not(feature = "library"), entry_point)]
43pub fn instantiate(
44    mut deps: DepsMut<TgradeQuery>,
45    _env: Env,
46    _info: MessageInfo,
47    msg: InstantiateMsg,
48) -> Result<Response, ContractError> {
49    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
50    let api = deps.api;
51    ADMIN.set(deps.branch(), maybe_addr(api, msg.admin)?)?;
52
53    PREAUTH_HOOKS.set_auth(deps.storage, msg.preauths_hooks)?;
54    PREAUTH_SLASHING.set_auth(deps.storage, msg.preauths_slashing)?;
55
56    // min_bond is at least 1, so 0 stake -> non-membership
57    let min_bond = if msg.min_bond == Uint128::zero() {
58        Uint128::new(1)
59    } else {
60        msg.min_bond
61    };
62
63    let config = Config {
64        denom: msg.denom,
65        tokens_per_point: msg.tokens_per_point,
66        min_bond,
67        unbonding_period: Duration::new(msg.unbonding_period),
68        auto_return_limit: msg.auto_return_limit,
69    };
70    CONFIG.save(deps.storage, &config)?;
71    TOTAL.save(deps.storage, &0)?;
72    SLASHERS.instantiate(deps.storage)?;
73
74    Ok(Response::default())
75}
76
77// And declare a custom Error variant for the ones where you will want to make use of it
78#[cfg_attr(not(feature = "library"), entry_point)]
79pub fn execute(
80    deps: DepsMut<TgradeQuery>,
81    env: Env,
82    info: MessageInfo,
83    msg: ExecuteMsg,
84) -> Result<Response, ContractError> {
85    let api = deps.api;
86    match msg {
87        ExecuteMsg::UpdateAdmin { admin } => ADMIN
88            .execute_update_admin(deps, info, maybe_addr(api, admin)?)
89            .map_err(Into::into),
90        ExecuteMsg::AddHook { addr } => execute_add_hook(deps, info, addr),
91        ExecuteMsg::RemoveHook { addr } => execute_remove_hook(deps, info, addr),
92        ExecuteMsg::Bond { vesting_tokens } => execute_bond(deps, env, info, vesting_tokens),
93        ExecuteMsg::Unbond {
94            tokens: Coin { amount, denom },
95        } => execute_unbond(deps, env, info, amount, denom),
96        ExecuteMsg::Claim {} => execute_claim(deps, env, info),
97        ExecuteMsg::AddSlasher { addr } => execute_add_slasher(deps, info, addr),
98        ExecuteMsg::RemoveSlasher { addr } => execute_remove_slasher(deps, info, addr),
99        ExecuteMsg::Slash { addr, portion } => execute_slash(deps, env, info, addr, portion),
100    }
101}
102
103pub fn execute_add_hook<Q: CustomQuery>(
104    deps: DepsMut<Q>,
105    info: MessageInfo,
106    hook: String,
107) -> Result<Response, ContractError> {
108    // custom guard: using a preauth OR being admin
109    if !ADMIN.is_admin(deps.as_ref(), &info.sender)? {
110        PREAUTH_HOOKS.use_auth(deps.storage)?;
111    }
112
113    // add the hook
114    HOOKS.add_hook(deps.storage, deps.api.addr_validate(&hook)?)?;
115
116    // response
117    let res = Response::new()
118        .add_attribute("action", "add_hook")
119        .add_attribute("hook", hook)
120        .add_attribute("sender", info.sender);
121    Ok(res)
122}
123
124pub fn execute_remove_hook<Q: CustomQuery>(
125    deps: DepsMut<Q>,
126    info: MessageInfo,
127    hook: String,
128) -> Result<Response, ContractError> {
129    // custom guard: self-removal OR being admin
130    let hook_addr = deps.api.addr_validate(&hook)?;
131    if info.sender != hook_addr && !ADMIN.is_admin(deps.as_ref(), &info.sender)? {
132        return Err(ContractError::Unauthorized(
133            "Hook address is not same as sender's and sender is not an admin".to_owned(),
134        ));
135    }
136
137    // remove the hook
138    HOOKS.remove_hook(deps.storage, hook_addr)?;
139
140    // response
141    let res = Response::new()
142        .add_attribute("action", "remove_hook")
143        .add_attribute("hook", hook)
144        .add_attribute("sender", info.sender);
145    Ok(res)
146}
147
148pub fn execute_bond<Q: CustomQuery>(
149    deps: DepsMut<Q>,
150    env: Env,
151    info: MessageInfo,
152    vesting_tokens: Option<Coin>,
153) -> Result<Response, ContractError> {
154    let cfg = CONFIG.load(deps.storage)?;
155    let amount = validate_funds(&info.funds, &cfg.denom)?;
156    let vesting_amount = vesting_tokens
157        .map(|v| validate_funds(&[v], &cfg.denom))
158        .transpose()?
159        .unwrap_or_default();
160    if amount + vesting_amount == Uint128::zero() {
161        return Err(ContractError::NoFunds {});
162    }
163
164    // update the sender's stake
165    let new_stake = STAKE.update(deps.storage, &info.sender, |stake| -> StdResult<_> {
166        Ok(stake.unwrap_or_default() + amount)
167    })?;
168
169    let mut res = Response::new()
170        .add_attribute("action", "bond")
171        .add_attribute("amount", amount)
172        .add_attribute("sender", &info.sender);
173
174    // Update the sender's vesting stake
175    let new_vesting_stake =
176        STAKE_VESTING.update(deps.storage, &info.sender, |stake| -> StdResult<_> {
177            Ok(stake.unwrap_or_default() + vesting_amount)
178        })?;
179    // Delegate (stake to contract) to sender's vesting account
180    if vesting_amount > Uint128::zero() {
181        let msg = TgradeMsg::Delegate {
182            funds: coin(vesting_amount.into(), cfg.denom.clone()),
183            staker: info.sender.to_string(),
184        };
185        res = res
186            .add_message(msg)
187            .add_attribute("vesting_amount", vesting_amount);
188    }
189
190    // Update membership messages
191    res = res.add_submessages(update_membership(
192        deps.storage,
193        info.sender,
194        new_stake + new_vesting_stake,
195        &cfg,
196        env.block.height,
197    )?);
198
199    Ok(res)
200}
201
202pub fn execute_unbond<Q: CustomQuery>(
203    deps: DepsMut<Q>,
204    env: Env,
205    info: MessageInfo,
206    amount: Uint128,
207    denom: String,
208) -> Result<Response, ContractError> {
209    if amount.is_zero() {
210        return Err(ContractError::ZeroAmount {});
211    }
212
213    // provide them a claim
214    let cfg = CONFIG.load(deps.storage)?;
215
216    if cfg.denom != denom {
217        return Err(ContractError::InvalidDenom {});
218    }
219
220    // Load stake first for comparison
221    let stake = STAKE
222        .may_load(deps.storage, &info.sender)?
223        .unwrap_or_default();
224    // Reduce the sender's stake - saturating if insufficient
225    let new_stake = STAKE.update(deps.storage, &info.sender, |stake| -> StdResult<_> {
226        Ok(stake.unwrap_or_default().saturating_sub(amount))
227    })?;
228
229    let mut res = Response::new()
230        .add_attribute("action", "unbond")
231        .add_attribute("amount", amount)
232        .add_attribute("denom", &denom)
233        .add_attribute("sender", &info.sender);
234
235    // Reduce the sender's vesting stake - aborting if insufficient
236    let vesting_amount = amount.saturating_sub(stake);
237    let new_vesting_stake =
238        STAKE_VESTING.update(deps.storage, &info.sender, |stake| -> StdResult<_> {
239            Ok(stake.unwrap_or_default().checked_sub(vesting_amount)?)
240        })?;
241
242    // Create claim for unbonded liquid and vesting amounts
243    let completion = cfg.unbonding_period.after(&env.block);
244    claims().create_claim(
245        deps.storage,
246        info.sender.clone(),
247        min(stake, amount),
248        vesting_amount,
249        completion,
250        env.block.height,
251    )?;
252    res = res.add_attribute("completion_time", completion.time().nanos().to_string());
253
254    // Update membership messages
255    res = res.add_submessages(update_membership(
256        deps.storage,
257        info.sender,
258        new_stake + new_vesting_stake,
259        &cfg,
260        env.block.height,
261    )?);
262
263    Ok(res)
264}
265
266pub fn execute_add_slasher<Q: CustomQuery>(
267    deps: DepsMut<Q>,
268    info: MessageInfo,
269    slasher: String,
270) -> Result<Response, ContractError> {
271    // custom guard: using a preauth OR being admin
272    if !ADMIN.is_admin(deps.as_ref(), &info.sender)? {
273        PREAUTH_SLASHING.use_auth(deps.storage)?;
274    }
275
276    // add the slasher
277    SLASHERS.add_slasher(deps.storage, deps.api.addr_validate(&slasher)?)?;
278
279    // response
280    let res = Response::new()
281        .add_attribute("action", "add_slasher")
282        .add_attribute("slasher", slasher)
283        .add_attribute("sender", info.sender);
284    Ok(res)
285}
286
287pub fn execute_remove_slasher<Q: CustomQuery>(
288    deps: DepsMut<Q>,
289    info: MessageInfo,
290    slasher: String,
291) -> Result<Response, ContractError> {
292    // custom guard: self-removal OR being admin
293    let slasher_addr = Addr::unchecked(&slasher);
294    if info.sender != slasher_addr && !ADMIN.is_admin(deps.as_ref(), &info.sender)? {
295        return Err(ContractError::Unauthorized(
296            "Only slasher might remove himself and sender is not an admin".to_owned(),
297        ));
298    }
299
300    // remove the slasher
301    SLASHERS.remove_slasher(deps.storage, slasher_addr)?;
302
303    // response
304    let res = Response::new()
305        .add_attribute("action", "remove_slasher")
306        .add_attribute("slasher", slasher)
307        .add_attribute("sender", info.sender);
308    Ok(res)
309}
310
311pub fn execute_slash<Q: CustomQuery>(
312    deps: DepsMut<Q>,
313    env: Env,
314    info: MessageInfo,
315    addr: String,
316    portion: Decimal,
317) -> Result<Response, ContractError> {
318    if !SLASHERS.is_slasher(deps.storage, &info.sender)? {
319        return Err(ContractError::Unauthorized(
320            "Sender is not on slashers list".to_owned(),
321        ));
322    }
323
324    validate_portion(portion)?;
325
326    let cfg = CONFIG.load(deps.storage)?;
327    let addr = deps.api.addr_validate(&addr)?;
328
329    let liquid_stake = STAKE.may_load(deps.storage, &addr)?;
330    let vesting_stake = STAKE_VESTING.may_load(deps.storage, &addr)?;
331
332    // If address doesn't match anyone, leave early
333    if liquid_stake.is_none() && vesting_stake.is_none() {
334        return Ok(Response::new());
335    }
336
337    // response
338    let mut res = Response::new()
339        .add_attribute("action", "slash")
340        .add_attribute("addr", &addr)
341        .add_attribute("sender", info.sender);
342
343    // slash the liquid stake, if any
344    let mut new_liquid_stake = Uint128::zero();
345    let mut liquid_slashed = Uint128::zero();
346    if let Some(liquid_stake) = liquid_stake {
347        liquid_slashed = liquid_stake * portion;
348        new_liquid_stake = STAKE.update(deps.storage, &addr, |stake| -> StdResult<_> {
349            Ok(stake.unwrap_or_default().sub(liquid_slashed))
350        })?;
351    }
352
353    // slash the vesting stake, if any
354    let mut new_vesting_stake = Uint128::zero();
355    let mut vesting_slashed = Uint128::zero();
356    if let Some(vesting_stake) = vesting_stake {
357        vesting_slashed = vesting_stake * portion;
358        new_vesting_stake = STAKE_VESTING.update(deps.storage, &addr, |stake| -> StdResult<_> {
359            Ok(stake.unwrap_or_default().sub(vesting_slashed))
360        })?;
361    }
362
363    // slash the liquid and vesting claims
364    let (liquid_claims_slashed, vesting_claims_slashed) =
365        claims().slash_claims_for_addr(deps.storage, addr.clone(), portion)?;
366    liquid_slashed += liquid_claims_slashed;
367    vesting_slashed += vesting_claims_slashed;
368
369    // burn the liquid slashed tokens
370    if liquid_slashed > Uint128::zero() {
371        let burn_liquid_msg = BankMsg::Burn {
372            amount: coins(liquid_slashed.u128(), &cfg.denom),
373        };
374        res = res.add_message(burn_liquid_msg);
375    }
376
377    // burn the vesting slashed tokens
378    if vesting_slashed > Uint128::zero() {
379        let burn_vesting_msg = BankMsg::Burn {
380            amount: coins(vesting_slashed.u128(), &cfg.denom),
381        };
382        res = res.add_message(burn_vesting_msg);
383    }
384
385    res.messages.extend(update_membership(
386        deps.storage,
387        addr,
388        new_liquid_stake + new_vesting_stake,
389        &cfg,
390        env.block.height,
391    )?);
392
393    Ok(res)
394}
395
396/// Validates funds sent with the message, that they are containing only a single denom. Returns
397/// amount of funds sent, or error if:
398/// * More than a single denom is sent (`ExtraDenoms` error)
399/// * Invalid single denom is sent (`MissingDenom` error)
400/// Note that no funds (or a coin of the right denom but zero amount) is a valid option here.
401pub fn validate_funds(funds: &[Coin], stake_denom: &str) -> Result<Uint128, ContractError> {
402    match funds {
403        [] => Ok(Uint128::zero()),
404        [Coin { denom, amount }] if denom == stake_denom => Ok(*amount),
405        [_] => Err(ContractError::MissingDenom(stake_denom.to_string())),
406        _ => Err(ContractError::ExtraDenoms(stake_denom.to_string())),
407    }
408}
409
410fn update_membership(
411    storage: &mut dyn Storage,
412    sender: Addr,
413    new_stake: Uint128,
414    cfg: &Config,
415    height: u64,
416) -> StdResult<Vec<SubMsg>> {
417    // update their membership points
418    let new = calc_points(new_stake, cfg);
419    let old = members().may_load(storage, &sender)?.map(|mi| mi.points);
420
421    // short-circuit if no change
422    if new == old {
423        return Ok(vec![]);
424    }
425    // otherwise, record change of points
426    match new.as_ref() {
427        Some(&p) => members().save(storage, &sender, &MemberInfo::new(p), height),
428        None => members().remove(storage, &sender, height),
429    }?;
430
431    // update total
432    TOTAL.update(storage, |total| -> StdResult<_> {
433        Ok(total + new.unwrap_or_default() - old.unwrap_or_default())
434    })?;
435
436    // alert the hooks
437    let diff = MemberDiff::new(sender, old, new);
438    HOOKS.prepare_hooks(storage, |h| {
439        MemberChangedHookMsg::one(diff.clone())
440            .into_cosmos_msg(h)
441            .map(SubMsg::new)
442    })
443}
444
445fn calc_points(stake: Uint128, cfg: &Config) -> Option<u64> {
446    if stake < cfg.min_bond {
447        None
448    } else {
449        let p = stake.u128() / cfg.tokens_per_point.u128();
450        Some(p as u64)
451    }
452}
453
454pub fn execute_claim<Q: CustomQuery>(
455    deps: DepsMut<Q>,
456    env: Env,
457    info: MessageInfo,
458) -> Result<Response, ContractError> {
459    let (release, vesting_release) =
460        claims().claim_addr(deps.storage, &info.sender, &env.block, None)?;
461    if release.is_zero() && vesting_release.is_zero() {
462        return Err(ContractError::NothingToClaim {});
463    }
464
465    let config = CONFIG.load(deps.storage)?;
466
467    let mut res = Response::new()
468        .add_attribute("action", "claim")
469        .add_attribute("sender", &info.sender);
470
471    if !release.is_zero() {
472        let amount = coin(release.into(), config.denom.clone());
473        res = res
474            .add_attribute("liquid_tokens", amount.to_string())
475            .add_message(BankMsg::Send {
476                to_address: info.sender.clone().into(),
477                amount: vec![amount],
478            });
479    }
480
481    if !vesting_release.is_zero() {
482        let vesting_amount = coin(vesting_release.into(), config.denom);
483        // Undelegate (unstake from contract) to sender's vesting account
484        res = res
485            .add_attribute("vesting_tokens", vesting_amount.to_string())
486            .add_message(TgradeMsg::Undelegate {
487                funds: vesting_amount,
488                recipient: info.sender.to_string(),
489            });
490    }
491
492    Ok(res)
493}
494
495#[cfg_attr(not(feature = "library"), entry_point)]
496pub fn sudo(
497    deps: DepsMut<TgradeQuery>,
498    env: Env,
499    msg: TgradeSudoMsg,
500) -> Result<Response, ContractError> {
501    match msg {
502        TgradeSudoMsg::PrivilegeChange(PrivilegeChangeMsg::Promoted {}) => privilege_promote(deps),
503        TgradeSudoMsg::EndBlock {} => end_block(deps, env),
504        _ => Err(ContractError::UnknownSudoMsg {}),
505    }
506}
507
508fn privilege_promote<Q: CustomQuery>(deps: DepsMut<Q>) -> Result<Response, ContractError> {
509    let config = CONFIG.load(deps.storage)?;
510
511    let mut res = Response::new();
512    if config.auto_return_limit > 0 {
513        let msgs = request_privileges(&[Privilege::EndBlocker]);
514        res = res.add_submessages(msgs);
515    }
516    let msgs = request_privileges(&[Privilege::Delegator]);
517    res = res.add_submessages(msgs);
518
519    Ok(res)
520}
521
522fn end_block<Q: CustomQuery>(deps: DepsMut<Q>, env: Env) -> Result<Response, ContractError> {
523    let mut resp = Response::new();
524
525    let config = CONFIG.load(deps.storage)?;
526    if config.auto_return_limit > 0 {
527        let sub_msgs = release_expired_claims(deps, env, config)?;
528        resp = resp.add_submessages(sub_msgs);
529    }
530
531    Ok(resp)
532}
533
534fn release_expired_claims<Q: CustomQuery>(
535    deps: DepsMut<Q>,
536    env: Env,
537    config: Config,
538) -> Result<Vec<SubMsg>, ContractError> {
539    let release_data =
540        claims().claim_expired(deps.storage, &env.block, config.auto_return_limit)?;
541
542    let send_msgs = release_data
543        .liquid_releases
544        .into_iter()
545        .filter(|release_info| !release_info.amount.is_zero())
546        .map(|release_info| {
547            let amount = coins(release_info.amount.into(), config.denom.clone());
548            Ok(SubMsg::new(BankMsg::Send {
549                to_address: release_info.addr.into(),
550                amount,
551            }))
552        })
553        .collect::<StdResult<Vec<_>>>()?;
554
555    let undelegate_msgs = release_data
556        .vesting_releases
557        .into_iter()
558        .filter(|release_info| !release_info.amount.is_zero())
559        .map(|release_info| {
560            let amount = coin(release_info.amount.into(), config.denom.clone());
561            Ok(SubMsg::new(TgradeMsg::Undelegate {
562                funds: amount,
563                recipient: release_info.addr.to_string(),
564            }))
565        })
566        .collect::<StdResult<Vec<_>>>()?;
567
568    Ok(send_msgs.into_iter().chain(undelegate_msgs).collect())
569}
570
571#[cfg_attr(not(feature = "library"), entry_point)]
572pub fn query(deps: Deps<TgradeQuery>, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
573    use QueryMsg::*;
574    match msg {
575        Configuration {} => to_binary(&CONFIG.load(deps.storage)?),
576        Member {
577            addr,
578            at_height: height,
579        } => to_binary(&query_member(deps, addr, height)?),
580        ListMembers { start_after, limit } => to_binary(&list_members(deps, start_after, limit)?),
581        ListMembersByPoints { start_after, limit } => {
582            to_binary(&list_members_by_points(deps, start_after, limit)?)
583        }
584        TotalPoints {} => to_binary(&query_total_points(deps)?),
585        Claims {
586            address,
587            limit,
588            start_after,
589        } => to_binary(&ClaimsResponse {
590            claims: claims().query_claims(
591                deps,
592                deps.api.addr_validate(&address)?,
593                limit,
594                start_after,
595            )?,
596        }),
597        Staked { address } => to_binary(&query_staked(deps, address)?),
598        Admin {} => to_binary(&ADMIN.query_admin(deps)?),
599        Hooks {} => {
600            let hooks = HOOKS.list_hooks(deps.storage)?;
601            to_binary(&HooksResponse { hooks })
602        }
603        Preauths {} => {
604            let preauths_hooks = PREAUTH_HOOKS.get_auth(deps.storage)?;
605            to_binary(&PreauthResponse { preauths_hooks })
606        }
607        UnbondingPeriod {} => {
608            let Config {
609                unbonding_period, ..
610            } = CONFIG.load(deps.storage)?;
611            to_binary(&UnbondingPeriodResponse { unbonding_period })
612        }
613        IsSlasher { addr } => {
614            let addr = deps.api.addr_validate(&addr)?;
615            to_binary(&SLASHERS.is_slasher(deps.storage, &addr)?)
616        }
617        ListSlashers {} => to_binary(&SLASHERS.list_slashers(deps.storage)?),
618    }
619}
620
621fn query_total_points<Q: CustomQuery>(deps: Deps<Q>) -> StdResult<TotalPointsResponse> {
622    let points = TOTAL.load(deps.storage)?;
623    Ok(TotalPointsResponse { points })
624}
625
626pub fn query_staked<Q: CustomQuery>(deps: Deps<Q>, addr: String) -> StdResult<StakedResponse> {
627    let addr = deps.api.addr_validate(&addr)?;
628    let stake = STAKE.may_load(deps.storage, &addr)?.unwrap_or_default();
629    let vesting = STAKE_VESTING
630        .may_load(deps.storage, &addr)?
631        .unwrap_or_default();
632    let config = CONFIG.load(deps.storage)?;
633
634    Ok(StakedResponse {
635        liquid: coin(stake.u128(), config.denom.clone()),
636        vesting: coin(vesting.u128(), config.denom),
637    })
638}
639
640fn query_member<Q: CustomQuery>(
641    deps: Deps<Q>,
642    addr: String,
643    height: Option<u64>,
644) -> StdResult<MemberResponse> {
645    let addr = deps.api.addr_validate(&addr)?;
646    let mi = match height {
647        Some(h) => members().may_load_at_height(deps.storage, &addr, h),
648        None => members().may_load(deps.storage, &addr),
649    }?;
650    Ok(mi.into())
651}
652
653// settings for pagination
654const MAX_LIMIT: u32 = 100;
655const DEFAULT_LIMIT: u32 = 30;
656
657fn list_members<Q: CustomQuery>(
658    deps: Deps<Q>,
659    start_after: Option<String>,
660    limit: Option<u32>,
661) -> StdResult<MemberListResponse> {
662    let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize;
663    let addr = maybe_addr(deps.api, start_after)?;
664    let start = addr.as_ref().map(Bound::exclusive);
665
666    let members: StdResult<Vec<_>> = members()
667        .range(deps.storage, start, None, Order::Ascending)
668        .take(limit)
669        .map(|item| {
670            let (
671                addr,
672                MemberInfo {
673                    points,
674                    start_height,
675                },
676            ) = item?;
677            Ok(Member {
678                addr: addr.into(),
679                points,
680                start_height,
681            })
682        })
683        .collect();
684
685    Ok(MemberListResponse { members: members? })
686}
687
688fn list_members_by_points<Q: CustomQuery>(
689    deps: Deps<Q>,
690    start_after: Option<Member>,
691    limit: Option<u32>,
692) -> StdResult<MemberListResponse> {
693    let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize;
694    let start = start_after
695        .map(|m| {
696            deps.api
697                .addr_validate(&m.addr)
698                .map(|addr| Bound::exclusive((m.points, addr)))
699        })
700        .transpose()?;
701
702    let members: StdResult<Vec<_>> = members()
703        .idx
704        .points
705        .range(deps.storage, None, start, Order::Descending)
706        .take(limit)
707        .map(|item| {
708            let (
709                addr,
710                MemberInfo {
711                    points,
712                    start_height,
713                },
714            ) = item?;
715            Ok(Member {
716                addr: addr.into(),
717                points,
718                start_height,
719            })
720        })
721        .collect();
722
723    Ok(MemberListResponse { members: members? })
724}
725
726#[cfg_attr(not(feature = "library"), entry_point)]
727pub fn migrate(
728    deps: DepsMut<TgradeQuery>,
729    _env: Env,
730    msg: MigrateMsg,
731) -> Result<Response, ContractError> {
732    ensure_from_older_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
733
734    CONFIG.update::<_, StdError>(deps.storage, |mut cfg| {
735        if let Some(tokens_per_point) = msg.tokens_per_point {
736            let tokens_per_point = if tokens_per_point == Uint128::zero() {
737                Uint128::new(1)
738            } else {
739                tokens_per_point
740            };
741            cfg.tokens_per_point = tokens_per_point;
742        }
743        if let Some(min_bond) = msg.min_bond {
744            let min_bond = if min_bond == Uint128::zero() {
745                Uint128::new(1)
746            } else {
747                min_bond
748            };
749            cfg.min_bond = min_bond;
750        }
751        if let Some(unbonding_period) = msg.unbonding_period {
752            cfg.unbonding_period = Duration::new(unbonding_period);
753        }
754        if let Some(auto_return_limit) = msg.auto_return_limit {
755            cfg.auto_return_limit = auto_return_limit;
756        }
757        Ok(cfg)
758    })?;
759
760    if let Some(undelegations) = msg.undelegations {
761        let msgs = process_pending_undelegations(deps.as_ref(), &undelegations)?;
762        Ok(Response::new().add_messages(msgs))
763    } else {
764        Ok(Response::new())
765    }
766}
767
768#[cfg(test)]
769mod tests {
770    use crate::claim::Claim;
771    use cosmwasm_std::testing::{mock_env, mock_info};
772    use cosmwasm_std::{
773        from_slice, CosmosMsg, OverflowError, OverflowOperation, StdError, Storage,
774    };
775    use tg4::{member_key, TOTAL_KEY};
776    use tg_utils::{Expiration, HookError, PreauthError, SlasherError};
777
778    use crate::error::ContractError;
779
780    use super::*;
781    use tg_bindings_test::mock_deps_tgrade;
782
783    const INIT_ADMIN: &str = "juan";
784    const USER1: &str = "user1";
785    const USER2: &str = "user2";
786    const USER3: &str = "user3";
787    const DENOM: &str = "stake";
788    const TOKENS_PER_POINT: Uint128 = Uint128::new(1_000);
789    const MIN_BOND: Uint128 = Uint128::new(5_000);
790    const UNBONDING_DURATION: u64 = 100;
791
792    fn default_instantiate(deps: DepsMut<TgradeQuery>) {
793        do_instantiate(deps, TOKENS_PER_POINT, MIN_BOND, UNBONDING_DURATION, 0)
794    }
795
796    fn do_instantiate(
797        deps: DepsMut<TgradeQuery>,
798        tokens_per_point: Uint128,
799        min_bond: Uint128,
800        unbonding_period: u64,
801        auto_return_limit: u64,
802    ) {
803        let msg = InstantiateMsg {
804            denom: "stake".to_owned(),
805            tokens_per_point,
806            min_bond,
807            unbonding_period,
808            admin: Some(INIT_ADMIN.into()),
809            preauths_hooks: 1,
810            preauths_slashing: 1,
811            auto_return_limit,
812        };
813        let info = mock_info("creator", &[]);
814        instantiate(deps, mock_env(), info, msg).unwrap();
815    }
816
817    // Helper for staking only liquid assets
818    fn bond_liquid(
819        deps: DepsMut<TgradeQuery>,
820        user1: u128,
821        user2: u128,
822        user3: u128,
823        height_delta: u64,
824    ) {
825        bond(deps, (user1, 0), (user2, 0), (user3, 0), height_delta);
826    }
827
828    // Helper for staking only illiquid assets
829    fn bond_vesting(
830        deps: DepsMut<TgradeQuery>,
831        user1: u128,
832        user2: u128,
833        user3: u128,
834        height_delta: u64,
835    ) {
836        bond(deps, (0, user1), (0, user2), (0, user3), height_delta);
837    }
838
839    // Full stake is composed of `(liquid, illiquid (vesting))` amounts
840    fn bond(
841        mut deps: DepsMut<TgradeQuery>,
842        user1_stake: (u128, u128),
843        user2_stake: (u128, u128),
844        user3_stake: (u128, u128),
845        height_delta: u64,
846    ) {
847        let mut env = mock_env();
848        env.block.height += height_delta;
849
850        for (addr, stake) in &[
851            (USER1, user1_stake),
852            (USER2, user2_stake),
853            (USER3, user3_stake),
854        ] {
855            if stake.0 != 0 || stake.1 != 0 {
856                let vesting_tokens = if stake.1 != 0 {
857                    Some(coin(stake.1, DENOM))
858                } else {
859                    None
860                };
861                let msg = ExecuteMsg::Bond { vesting_tokens };
862                let info = mock_info(addr, &coins(stake.0, DENOM));
863                execute(deps.branch(), env.clone(), info, msg).unwrap();
864            }
865        }
866    }
867
868    fn unbond(
869        mut deps: DepsMut<TgradeQuery>,
870        user1: u128,
871        user2: u128,
872        user3: u128,
873        height_delta: u64,
874        time_delta: u64,
875    ) {
876        let mut env = mock_env();
877        env.block.height += height_delta;
878        env.block.time = env.block.time.plus_seconds(time_delta);
879
880        for (addr, stake) in &[(USER1, user1), (USER2, user2), (USER3, user3)] {
881            if *stake != 0 {
882                let msg = ExecuteMsg::Unbond {
883                    tokens: coin(*stake, DENOM),
884                };
885                let info = mock_info(addr, &[]);
886                execute(deps.branch(), env.clone(), info, msg).unwrap();
887            }
888        }
889    }
890
891    #[test]
892    fn proper_instantiation() {
893        let mut deps = mock_deps_tgrade();
894        default_instantiate(deps.as_mut());
895
896        // it worked, let's query the state
897        let res = ADMIN.query_admin(deps.as_ref()).unwrap();
898        assert_eq!(Some(INIT_ADMIN.into()), res.admin);
899
900        let res = query_total_points(deps.as_ref()).unwrap();
901        assert_eq!(0, res.points);
902
903        let raw = query(deps.as_ref(), mock_env(), QueryMsg::Configuration {}).unwrap();
904        let res: Config = from_slice(&raw).unwrap();
905        assert_eq!(
906            res,
907            Config {
908                denom: "stake".to_owned(),
909                tokens_per_point: TOKENS_PER_POINT,
910                min_bond: MIN_BOND,
911                unbonding_period: Duration::new(UNBONDING_DURATION),
912                auto_return_limit: 0,
913            }
914        );
915
916        // query the admin's staked amount (just to confirm the query works)
917        let res = query_staked(deps.as_ref(), INIT_ADMIN.into()).unwrap();
918        assert_eq!(coin(0, "stake"), res.liquid);
919        assert_eq!(coin(0, "stake"), res.vesting);
920    }
921
922    #[test]
923    fn unbonding_period_query_works() {
924        let mut deps = mock_deps_tgrade();
925        default_instantiate(deps.as_mut());
926
927        let raw = query(deps.as_ref(), mock_env(), QueryMsg::UnbondingPeriod {}).unwrap();
928        let res: UnbondingPeriodResponse = from_slice(&raw).unwrap();
929        assert_eq!(res.unbonding_period, Duration::new(UNBONDING_DURATION));
930    }
931
932    fn get_member(deps: Deps<TgradeQuery>, addr: String, at_height: Option<u64>) -> Option<u64> {
933        let raw = query(deps, mock_env(), QueryMsg::Member { addr, at_height }).unwrap();
934        let res: MemberResponse = from_slice(&raw).unwrap();
935        res.points
936    }
937
938    // this tests the member queries
939    #[track_caller]
940    fn assert_users(
941        deps: Deps<TgradeQuery>,
942        user1_points: Option<u64>,
943        user2_points: Option<u64>,
944        user3_points: Option<u64>,
945        height: Option<u64>,
946    ) {
947        let member1 = get_member(deps, USER1.into(), height);
948        assert_eq!(member1, user1_points);
949
950        let member2 = get_member(deps, USER2.into(), height);
951        assert_eq!(member2, user2_points);
952
953        let member3 = get_member(deps, USER3.into(), height);
954        assert_eq!(member3, user3_points);
955
956        // this is only valid if we are not doing a historical query
957        if height.is_none() {
958            // compute expected metrics
959            let points = vec![user1_points, user2_points, user3_points];
960            let sum: u64 = points.iter().map(|x| x.unwrap_or_default()).sum();
961            let count = points.iter().filter(|x| x.is_some()).count();
962
963            // TODO: more detailed compare?
964            let msg = QueryMsg::ListMembers {
965                start_after: None,
966                limit: None,
967            };
968            let raw = query(deps, mock_env(), msg).unwrap();
969            let members: MemberListResponse = from_slice(&raw).unwrap();
970            assert_eq!(count, members.members.len());
971
972            let raw = query(deps, mock_env(), QueryMsg::TotalPoints {}).unwrap();
973            let total: TotalPointsResponse = from_slice(&raw).unwrap();
974            assert_eq!(sum, total.points); // 17 - 11 + 15 = 21
975        }
976    }
977
978    // this tests the member queries of liquid amounts
979    #[track_caller]
980    fn assert_stake_liquid(deps: Deps<TgradeQuery>, user1: u128, user2: u128, user3: u128) {
981        let stake1 = query_staked(deps, USER1.into()).unwrap();
982        assert_eq!(stake1.liquid, coin(user1, DENOM));
983
984        let stake2 = query_staked(deps, USER2.into()).unwrap();
985        assert_eq!(stake2.liquid, coin(user2, DENOM));
986
987        let stake3 = query_staked(deps, USER3.into()).unwrap();
988        assert_eq!(stake3.liquid, coin(user3, DENOM));
989    }
990
991    // this tests the member queries of illiquid amounts
992    #[track_caller]
993    fn assert_stake_vesting(deps: Deps<TgradeQuery>, user1: u128, user2: u128, user3: u128) {
994        let stake1 = query_staked(deps, USER1.into()).unwrap();
995        assert_eq!(stake1.vesting, coin(user1, DENOM));
996
997        let stake2 = query_staked(deps, USER2.into()).unwrap();
998        assert_eq!(stake2.vesting, coin(user2, DENOM));
999
1000        let stake3 = query_staked(deps, USER3.into()).unwrap();
1001        assert_eq!(stake3.vesting, coin(user3, DENOM));
1002    }
1003
1004    #[test]
1005    fn bond_stake_liquid_adds_membership() {
1006        let mut deps = mock_deps_tgrade();
1007        default_instantiate(deps.as_mut());
1008        let height = mock_env().block.height;
1009
1010        // Assert original points
1011        assert_users(deps.as_ref(), None, None, None, None);
1012
1013        // ensure it rounds down, and respects cut-off
1014        bond_liquid(deps.as_mut(), 12_000, 7_500, 4_000, 1);
1015
1016        // Assert updated points
1017        assert_stake_liquid(deps.as_ref(), 12_000, 7_500, 4_000);
1018        assert_users(deps.as_ref(), Some(12), Some(7), None, None);
1019
1020        // add some more, ensure the sum is properly respected (7.5 + 7.6 = 15 not 14)
1021        bond_liquid(deps.as_mut(), 0, 7_600, 1_200, 2);
1022
1023        // Assert updated points
1024        assert_stake_liquid(deps.as_ref(), 12_000, 15_100, 5_200);
1025        assert_users(deps.as_ref(), Some(12), Some(15), Some(5), None);
1026
1027        // check historical queries all work
1028        assert_users(deps.as_ref(), None, None, None, Some(height + 1)); // before first stake
1029        assert_users(deps.as_ref(), Some(12), Some(7), None, Some(height + 2)); // after first stake
1030        assert_users(deps.as_ref(), Some(12), Some(15), Some(5), Some(height + 3));
1031        // after second stake
1032    }
1033
1034    #[test]
1035    fn bond_stake_vesting_adds_membership() {
1036        let mut deps = mock_deps_tgrade();
1037        default_instantiate(deps.as_mut());
1038        let height = mock_env().block.height;
1039
1040        // Assert original points
1041        assert_users(deps.as_ref(), None, None, None, None);
1042
1043        // ensure it rounds down, and respects cut-off
1044        bond_vesting(deps.as_mut(), 12_000, 7_500, 4_000, 1);
1045
1046        // Assert updated points
1047        assert_stake_vesting(deps.as_ref(), 12_000, 7_500, 4_000);
1048        assert_users(deps.as_ref(), Some(12), Some(7), None, None);
1049
1050        // add some more, ensure the sum is properly respected (7.5 + 7.6 = 15 not 14)
1051        bond_vesting(deps.as_mut(), 0, 7_600, 1_200, 2);
1052
1053        // Assert updated points
1054        assert_stake_vesting(deps.as_ref(), 12_000, 15_100, 5_200);
1055        assert_users(deps.as_ref(), Some(12), Some(15), Some(5), None);
1056
1057        // check historical queries all work
1058        assert_users(deps.as_ref(), None, None, None, Some(height + 1)); // before first stake
1059        assert_users(deps.as_ref(), Some(12), Some(7), None, Some(height + 2)); // after first stake
1060        assert_users(deps.as_ref(), Some(12), Some(15), Some(5), Some(height + 3));
1061        // after second stake
1062    }
1063
1064    #[test]
1065    fn bond_mixed_stake_adds_membership() {
1066        let mut deps = mock_deps_tgrade();
1067        default_instantiate(deps.as_mut());
1068        let height = mock_env().block.height;
1069
1070        // Assert original points
1071        assert_users(deps.as_ref(), None, None, None, None);
1072
1073        // ensure it rounds down, and respects cut-off
1074        bond_liquid(deps.as_mut(), 12_000, 7_500, 4_000, 1);
1075
1076        // Assert updated points
1077        assert_stake_liquid(deps.as_ref(), 12_000, 7_500, 4_000);
1078        assert_users(deps.as_ref(), Some(12), Some(7), None, None);
1079
1080        // add some more, ensure the sum is properly respected (7.5 + 7.6 = 15 not 14)
1081        bond_vesting(deps.as_mut(), 0, 7_600, 1_200, 2);
1082
1083        // Assert updated points
1084        assert_stake_vesting(deps.as_ref(), 0, 7_600, 1_200);
1085        assert_users(deps.as_ref(), Some(12), Some(15), Some(5), None);
1086
1087        // check historical queries all work
1088        assert_users(deps.as_ref(), None, None, None, Some(height + 1)); // before first stake
1089        assert_users(deps.as_ref(), Some(12), Some(7), None, Some(height + 2)); // after first stake
1090        assert_users(deps.as_ref(), Some(12), Some(15), Some(5), Some(height + 3));
1091        // after second stake
1092    }
1093
1094    #[test]
1095    fn try_member_queries() {
1096        let mut deps = mock_deps_tgrade();
1097        default_instantiate(deps.as_mut());
1098
1099        bond(deps.as_mut(), (12_000, 0), (7_400, 100), (0, 4_000), 1);
1100
1101        let member1 = query_member(deps.as_ref(), USER1.into(), None).unwrap();
1102        assert_eq!(member1.points, Some(12));
1103
1104        let member2 = query_member(deps.as_ref(), USER2.into(), None).unwrap();
1105        assert_eq!(member2.points, Some(7));
1106
1107        let member3 = query_member(deps.as_ref(), USER3.into(), None).unwrap();
1108        assert_eq!(member3.points, None);
1109
1110        let members = list_members(deps.as_ref(), None, None).unwrap().members;
1111        assert_eq!(members.len(), 2);
1112        // Assert the set is proper
1113        assert_eq!(
1114            members,
1115            vec![
1116                Member {
1117                    addr: USER1.into(),
1118                    points: 12,
1119                    start_height: None,
1120                },
1121                Member {
1122                    addr: USER2.into(),
1123                    points: 7,
1124                    start_height: None,
1125                },
1126            ]
1127        );
1128
1129        // Test pagination / limits
1130        let members = list_members(deps.as_ref(), None, Some(1)).unwrap().members;
1131        assert_eq!(members.len(), 1);
1132        // Assert the set is proper
1133        assert_eq!(
1134            members,
1135            vec![Member {
1136                addr: USER1.into(),
1137                points: 12,
1138                start_height: None,
1139            },]
1140        );
1141
1142        // Next page
1143        let start_after = Some(members[0].addr.clone());
1144        let members = list_members(deps.as_ref(), start_after, Some(1))
1145            .unwrap()
1146            .members;
1147        assert_eq!(members.len(), 1);
1148        // Assert the set is proper
1149        assert_eq!(
1150            members,
1151            vec![Member {
1152                addr: USER2.into(),
1153                points: 7,
1154                start_height: None,
1155            },]
1156        );
1157
1158        // Assert there's no more
1159        let start_after = Some(members[0].addr.clone());
1160        let members = list_members(deps.as_ref(), start_after, Some(1))
1161            .unwrap()
1162            .members;
1163        assert_eq!(members.len(), 0);
1164    }
1165
1166    #[test]
1167    fn try_list_members_by_points() {
1168        let mut deps = mock_deps_tgrade();
1169        default_instantiate(deps.as_mut());
1170
1171        bond(deps.as_mut(), (10_000, 1_000), (6_500, 0), (0, 5_000), 1);
1172
1173        let members = list_members_by_points(deps.as_ref(), None, None)
1174            .unwrap()
1175            .members;
1176        assert_eq!(members.len(), 3);
1177        // Assert the set is sorted by (descending) points
1178        assert_eq!(
1179            members,
1180            vec![
1181                Member {
1182                    addr: USER1.into(),
1183                    points: 11,
1184                    start_height: None,
1185                },
1186                Member {
1187                    addr: USER2.into(),
1188                    points: 6,
1189                    start_height: None,
1190                },
1191                Member {
1192                    addr: USER3.into(),
1193                    points: 5,
1194                    start_height: None,
1195                },
1196            ]
1197        );
1198
1199        // Test pagination / limits
1200        let members = list_members_by_points(deps.as_ref(), None, Some(1))
1201            .unwrap()
1202            .members;
1203        assert_eq!(members.len(), 1);
1204        // Assert the set is proper
1205        assert_eq!(
1206            members,
1207            vec![Member {
1208                addr: USER1.into(),
1209                points: 11,
1210                start_height: None,
1211            },]
1212        );
1213
1214        // Next page
1215        let last = members.last().unwrap();
1216        let start_after = Some(last.clone());
1217        let members = list_members_by_points(deps.as_ref(), start_after, None)
1218            .unwrap()
1219            .members;
1220        assert_eq!(members.len(), 2);
1221        // Assert the set is proper
1222        assert_eq!(
1223            members,
1224            vec![
1225                Member {
1226                    addr: USER2.into(),
1227                    points: 6,
1228                    start_height: None,
1229                },
1230                Member {
1231                    addr: USER3.into(),
1232                    points: 5,
1233                    start_height: None,
1234                },
1235            ]
1236        );
1237
1238        // Assert there's no more
1239        let last = members.last().unwrap();
1240        let start_after = Some(last.clone());
1241        let members = list_members_by_points(deps.as_ref(), start_after, Some(1))
1242            .unwrap()
1243            .members;
1244        assert_eq!(members.len(), 0);
1245    }
1246
1247    #[test]
1248    fn unbond_validations() {
1249        let mut deps = mock_deps_tgrade();
1250        default_instantiate(deps.as_mut());
1251
1252        // Zero amount unbonds are rejected
1253        let msg = ExecuteMsg::Unbond {
1254            tokens: coin(0, DENOM),
1255        };
1256        let env = mock_env();
1257        let info = mock_info(USER1, &[]);
1258        let err = execute(deps.as_mut(), env, info, msg).unwrap_err();
1259        assert_eq!(ContractError::ZeroAmount {}, err);
1260
1261        // Invalid denom unbonds are rejected
1262        let msg = ExecuteMsg::Unbond {
1263            tokens: coin(1234, "INV"),
1264        };
1265        let env = mock_env();
1266        let info = mock_info(USER1, &[]);
1267        let err = execute(deps.as_mut(), env, info, msg).unwrap_err();
1268        assert_eq!(ContractError::InvalidDenom {}, err);
1269    }
1270
1271    #[test]
1272    fn unbond_stake_update_membership() {
1273        let mut deps = mock_deps_tgrade();
1274        default_instantiate(deps.as_mut());
1275        let height = mock_env().block.height;
1276
1277        // ensure it rounds down, and respects cut-off
1278        bond(deps.as_mut(), (0, 12_000), (500, 7_000), (3_000, 3_000), 1);
1279        assert_users(deps.as_ref(), Some(12), Some(7), Some(6), None);
1280
1281        unbond(deps.as_mut(), 4_500, 2_600, 1_000, 2, 0);
1282
1283        // Assert updated points
1284        assert_stake_liquid(deps.as_ref(), 0, 0, 2000);
1285        assert_stake_vesting(deps.as_ref(), 7_500, 4_900, 3000);
1286        assert_users(deps.as_ref(), Some(7), None, Some(5), None);
1287
1288        // Adding a little more returns points
1289        bond(deps.as_mut(), (500, 100), (100, 0), (0, 2_222), 3);
1290
1291        // Assert updated points
1292        assert_stake_liquid(deps.as_ref(), 500, 100, 2000);
1293        assert_stake_vesting(deps.as_ref(), 7_600, 4_900, 5_222);
1294        assert_users(deps.as_ref(), Some(8), Some(5), Some(7), None);
1295
1296        // check historical queries all work
1297        assert_users(deps.as_ref(), None, None, None, Some(height + 1)); // before first stake
1298        assert_users(deps.as_ref(), Some(12), Some(7), Some(6), Some(height + 2)); // after first bond
1299        assert_users(deps.as_ref(), Some(7), None, Some(5), Some(height + 3)); // after first unbond
1300        assert_users(deps.as_ref(), Some(8), Some(5), Some(7), Some(height + 4)); // after second bond
1301
1302        // error if try to unbond more than stake (USER2 has 5000 staked)
1303        let msg = ExecuteMsg::Unbond {
1304            tokens: coin(5100, DENOM),
1305        };
1306        let mut env = mock_env();
1307        env.block.height += 5;
1308        let info = mock_info(USER2, &[]);
1309        let err = execute(deps.as_mut(), env, info, msg).unwrap_err();
1310        assert_eq!(
1311            err,
1312            ContractError::Std(StdError::overflow(OverflowError::new(
1313                OverflowOperation::Sub,
1314                4900,
1315                5000,
1316            )))
1317        );
1318    }
1319
1320    #[test]
1321    fn raw_queries_work() {
1322        // add will over-write and remove have no effect
1323        let mut deps = mock_deps_tgrade();
1324        default_instantiate(deps.as_mut());
1325        // Set values as (11, 6, None)
1326        bond(deps.as_mut(), (1_000, 10_000), (6_000, 0), (0, 0), 1);
1327
1328        // get total from raw key
1329        let total_raw = deps.storage.get(TOTAL_KEY.as_bytes()).unwrap();
1330        let total: u64 = from_slice(&total_raw).unwrap();
1331        assert_eq!(17, total);
1332
1333        // get member votes from raw key
1334        let member2_raw = deps.storage.get(&member_key(USER2)).unwrap();
1335        let member2: MemberInfo = from_slice(&member2_raw).unwrap();
1336        assert_eq!(6, member2.points);
1337
1338        // and execute misses
1339        let member3_raw = deps.storage.get(&member_key(USER3));
1340        assert_eq!(None, member3_raw);
1341    }
1342
1343    #[track_caller]
1344    fn get_claims(
1345        deps: Deps<TgradeQuery>,
1346        addr: Addr,
1347        limit: Option<u32>,
1348        start_after: Option<Expiration>,
1349    ) -> Vec<Claim> {
1350        claims()
1351            .query_claims(deps, addr, limit, start_after)
1352            .unwrap()
1353    }
1354
1355    #[test]
1356    fn unbond_claim_workflow() {
1357        let mut deps = mock_deps_tgrade();
1358        default_instantiate(deps.as_mut());
1359
1360        // create some data
1361        bond(deps.as_mut(), (4_000, 7_500), (7_500, 0), (3_000, 1_000), 1);
1362        let height_delta = 2;
1363        // 4_000 (liquid) and 500 (vesting) will be claimed for USER1
1364        unbond(deps.as_mut(), 4_500, 2_600, 0, height_delta, 0);
1365        let mut env = mock_env();
1366        env.block.height += height_delta;
1367
1368        // check the claims for each user
1369        let expires = Duration::new(UNBONDING_DURATION).after(&env.block);
1370        assert_eq!(
1371            get_claims(deps.as_ref(), Addr::unchecked(USER1), None, None),
1372            vec![Claim::new(
1373                Addr::unchecked(USER1),
1374                4_000,
1375                500,
1376                expires,
1377                env.block.height,
1378            )]
1379        );
1380        assert_eq!(
1381            get_claims(deps.as_ref(), Addr::unchecked(USER2), None, None),
1382            vec![Claim::new(
1383                Addr::unchecked(USER2),
1384                2_600,
1385                0,
1386                expires,
1387                env.block.height,
1388            )]
1389        );
1390        assert_eq!(
1391            get_claims(deps.as_ref(), Addr::unchecked(USER3), None, None),
1392            vec![]
1393        );
1394
1395        // do another unbond later on
1396        let mut env2 = mock_env();
1397        let height_delta = 22;
1398        env2.block.height += height_delta;
1399        let time_delta = 50;
1400        unbond(deps.as_mut(), 0, 1_345, 1_500, height_delta, time_delta);
1401
1402        // with updated claims
1403        let expires2 = Duration::new(UNBONDING_DURATION + time_delta).after(&env2.block);
1404        assert_ne!(expires, expires2);
1405        assert_eq!(
1406            get_claims(deps.as_ref(), Addr::unchecked(USER1), None, None),
1407            vec![Claim::new(
1408                Addr::unchecked(USER1),
1409                4_000,
1410                500,
1411                expires,
1412                env.block.height,
1413            )]
1414        );
1415        assert_eq!(
1416            get_claims(deps.as_ref(), Addr::unchecked(USER2), None, None),
1417            vec![
1418                Claim::new(Addr::unchecked(USER2), 2_600, 0, expires, env.block.height),
1419                Claim::new(
1420                    Addr::unchecked(USER2),
1421                    1_345,
1422                    0,
1423                    expires2,
1424                    env2.block.height,
1425                ),
1426            ]
1427        );
1428        assert_eq!(
1429            get_claims(deps.as_ref(), Addr::unchecked(USER3), None, None),
1430            vec![Claim::new(
1431                Addr::unchecked(USER3),
1432                1_500,
1433                0,
1434                expires2,
1435                env2.block.height,
1436            )]
1437        );
1438
1439        // nothing can be withdrawn yet
1440        let err = execute(
1441            deps.as_mut(),
1442            env,
1443            mock_info(USER1, &[]),
1444            ExecuteMsg::Claim {},
1445        )
1446        .unwrap_err();
1447        assert_eq!(err, ContractError::NothingToClaim {});
1448
1449        // now mature first section, withdraw that
1450        let mut env3 = mock_env();
1451        env3.block.time = env3.block.time.plus_seconds(UNBONDING_DURATION);
1452        // first one can now release
1453        let res = execute(
1454            deps.as_mut(),
1455            env3.clone(),
1456            mock_info(USER1, &[]),
1457            ExecuteMsg::Claim {},
1458        )
1459        .unwrap();
1460        assert_eq!(
1461            res.messages,
1462            vec![
1463                SubMsg::new(BankMsg::Send {
1464                    to_address: USER1.into(),
1465                    amount: coins(4_000, DENOM),
1466                }),
1467                SubMsg::new(TgradeMsg::Undelegate {
1468                    funds: coin(500, DENOM),
1469                    recipient: USER1.into(),
1470                })
1471            ]
1472        );
1473
1474        // second releases partially
1475        let res = execute(
1476            deps.as_mut(),
1477            env3.clone(),
1478            mock_info(USER2, &[]),
1479            ExecuteMsg::Claim {},
1480        )
1481        .unwrap();
1482        assert_eq!(
1483            res.messages,
1484            vec![SubMsg::new(BankMsg::Send {
1485                to_address: USER2.into(),
1486                amount: coins(2_600, DENOM),
1487            })]
1488        );
1489
1490        // but the third one cannot release
1491        let err = execute(
1492            deps.as_mut(),
1493            env3,
1494            mock_info(USER3, &[]),
1495            ExecuteMsg::Claim {},
1496        )
1497        .unwrap_err();
1498        assert_eq!(err, ContractError::NothingToClaim {});
1499
1500        // claims updated properly
1501        assert_eq!(
1502            get_claims(deps.as_ref(), Addr::unchecked(USER1), None, None),
1503            vec![]
1504        );
1505        assert_eq!(
1506            get_claims(deps.as_ref(), Addr::unchecked(USER2), None, None),
1507            vec![Claim::new(
1508                Addr::unchecked(USER2),
1509                1_345,
1510                0,
1511                expires2,
1512                env2.block.height,
1513            )]
1514        );
1515        assert_eq!(
1516            get_claims(deps.as_ref(), Addr::unchecked(USER3), None, None),
1517            vec![Claim::new(
1518                Addr::unchecked(USER3),
1519                1_500,
1520                0,
1521                expires2,
1522                env2.block.height,
1523            )]
1524        );
1525
1526        // add another few claims for 2
1527        unbond(deps.as_mut(), 0, 600, 0, 30, 0);
1528        unbond(deps.as_mut(), 0, 1_005, 0, 50, 0);
1529
1530        // ensure second can claim all tokens at once
1531        let mut env4 = mock_env();
1532        env4.block.time = env4
1533            .block
1534            .time
1535            .plus_seconds(UNBONDING_DURATION + time_delta);
1536        let res = execute(
1537            deps.as_mut(),
1538            env4,
1539            mock_info(USER2, &[]),
1540            ExecuteMsg::Claim {},
1541        )
1542        .unwrap();
1543        assert_eq!(
1544            res.messages,
1545            vec![SubMsg::new(BankMsg::Send {
1546                to_address: USER2.into(),
1547                // 1_345 + 600 + 1_005
1548                amount: coins(2_950, DENOM),
1549            })]
1550        );
1551        assert_eq!(
1552            get_claims(deps.as_ref(), Addr::unchecked(USER2), None, None),
1553            vec![]
1554        );
1555    }
1556
1557    #[test]
1558    fn add_remove_hooks() {
1559        // add will over-write and remove have no effect
1560        let mut deps = mock_deps_tgrade();
1561        default_instantiate(deps.as_mut());
1562
1563        let hooks = HOOKS.list_hooks(&deps.storage).unwrap();
1564        assert!(hooks.is_empty());
1565
1566        let contract1 = String::from("hook1");
1567        let contract2 = String::from("hook2");
1568
1569        let add_msg = ExecuteMsg::AddHook {
1570            addr: contract1.clone(),
1571        };
1572
1573        // anyone can add the first one, until preauth is consume
1574        assert_eq!(1, PREAUTH_HOOKS.get_auth(&deps.storage).unwrap());
1575        let user_info = mock_info(USER1, &[]);
1576        let _ = execute(deps.as_mut(), mock_env(), user_info, add_msg.clone()).unwrap();
1577        let hooks = HOOKS.list_hooks(&deps.storage).unwrap();
1578        assert_eq!(hooks, vec![contract1.clone()]);
1579
1580        // non-admin cannot add hook without preauth
1581        assert_eq!(0, PREAUTH_HOOKS.get_auth(&deps.storage).unwrap());
1582        let user_info = mock_info(USER1, &[]);
1583        let err = execute(
1584            deps.as_mut(),
1585            mock_env(),
1586            user_info.clone(),
1587            add_msg.clone(),
1588        )
1589        .unwrap_err();
1590        assert_eq!(err, PreauthError::NoPreauth {}.into());
1591
1592        // cannot remove a non-registered contract
1593        let admin_info = mock_info(INIT_ADMIN, &[]);
1594        let remove_msg = ExecuteMsg::RemoveHook {
1595            addr: contract2.clone(),
1596        };
1597        let err = execute(deps.as_mut(), mock_env(), admin_info.clone(), remove_msg).unwrap_err();
1598        assert_eq!(err, HookError::HookNotRegistered {}.into());
1599
1600        // admin can second contract, and it appears in the query
1601        let add_msg2 = ExecuteMsg::AddHook {
1602            addr: contract2.clone(),
1603        };
1604        execute(deps.as_mut(), mock_env(), admin_info.clone(), add_msg2).unwrap();
1605        let hooks = HOOKS.list_hooks(&deps.storage).unwrap();
1606        assert_eq!(hooks, vec![contract1.clone(), contract2.clone()]);
1607
1608        // cannot re-add an existing contract
1609        let err = execute(deps.as_mut(), mock_env(), admin_info.clone(), add_msg).unwrap_err();
1610        assert_eq!(err, HookError::HookAlreadyRegistered {}.into());
1611
1612        // non-admin cannot remove
1613        let remove_msg = ExecuteMsg::RemoveHook { addr: contract1 };
1614        let err = execute(deps.as_mut(), mock_env(), user_info, remove_msg.clone()).unwrap_err();
1615        assert_eq!(
1616            err,
1617            ContractError::Unauthorized(
1618                "Hook address is not same as sender's and sender is not an admin".to_owned()
1619            )
1620        );
1621
1622        // remove the original
1623        execute(deps.as_mut(), mock_env(), admin_info, remove_msg).unwrap();
1624        let hooks = HOOKS.list_hooks(&deps.storage).unwrap();
1625        assert_eq!(hooks, vec![contract2.clone()]);
1626
1627        // contract can self-remove
1628        let contract_info = mock_info(&contract2, &[]);
1629        let remove_msg2 = ExecuteMsg::RemoveHook { addr: contract2 };
1630        execute(deps.as_mut(), mock_env(), contract_info, remove_msg2).unwrap();
1631        let hooks = HOOKS.list_hooks(&deps.storage).unwrap();
1632        assert_eq!(hooks, Vec::<String>::new());
1633    }
1634
1635    mod slash {
1636        use super::*;
1637
1638        fn query_is_slasher(deps: Deps<TgradeQuery>, env: Env, addr: String) -> StdResult<bool> {
1639            let msg = QueryMsg::IsSlasher { addr };
1640            let raw = query(deps, env, msg)?;
1641            let is_slasher: bool = from_slice(&raw)?;
1642            Ok(is_slasher)
1643        }
1644
1645        fn query_list_slashers(deps: Deps<TgradeQuery>, env: Env) -> StdResult<Vec<String>> {
1646            let msg = QueryMsg::ListSlashers {};
1647            let raw = query(deps, env, msg)?;
1648            let slashers: Vec<String> = from_slice(&raw)?;
1649            Ok(slashers)
1650        }
1651
1652        fn add_slasher(deps: DepsMut<TgradeQuery>) -> String {
1653            let slasher = String::from("slasher");
1654            let add_msg = ExecuteMsg::AddSlasher {
1655                addr: slasher.clone(),
1656            };
1657            let user_info = mock_info(USER1, &[]);
1658            execute(deps, mock_env(), user_info, add_msg).unwrap();
1659
1660            slasher
1661        }
1662
1663        fn remove_slasher(deps: DepsMut<TgradeQuery>, slasher: &str) {
1664            let add_msg = ExecuteMsg::RemoveSlasher {
1665                addr: slasher.to_string(),
1666            };
1667            let user_info = mock_info(INIT_ADMIN, &[]);
1668            execute(deps, mock_env(), user_info, add_msg).unwrap();
1669        }
1670
1671        fn slash(
1672            deps: DepsMut<TgradeQuery>,
1673            slasher: &str,
1674            addr: &str,
1675            portion: Decimal,
1676        ) -> Result<Response, ContractError> {
1677            let msg = ExecuteMsg::Slash {
1678                addr: addr.to_string(),
1679                portion,
1680            };
1681            let slasher_info = mock_info(slasher, &[]);
1682
1683            execute(deps, mock_env(), slasher_info, msg)
1684        }
1685
1686        fn assert_burned(res: Response, expected_liquid: &[Coin], expected_vesting: &[Coin]) {
1687            // Args checks for robustness
1688            assert!(expected_liquid.len() <= 1);
1689            assert!(expected_vesting.len() <= 1);
1690
1691            // Find all instances of BankMsg::Burn in the response and extract the burned amounts
1692            let burned_amounts: Vec<_> = res
1693                .messages
1694                .iter()
1695                .filter_map(|sub_msg| match &sub_msg.msg {
1696                    CosmosMsg::Bank(BankMsg::Burn { amount }) => Some(amount),
1697                    _ => None,
1698                })
1699                .collect();
1700
1701            assert!(
1702                burned_amounts.len() == 1 || burned_amounts.len() == 2,
1703                "Expected exactly 1 or 2 Bank::Burn message, got {}",
1704                burned_amounts.len()
1705            );
1706
1707            let mut index = 0;
1708            if !expected_liquid.is_empty() {
1709                assert_eq!(
1710                    burned_amounts[index], &expected_liquid,
1711                    "Expected to burn {} liquid, burned {}",
1712                    expected_liquid[0], burned_amounts[index][0]
1713                );
1714                index += 1;
1715            }
1716            if !expected_vesting.is_empty() {
1717                assert_eq!(
1718                    burned_amounts[index], &expected_vesting,
1719                    "Expected to burn {} vesting, burned {}",
1720                    expected_liquid[0], burned_amounts[index][0]
1721                );
1722            }
1723        }
1724
1725        #[test]
1726        fn add_remove_slashers() {
1727            let mut deps = mock_deps_tgrade();
1728            let env = mock_env();
1729            default_instantiate(deps.as_mut());
1730
1731            let slashers = query_list_slashers(deps.as_ref(), env.clone()).unwrap();
1732            assert!(slashers.is_empty());
1733
1734            let contract1 = String::from("slasher1");
1735            let contract2 = String::from("slasher2");
1736
1737            let add_msg = ExecuteMsg::AddSlasher {
1738                addr: contract1.clone(),
1739            };
1740
1741            // anyone can add the first one, until preauth is consumed
1742            assert_eq!(1, PREAUTH_SLASHING.get_auth(&deps.storage).unwrap());
1743            let user_info = mock_info(USER1, &[]);
1744            let _ = execute(deps.as_mut(), mock_env(), user_info, add_msg.clone()).unwrap();
1745            let slashers = query_list_slashers(deps.as_ref(), env.clone()).unwrap();
1746            assert_eq!(slashers, vec![contract1.clone()]);
1747
1748            // non-admin cannot add slasher without preauth
1749            assert_eq!(0, PREAUTH_SLASHING.get_auth(&deps.storage).unwrap());
1750            let user_info = mock_info(USER1, &[]);
1751            let err = execute(
1752                deps.as_mut(),
1753                mock_env(),
1754                user_info.clone(),
1755                add_msg.clone(),
1756            )
1757            .unwrap_err();
1758            assert_eq!(err, PreauthError::NoPreauth {}.into());
1759
1760            // cannot remove a non-registered slasher
1761            let admin_info = mock_info(INIT_ADMIN, &[]);
1762            let remove_msg = ExecuteMsg::RemoveSlasher {
1763                addr: contract2.clone(),
1764            };
1765            let err =
1766                execute(deps.as_mut(), mock_env(), admin_info.clone(), remove_msg).unwrap_err();
1767            assert_eq!(
1768                err,
1769                ContractError::Slasher(SlasherError::SlasherNotRegistered(contract2.clone()))
1770            );
1771
1772            // admin can add a second slasher, and it appears in the query
1773            let add_msg2 = ExecuteMsg::AddSlasher {
1774                addr: contract2.clone(),
1775            };
1776            execute(deps.as_mut(), mock_env(), admin_info.clone(), add_msg2).unwrap();
1777            let slashers = query_list_slashers(deps.as_ref(), env.clone()).unwrap();
1778            assert_eq!(slashers, vec![contract1.clone(), contract2.clone()]);
1779
1780            // cannot re-add an existing contract
1781            let err = execute(deps.as_mut(), mock_env(), admin_info.clone(), add_msg).unwrap_err();
1782            assert_eq!(
1783                err,
1784                ContractError::Slasher(SlasherError::SlasherAlreadyRegistered(contract1.clone()))
1785            );
1786
1787            // non-admin cannot remove
1788            let remove_msg = ExecuteMsg::RemoveSlasher { addr: contract1 };
1789            let err =
1790                execute(deps.as_mut(), mock_env(), user_info, remove_msg.clone()).unwrap_err();
1791            assert_eq!(
1792                err,
1793                ContractError::Unauthorized(
1794                    "Only slasher might remove himself and sender is not an admin".to_owned()
1795                )
1796            );
1797
1798            // remove the original
1799            execute(deps.as_mut(), mock_env(), admin_info, remove_msg).unwrap();
1800            let slashers = query_list_slashers(deps.as_ref(), env.clone()).unwrap();
1801            assert_eq!(slashers, vec![contract2.clone()]);
1802
1803            // contract can self-remove
1804            let contract_info = mock_info(&contract2, &[]);
1805            let remove_msg2 = ExecuteMsg::RemoveSlasher { addr: contract2 };
1806            execute(deps.as_mut(), mock_env(), contract_info, remove_msg2).unwrap();
1807            let slashers = query_list_slashers(deps.as_ref(), env).unwrap();
1808            assert_eq!(slashers, Vec::<String>::new());
1809        }
1810
1811        #[test]
1812        fn slashing_nonexisting_member() {
1813            let mut deps = mock_deps_tgrade();
1814            default_instantiate(deps.as_mut());
1815
1816            // confirm address doesn't return true on slasher query
1817            assert!(!query_is_slasher(deps.as_ref(), mock_env(), "slasher".to_owned()).unwrap());
1818
1819            let slasher = add_slasher(deps.as_mut());
1820            assert!(query_is_slasher(deps.as_ref(), mock_env(), slasher.clone()).unwrap());
1821
1822            bond_liquid(deps.as_mut(), 12_000, 7_500, 4_000, 1);
1823            assert_stake_liquid(deps.as_ref(), 12_000, 7_500, 4_000);
1824
1825            bond_vesting(deps.as_mut(), 1_200, 750, 8_000, 2);
1826            assert_stake_vesting(deps.as_ref(), 1_200, 750, 8_000);
1827
1828            // Trying to slash nonexisting user will result in no-op
1829            let res = slash(deps.as_mut(), &slasher, "nonexisting", Decimal::percent(20)).unwrap();
1830            assert_eq!(res, Response::new());
1831        }
1832
1833        #[test]
1834        fn slashing_bonded_liquid_tokens_works() {
1835            let mut deps = mock_deps_tgrade();
1836            default_instantiate(deps.as_mut());
1837            let cfg = CONFIG.load(&deps.storage).unwrap();
1838            let slasher = add_slasher(deps.as_mut());
1839
1840            bond_liquid(deps.as_mut(), 12_000, 7_500, 4_000, 1);
1841            assert_stake_liquid(deps.as_ref(), 12_000, 7_500, 4_000);
1842
1843            // The slasher we added can slash
1844            let res1 = slash(deps.as_mut(), &slasher, USER1, Decimal::percent(20)).unwrap();
1845            let res2 = slash(deps.as_mut(), &slasher, USER3, Decimal::percent(50)).unwrap();
1846            assert_stake_liquid(deps.as_ref(), 9_600, 7_500, 2_000);
1847
1848            // Tokens are burned
1849            assert_burned(res1, &coins(2_400, &cfg.denom), &[]);
1850            assert_burned(res2, &coins(2_000, &cfg.denom), &[]);
1851        }
1852
1853        #[test]
1854        fn slashing_bonded_vesting_tokens_works() {
1855            let mut deps = mock_deps_tgrade();
1856            default_instantiate(deps.as_mut());
1857            let cfg = CONFIG.load(&deps.storage).unwrap();
1858            let slasher = add_slasher(deps.as_mut());
1859
1860            bond_vesting(deps.as_mut(), 12_000, 7_500, 4_000, 1);
1861            assert_stake_vesting(deps.as_ref(), 12_000, 7_500, 4_000);
1862
1863            // The slasher we added can slash
1864            let res1 = slash(deps.as_mut(), &slasher, USER1, Decimal::percent(20)).unwrap();
1865            let res2 = slash(deps.as_mut(), &slasher, USER3, Decimal::percent(50)).unwrap();
1866            assert_stake_vesting(deps.as_ref(), 9_600, 7_500, 2_000);
1867
1868            // Tokens are burned
1869            assert_burned(res1, &[], &coins(2_400, &cfg.denom));
1870            assert_burned(res2, &[], &coins(2_000, &cfg.denom));
1871        }
1872
1873        #[test]
1874        fn slashing_bonded_mixed_tokens_works() {
1875            let mut deps = mock_deps_tgrade();
1876            default_instantiate(deps.as_mut());
1877            let cfg = CONFIG.load(&deps.storage).unwrap();
1878            let slasher = add_slasher(deps.as_mut());
1879
1880            bond_liquid(deps.as_mut(), 12_000, 1_500, 0, 1);
1881            assert_stake_liquid(deps.as_ref(), 12_000, 1_500, 0);
1882            bond_vesting(deps.as_mut(), 0, 6_000, 4_000, 1);
1883            assert_stake_vesting(deps.as_ref(), 0, 6_000, 4_000);
1884
1885            // The slasher we added can slash
1886            let res1 = slash(deps.as_mut(), &slasher, USER1, Decimal::percent(20)).unwrap();
1887            let res2 = slash(deps.as_mut(), &slasher, USER3, Decimal::percent(50)).unwrap();
1888            let res3 = slash(deps.as_mut(), &slasher, USER2, Decimal::percent(10)).unwrap();
1889            assert_stake_liquid(deps.as_ref(), 9_600, 1_350, 0);
1890            assert_stake_vesting(deps.as_ref(), 0, 5_400, 2_000);
1891
1892            // Tokens are burned
1893            assert_burned(res1, &coins(2_400, &cfg.denom), &[]);
1894            assert_burned(res2, &[], &coins(2_000, &cfg.denom));
1895            assert_burned(res3, &coins(150, &cfg.denom), &coins(600, &cfg.denom));
1896        }
1897
1898        #[test]
1899        fn slashing_stake_update_membership() {
1900            let mut deps = mock_deps_tgrade();
1901            default_instantiate(deps.as_mut());
1902            let slasher = add_slasher(deps.as_mut());
1903
1904            // ensure it rounds down, and respects cut-off
1905            bond(deps.as_mut(), (0, 12_000), (7_000, 0), (3_000, 4_000), 1);
1906            assert_users(deps.as_ref(), Some(12), Some(7), Some(7), None);
1907
1908            slash(deps.as_mut(), &slasher, USER1, Decimal::percent(50)).unwrap();
1909            slash(deps.as_mut(), &slasher, USER2, Decimal::percent(10)).unwrap();
1910            slash(deps.as_mut(), &slasher, USER3, Decimal::percent(20)).unwrap();
1911
1912            // Assert updated points
1913            assert_stake_liquid(deps.as_ref(), 0, 6_300, 2_400);
1914            assert_stake_vesting(deps.as_ref(), 6_000, 0, 3_200);
1915            assert_users(deps.as_ref(), Some(6), Some(6), Some(5), None);
1916        }
1917
1918        #[test]
1919        fn slashing_claims_works() {
1920            let mut deps = mock_deps_tgrade();
1921            default_instantiate(deps.as_mut());
1922            let cfg = CONFIG.load(&deps.storage).unwrap();
1923            let slasher = add_slasher(deps.as_mut());
1924
1925            // create some data
1926            bond_liquid(deps.as_mut(), 12_000, 7_500, 4_000, 1);
1927            bond_vesting(deps.as_mut(), 1_000, 750, 40, 1);
1928            let height_delta = 2;
1929            unbond(deps.as_mut(), 13_000, 2_600, 0, height_delta, 0);
1930            let mut env = mock_env();
1931            env.block.height += height_delta;
1932
1933            // check the claims for each user
1934            let expires = Duration::new(UNBONDING_DURATION).after(&env.block);
1935            assert_eq!(
1936                get_claims(deps.as_ref(), Addr::unchecked(USER1), None, None),
1937                vec![Claim::new(
1938                    Addr::unchecked(USER1),
1939                    12_000,
1940                    1_000,
1941                    expires,
1942                    env.block.height,
1943                )]
1944            );
1945
1946            let res = slash(deps.as_mut(), &slasher, USER1, Decimal::percent(20)).unwrap();
1947
1948            assert_eq!(
1949                get_claims(deps.as_ref(), Addr::unchecked(USER1), None, None),
1950                vec![Claim::new(
1951                    Addr::unchecked(USER1),
1952                    9_600,
1953                    800,
1954                    expires,
1955                    env.block.height,
1956                )]
1957            );
1958            assert_burned(res, &coins(2_400, &cfg.denom), &coins(200, &cfg.denom));
1959        }
1960
1961        #[test]
1962        fn random_user_cannot_slash() {
1963            let mut deps = mock_deps_tgrade();
1964            default_instantiate(deps.as_mut());
1965            let _slasher = add_slasher(deps.as_mut());
1966
1967            bond_liquid(deps.as_mut(), 12_000, 7_500, 4_000, 1);
1968            assert_stake_liquid(deps.as_ref(), 12_000, 7_500, 4_000);
1969
1970            bond_vesting(deps.as_mut(), 1_200, 750, 8_000, 2);
1971            assert_stake_vesting(deps.as_ref(), 1_200, 750, 8_000);
1972
1973            let res = slash(deps.as_mut(), USER2, USER1, Decimal::percent(20));
1974            assert_eq!(
1975                res,
1976                Err(ContractError::Unauthorized(
1977                    "Sender is not on slashers list".to_owned()
1978                ))
1979            );
1980            assert_stake_liquid(deps.as_ref(), 12_000, 7_500, 4_000);
1981        }
1982
1983        #[test]
1984        fn admin_cannot_slash() {
1985            let mut deps = mock_deps_tgrade();
1986            default_instantiate(deps.as_mut());
1987            let _slasher = add_slasher(deps.as_mut());
1988
1989            bond_liquid(deps.as_mut(), 12_000, 7_500, 4_000, 1);
1990            assert_stake_liquid(deps.as_ref(), 12_000, 7_500, 4_000);
1991
1992            bond_vesting(deps.as_mut(), 1_200, 750, 8_000, 2);
1993            assert_stake_vesting(deps.as_ref(), 1_200, 750, 8_000);
1994
1995            let res = slash(deps.as_mut(), INIT_ADMIN, USER1, Decimal::percent(20));
1996            assert_eq!(
1997                res,
1998                Err(ContractError::Unauthorized(
1999                    "Sender is not on slashers list".to_owned()
2000                ))
2001            );
2002            assert_stake_liquid(deps.as_ref(), 12_000, 7_500, 4_000);
2003        }
2004
2005        #[test]
2006        fn removed_slasher_cannot_slash() {
2007            let mut deps = mock_deps_tgrade();
2008            default_instantiate(deps.as_mut());
2009
2010            // Add, then remove a slasher
2011            let slasher = add_slasher(deps.as_mut());
2012            remove_slasher(deps.as_mut(), &slasher);
2013
2014            bond_liquid(deps.as_mut(), 12_000, 7_500, 4_000, 1);
2015            assert_stake_liquid(deps.as_ref(), 12_000, 7_500, 4_000);
2016
2017            bond_vesting(deps.as_mut(), 12_000, 7_500, 8_000, 2);
2018            assert_stake_vesting(deps.as_ref(), 12_000, 7_500, 8_000);
2019
2020            let res = slash(deps.as_mut(), &slasher, USER1, Decimal::percent(20));
2021            assert_eq!(
2022                res,
2023                Err(ContractError::Unauthorized(
2024                    "Sender is not on slashers list".to_owned()
2025                ))
2026            );
2027            assert_stake_liquid(deps.as_ref(), 12_000, 7_500, 4_000);
2028        }
2029    }
2030
2031    #[test]
2032    fn hooks_fire() {
2033        let mut deps = mock_deps_tgrade();
2034        default_instantiate(deps.as_mut());
2035
2036        let hooks = HOOKS.list_hooks(&deps.storage).unwrap();
2037        assert!(hooks.is_empty());
2038
2039        let contract1 = String::from("hook1");
2040        let contract2 = String::from("hook2");
2041
2042        // register 2 hooks
2043        let admin_info = mock_info(INIT_ADMIN, &[]);
2044        let add_msg = ExecuteMsg::AddHook {
2045            addr: contract1.clone(),
2046        };
2047        let add_msg2 = ExecuteMsg::AddHook {
2048            addr: contract2.clone(),
2049        };
2050        for msg in vec![add_msg, add_msg2] {
2051            let _ = execute(deps.as_mut(), mock_env(), admin_info.clone(), msg).unwrap();
2052        }
2053
2054        // check firing on bond
2055        assert_users(deps.as_ref(), None, None, None, None);
2056        let info = mock_info(USER1, &coins(13_800, DENOM));
2057        let res = execute(
2058            deps.as_mut(),
2059            mock_env(),
2060            info,
2061            ExecuteMsg::Bond {
2062                vesting_tokens: None,
2063            },
2064        )
2065        .unwrap();
2066        assert_users(deps.as_ref(), Some(13), None, None, None);
2067
2068        // ensure messages for each of the 2 hooks
2069        assert_eq!(res.messages.len(), 2);
2070        let diff = MemberDiff::new(USER1, None, Some(13));
2071        let hook_msg = MemberChangedHookMsg::one(diff);
2072        let msg1 = hook_msg
2073            .clone()
2074            .into_cosmos_msg(contract1.clone())
2075            .map(SubMsg::new)
2076            .unwrap();
2077        let msg2 = hook_msg
2078            .into_cosmos_msg(contract2.clone())
2079            .map(SubMsg::new)
2080            .unwrap();
2081        assert_eq!(res.messages, vec![msg1, msg2]);
2082
2083        // check firing on unbond
2084        let msg = ExecuteMsg::Unbond {
2085            tokens: coin(7_300, DENOM),
2086        };
2087        let info = mock_info(USER1, &[]);
2088        let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap();
2089        assert_users(deps.as_ref(), Some(6), None, None, None);
2090
2091        // ensure messages for each of the 2 hooks
2092        assert_eq!(res.messages.len(), 2);
2093        let diff = MemberDiff::new(USER1, Some(13), Some(6));
2094        let hook_msg = MemberChangedHookMsg::one(diff);
2095        let msg1 = hook_msg
2096            .clone()
2097            .into_cosmos_msg(contract1)
2098            .map(SubMsg::new)
2099            .unwrap();
2100        let msg2 = hook_msg
2101            .into_cosmos_msg(contract2)
2102            .map(SubMsg::new)
2103            .unwrap();
2104        assert_eq!(res.messages, vec![msg1, msg2]);
2105    }
2106
2107    #[test]
2108    fn only_bond_valid_coins() {
2109        let mut deps = mock_deps_tgrade();
2110        default_instantiate(deps.as_mut());
2111
2112        // cannot bond with 0 coins
2113        let info = mock_info(USER1, &[]);
2114        let err = execute(
2115            deps.as_mut(),
2116            mock_env(),
2117            info,
2118            ExecuteMsg::Bond {
2119                vesting_tokens: None,
2120            },
2121        )
2122        .unwrap_err();
2123        assert_eq!(err, ContractError::NoFunds {});
2124
2125        // cannot bond with incorrect denom
2126        let info = mock_info(USER1, &[coin(500, "FOO")]);
2127        let err = execute(
2128            deps.as_mut(),
2129            mock_env(),
2130            info,
2131            ExecuteMsg::Bond {
2132                vesting_tokens: None,
2133            },
2134        )
2135        .unwrap_err();
2136        assert_eq!(err, ContractError::MissingDenom(DENOM.to_string()));
2137
2138        // cannot bond with 2 coins (even if one is correct)
2139        let info = mock_info(USER1, &[coin(1234, DENOM), coin(5000, "BAR")]);
2140        let err = execute(
2141            deps.as_mut(),
2142            mock_env(),
2143            info,
2144            ExecuteMsg::Bond {
2145                vesting_tokens: None,
2146            },
2147        )
2148        .unwrap_err();
2149        assert_eq!(err, ContractError::ExtraDenoms(DENOM.to_string()));
2150
2151        // can bond with just the proper denom
2152        // cannot bond with incorrect denom
2153        let info = mock_info(USER1, &[coin(500, DENOM)]);
2154        execute(
2155            deps.as_mut(),
2156            mock_env(),
2157            info,
2158            ExecuteMsg::Bond {
2159                vesting_tokens: None,
2160            },
2161        )
2162        .unwrap();
2163    }
2164
2165    #[test]
2166    fn ensure_bonding_edge_cases_liquid() {
2167        // use min_bond 0, tokens_per_points 100
2168        let mut deps = mock_deps_tgrade();
2169        do_instantiate(deps.as_mut(), Uint128::new(100), Uint128::zero(), 5, 0);
2170
2171        // setting 50 tokens, gives us Some(0) points
2172        // even setting to 1 token
2173        bond_liquid(deps.as_mut(), 50, 1, 102, 1);
2174        assert_users(deps.as_ref(), Some(0), Some(0), Some(1), None);
2175
2176        // reducing to 0 token makes us None even with min_bond 0
2177        unbond(deps.as_mut(), 49, 1, 102, 2, 0);
2178        assert_users(deps.as_ref(), Some(0), None, None, None);
2179    }
2180
2181    #[test]
2182    fn ensure_bonding_edge_cases_vesting() {
2183        // use min_bond 0, tokens_per_points 100
2184        let mut deps = mock_deps_tgrade();
2185        do_instantiate(deps.as_mut(), Uint128::new(100), Uint128::zero(), 5, 0);
2186
2187        // setting 50 tokens, gives us Some(0) points
2188        // even setting to 1 token
2189        bond_vesting(deps.as_mut(), 50, 1, 102, 1);
2190        assert_users(deps.as_ref(), Some(0), Some(0), Some(1), None);
2191
2192        // reducing to 0 token makes us None even with min_bond 0
2193        unbond(deps.as_mut(), 49, 1, 102, 2, 0);
2194        assert_users(deps.as_ref(), Some(0), None, None, None);
2195    }
2196
2197    #[test]
2198    fn ensure_bonding_edge_cases_mixed() {
2199        // use min_bond 0, tokens_per_points 100
2200        let mut deps = mock_deps_tgrade();
2201        do_instantiate(deps.as_mut(), Uint128::new(100), Uint128::zero(), 5, 0);
2202
2203        // setting 25 liquid tokens, gives us Some(0) points
2204        // even setting to 1 token
2205        bond_liquid(deps.as_mut(), 25, 1, 102, 1);
2206        assert_users(deps.as_ref(), Some(0), Some(0), Some(1), None);
2207
2208        // setting other 25 vesting tokens, still gives us Some(0) points
2209        // also setting to plus 1 token
2210        bond_vesting(deps.as_mut(), 25, 1, 102, 2);
2211        assert_users(deps.as_ref(), Some(0), Some(0), Some(2), None);
2212
2213        // reducing to 0 token makes us None even with min_bond 0
2214        unbond(deps.as_mut(), 49, 2, 204, 3, 0);
2215        assert_users(deps.as_ref(), Some(0), None, None, None);
2216    }
2217
2218    #[test]
2219    fn paginated_claim_query() {
2220        let mut deps = mock_deps_tgrade();
2221        default_instantiate(deps.as_mut());
2222
2223        // create some data
2224        let mut env = mock_env();
2225        let msg = ExecuteMsg::Bond {
2226            vesting_tokens: None,
2227        };
2228        let info = mock_info(USER1, &coins(500, DENOM));
2229        execute(deps.as_mut(), env.clone(), info, msg).unwrap();
2230
2231        let info = mock_info(USER1, &[]);
2232        for _ in 0..10 {
2233            env.block.time = env.block.time.plus_seconds(10);
2234            let msg = ExecuteMsg::Unbond {
2235                tokens: coin(10, DENOM),
2236            };
2237            execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();
2238        }
2239
2240        // check is number of claims is properly limited
2241        let claims = get_claims(deps.as_ref(), Addr::unchecked(USER1), Some(6), None);
2242        assert_eq!(claims.len(), 6);
2243        // check if rest is equal to remainder
2244        let next = get_claims(
2245            deps.as_ref(),
2246            Addr::unchecked(USER1),
2247            None,
2248            Some(claims[5].release_at),
2249        );
2250        assert_eq!(next.len(), 4);
2251
2252        // check if joining and sorting both vectors equal number from start
2253        let mut all_claims = get_claims(deps.as_ref(), Addr::unchecked(USER1), None, None);
2254        all_claims.sort_by_key(|claim| claim.addr.clone());
2255
2256        let mut concatenated = [claims, next].concat();
2257        concatenated.sort_by_key(|claim| claim.addr.clone());
2258        assert_eq!(concatenated, all_claims);
2259    }
2260
2261    mod auto_release_claims {
2262        // Because of tests framework limitations at the point of implementing this test, it is
2263        // difficult to actually test reaction for tgrade sudo messages. Instead to check the
2264        // auto-release functionality, there are assumptions made:
2265        // * Registration to sudo events is correct
2266        // * Auto-releasing claims occurs on sudo EndBlock message, and the message is purely
2267        // calling the `end_block` function - calling this function in test is simulating actual
2268        // end block
2269
2270        use cosmwasm_std::CosmosMsg;
2271
2272        use super::*;
2273
2274        fn do_instantiate(deps: DepsMut<TgradeQuery>, limit: u64) {
2275            super::do_instantiate(deps, TOKENS_PER_POINT, MIN_BOND, UNBONDING_DURATION, limit)
2276        }
2277
2278        /// Helper for asserting if expected transfers occurred in response. Panics if any non
2279        /// `BankMsg::Send` occurred, or transfers are different than expected.
2280        ///
2281        /// Transfers are passed in form of pairs `(addr, amount)`, as for all test in this module
2282        /// expected denom is fixed
2283        #[track_caller]
2284        fn assert_sends(response: Response, mut expected_transfers: Vec<(&str, u128)>) {
2285            let mut sends: Vec<_> = response
2286                .messages
2287                .into_iter()
2288                .map(|msg| match msg.msg {
2289                    // Trick is used here - bank send messages are filtered out, and mapped to triple
2290                    // `(addr, amount_sum, msg)` - `addr` and `amount_sum` would be used only to
2291                    // properly sort messages, then they would be discarded. As in expected messages
2292                    // always only one coin is expected for all send messages, taking sum for sorting
2293                    // is good enough - in case of multiple of invalid denoms it would be visible on
2294                    // comparison.
2295                    //
2296                    // Possibly in future it would be possible for another messages to occur - in such
2297                    // case instead of returning err and panicking from this function, such messages
2298                    // should be filtered out.
2299                    CosmosMsg::Bank(BankMsg::Send { to_address, amount }) => Ok((
2300                        to_address.clone(),
2301                        amount.iter().map(|c| c.amount).sum::<Uint128>(),
2302                        BankMsg::Send { to_address, amount },
2303                    )),
2304                    msg => Err(format!(
2305                        "Unexpected message on response, expected only bank send messages: {:?}",
2306                        msg
2307                    )),
2308                })
2309                .collect::<Result<_, _>>()
2310                .unwrap();
2311
2312            sends.sort_by_key(|(addr, amount_sum, _)| (addr.clone(), *amount_sum));
2313            // Drop  addr and amount_sum for comparison
2314            let sends: Vec<_> = sends.into_iter().map(|(_, _, msg)| msg).collect();
2315
2316            // Tuples are sorted simply first by addresses, then by amount
2317            expected_transfers.sort_unstable();
2318
2319            // Build messages for comparison
2320            let expected_transfers: Vec<_> = expected_transfers
2321                .into_iter()
2322                .map(|(addr, amount)| BankMsg::Send {
2323                    to_address: addr.to_owned(),
2324                    amount: coins(amount, DENOM),
2325                })
2326                .collect();
2327
2328            assert_eq!(sends, expected_transfers);
2329        }
2330
2331        /// Helper for asserting if expected undelegates occurred in response. Panics if any non
2332        /// `TgradeMsg::Undelegate` occurred, or undelegates are different than expected.
2333        ///
2334        /// Transfers are passed in form of pairs `(addr, amount)`, as for all tests in this module
2335        /// the expected denom is fixed.
2336        #[track_caller]
2337        fn assert_undelegates(response: Response, mut expected_transfers: Vec<(&str, u128)>) {
2338            let mut undelegates: Vec<_> = response
2339                .messages
2340                .into_iter()
2341                .map(|msg| match msg.msg {
2342                    // Trick is used here - undelegate messages are filtered out, and mapped to a triple
2343                    // `(addr, amount, msg)` - `addr` and `amount` will be used only to properly
2344                    // sort messages, then they will be discarded. As in the expected messages
2345                    // always only one coin is expected for all messages, taking the amount for sorting
2346                    // is good enough - in the case of multiple or invalid denoms, it'll error on
2347                    // comparison.
2348                    //
2349                    // Possibly in the future it would be possible for another messages to occur -
2350                    // in such case instead of returning err and panicking from this function, such
2351                    // messages should be filtered out.
2352                    CosmosMsg::Custom(TgradeMsg::Undelegate { funds, recipient }) => Ok((
2353                        recipient.clone(),
2354                        funds.amount,
2355                        TgradeMsg::Undelegate { recipient, funds },
2356                    )),
2357                    msg => Err(format!(
2358                        "Unexpected message on response, expected only tgrade undelegate messages: {:?}",
2359                        msg
2360                    )),
2361                })
2362                .collect::<Result<_, _>>()
2363                .unwrap();
2364
2365            undelegates.sort_by_key(|(addr, amount, _)| (addr.clone(), *amount));
2366            // Drop  addr and amount for comparison
2367            let undelegates: Vec<_> = undelegates.into_iter().map(|(_, _, msg)| msg).collect();
2368
2369            // Tuples are sorted simply first by addresses, then by amounts
2370            expected_transfers.sort_unstable();
2371
2372            // Build messages for comparison
2373            let expected_transfers: Vec<_> = expected_transfers
2374                .into_iter()
2375                .map(|(addr, amount)| TgradeMsg::Undelegate {
2376                    funds: coin(amount, DENOM),
2377                    recipient: addr.to_owned(),
2378                })
2379                .collect();
2380
2381            assert_eq!(undelegates, expected_transfers);
2382        }
2383
2384        /// Helper for asserting if expected sends and undelegates occurred in response. Panics if
2385        /// any non `Bank::Send` or `TgradeMsg::Undelegate` occurred, or send / undelegates are
2386        /// different than expected.
2387        ///
2388        /// Transfers are passed in form of pairs `(addr, amount)`, as for all tests in this module
2389        /// the expected denom is fixed.
2390        #[track_caller]
2391        fn assert_sends_undelegates(
2392            response: Response,
2393            expected_sends: Vec<(&str, u128)>,
2394            expected_undelegates: Vec<(&str, u128)>,
2395        ) {
2396            let sends = response
2397                .clone()
2398                .messages
2399                .into_iter()
2400                .filter(|msg| matches!(msg.msg, CosmosMsg::Bank(BankMsg::Send { .. })));
2401            let undelegates =
2402                response.clone().messages.into_iter().filter(|msg| {
2403                    matches!(msg.msg, CosmosMsg::Custom(TgradeMsg::Undelegate { .. }))
2404                });
2405
2406            assert_sends(
2407                Response::new().add_submessages(sends.clone()),
2408                expected_sends,
2409            );
2410            assert_undelegates(
2411                Response::new().add_submessages(undelegates.clone()),
2412                expected_undelegates,
2413            );
2414
2415            // Assert there are no other message types
2416            assert_eq!(
2417                response.messages.len(),
2418                sends.count() + undelegates.count(),
2419                "Unexpected messages in response, expected only bank send or tgrade undelegate messages"
2420            );
2421        }
2422
2423        #[test]
2424        fn single_claim_liquid() {
2425            let mut deps = mock_deps_tgrade();
2426            do_instantiate(deps.as_mut(), 2);
2427
2428            bond_liquid(deps.as_mut(), 12_000, 7_500, 4_000, 1);
2429            let height_delta = 2;
2430
2431            unbond(deps.as_mut(), 1000, 0, 0, height_delta, 0);
2432            let mut env = mock_env();
2433            env.block.height += height_delta;
2434            env.block.time = env.block.time.plus_seconds(UNBONDING_DURATION);
2435
2436            let resp = end_block(deps.as_mut(), env).unwrap();
2437            assert_sends(resp, vec![(USER1, 1000)]);
2438        }
2439
2440        #[test]
2441        fn single_claim_vesting() {
2442            let mut deps = mock_deps_tgrade();
2443            do_instantiate(deps.as_mut(), 2);
2444
2445            bond_vesting(deps.as_mut(), 12_000, 7_500, 4_000, 1);
2446            let height_delta = 2;
2447
2448            unbond(deps.as_mut(), 1000, 0, 0, height_delta, 0);
2449            let mut env = mock_env();
2450            env.block.height += height_delta;
2451            env.block.time = env.block.time.plus_seconds(UNBONDING_DURATION);
2452
2453            let resp = end_block(deps.as_mut(), env).unwrap();
2454            assert_undelegates(resp, vec![(USER1, 1000)]);
2455        }
2456
2457        #[test]
2458        fn single_claim_mixed() {
2459            let mut deps = mock_deps_tgrade();
2460            do_instantiate(deps.as_mut(), 2);
2461
2462            bond_liquid(deps.as_mut(), 1_000, 7_500, 4_000, 1);
2463
2464            bond_vesting(deps.as_mut(), 11_000, 7_500, 4_000, 2);
2465            let height_delta = 3;
2466
2467            unbond(deps.as_mut(), 1001, 0, 0, height_delta, 0);
2468            let mut env = mock_env();
2469            env.block.height += height_delta;
2470            env.block.time = env.block.time.plus_seconds(UNBONDING_DURATION);
2471
2472            let resp = end_block(deps.as_mut(), env).unwrap();
2473            // liquid stakes are claimed first, then vesting ones as needed
2474            assert_sends_undelegates(resp, vec![(USER1, 1000)], vec![(USER1, 1)]);
2475        }
2476
2477        #[test]
2478        fn multiple_users_claims_liquid() {
2479            let mut deps = mock_deps_tgrade();
2480            do_instantiate(deps.as_mut(), 4);
2481
2482            bond_liquid(deps.as_mut(), 12_000, 7_500, 4_000, 1);
2483            let height_delta = 2;
2484
2485            unbond(deps.as_mut(), 1000, 500, 0, height_delta, 0);
2486            unbond(deps.as_mut(), 0, 0, 200, height_delta, 1);
2487            let mut env = mock_env();
2488            env.block.height += height_delta;
2489            env.block.time = env.block.time.plus_seconds(UNBONDING_DURATION + 1);
2490
2491            let resp = end_block(deps.as_mut(), env).unwrap();
2492            assert_sends(resp, vec![(USER1, 1000), (USER2, 500), (USER3, 200)]);
2493        }
2494
2495        #[test]
2496        fn multiple_users_claims_vesting() {
2497            let mut deps = mock_deps_tgrade();
2498            do_instantiate(deps.as_mut(), 4);
2499
2500            bond_vesting(deps.as_mut(), 12_000, 7_500, 4_000, 1);
2501            let height_delta = 2;
2502
2503            unbond(deps.as_mut(), 1000, 500, 0, height_delta, 0);
2504            unbond(deps.as_mut(), 0, 0, 200, height_delta, 1);
2505            let mut env = mock_env();
2506            env.block.height += height_delta;
2507            env.block.time = env.block.time.plus_seconds(UNBONDING_DURATION + 1);
2508
2509            let resp = end_block(deps.as_mut(), env).unwrap();
2510            assert_undelegates(resp, vec![(USER1, 1000), (USER2, 500), (USER3, 200)]);
2511        }
2512
2513        #[test]
2514        fn multiple_users_claims_mixed() {
2515            let mut deps = mock_deps_tgrade();
2516            do_instantiate(deps.as_mut(), 4);
2517
2518            bond_liquid(deps.as_mut(), 11_000, 7_000, 4_000, 1);
2519            bond_vesting(deps.as_mut(), 1_000, 500, 0, 2);
2520            let height_delta = 3;
2521
2522            unbond(deps.as_mut(), 12_000, 7_001, 0, height_delta, 0);
2523            unbond(deps.as_mut(), 0, 0, 200, height_delta, 1);
2524            let mut env = mock_env();
2525            env.block.height += height_delta;
2526            env.block.time = env.block.time.plus_seconds(UNBONDING_DURATION + 1);
2527
2528            let resp = end_block(deps.as_mut(), env).unwrap();
2529            assert_sends_undelegates(
2530                resp,
2531                vec![(USER1, 11_000), (USER2, 7_000), (USER3, 200)],
2532                vec![(USER1, 1_000), (USER2, 1)],
2533            );
2534        }
2535
2536        #[test]
2537        fn single_user_multiple_claims_liquid() {
2538            let mut deps = mock_deps_tgrade();
2539            do_instantiate(deps.as_mut(), 3);
2540
2541            bond_liquid(deps.as_mut(), 12_000, 7_500, 4_000, 1);
2542            let height_delta = 2;
2543
2544            unbond(deps.as_mut(), 1000, 0, 0, height_delta, 0);
2545            unbond(deps.as_mut(), 500, 0, 0, height_delta, 1);
2546            let mut env = mock_env();
2547            env.block.height += height_delta;
2548            env.block.time = env.block.time.plus_seconds(UNBONDING_DURATION + 1);
2549
2550            let resp = end_block(deps.as_mut(), env).unwrap();
2551            assert_sends(resp, vec![(USER1, 1500)]);
2552        }
2553
2554        #[test]
2555        fn single_user_multiple_claims_vesting() {
2556            let mut deps = mock_deps_tgrade();
2557            do_instantiate(deps.as_mut(), 3);
2558
2559            bond_vesting(deps.as_mut(), 12_000, 7_500, 4_000, 1);
2560            let height_delta = 2;
2561
2562            unbond(deps.as_mut(), 1000, 0, 0, height_delta, 0);
2563            unbond(deps.as_mut(), 500, 0, 0, height_delta, 1);
2564            let mut env = mock_env();
2565            env.block.height += height_delta;
2566            env.block.time = env.block.time.plus_seconds(UNBONDING_DURATION + 1);
2567
2568            let resp = end_block(deps.as_mut(), env).unwrap();
2569            assert_undelegates(resp, vec![(USER1, 1500)]);
2570        }
2571
2572        #[test]
2573        fn single_user_multiple_claims_mixed() {
2574            let mut deps = mock_deps_tgrade();
2575            do_instantiate(deps.as_mut(), 3);
2576
2577            bond_vesting(deps.as_mut(), 11_000, 7_000, 4_000, 1);
2578            bond_liquid(deps.as_mut(), 1_000, 500, 4_000, 2);
2579            let height_delta = 3;
2580
2581            unbond(deps.as_mut(), 1001, 0, 0, height_delta, 0);
2582            unbond(deps.as_mut(), 500, 0, 0, height_delta, 1);
2583            let mut env = mock_env();
2584            env.block.height += height_delta;
2585            env.block.time = env.block.time.plus_seconds(UNBONDING_DURATION + 1);
2586
2587            let resp = end_block(deps.as_mut(), env).unwrap();
2588            assert_sends_undelegates(resp, vec![(USER1, 1000)], vec![(USER1, 501)]);
2589        }
2590
2591        #[test]
2592        fn only_expired_claims_liquid() {
2593            let mut deps = mock_deps_tgrade();
2594            do_instantiate(deps.as_mut(), 3);
2595
2596            bond_liquid(deps.as_mut(), 12_000, 7_500, 4_000, 1);
2597            let height_delta = 2;
2598
2599            // Claims to be returned
2600            unbond(deps.as_mut(), 1000, 0, 0, height_delta, 0);
2601            unbond(deps.as_mut(), 500, 600, 0, height_delta, 1);
2602
2603            // Clams not yet expired
2604            unbond(deps.as_mut(), 200, 300, 400, height_delta, 2);
2605            unbond(deps.as_mut(), 700, 0, 0, height_delta, 3);
2606            unbond(deps.as_mut(), 0, 100, 50, height_delta, 4);
2607
2608            let mut env = mock_env();
2609            env.block.height += height_delta;
2610            env.block.time = env.block.time.plus_seconds(UNBONDING_DURATION + 1);
2611
2612            let resp = end_block(deps.as_mut(), env).unwrap();
2613            assert_sends(resp, vec![(USER1, 1500), (USER2, 600)]);
2614        }
2615
2616        #[test]
2617        fn only_expired_claims_vesting() {
2618            let mut deps = mock_deps_tgrade();
2619            do_instantiate(deps.as_mut(), 3);
2620
2621            bond_vesting(deps.as_mut(), 12_000, 7_500, 4_000, 1);
2622            let height_delta = 2;
2623
2624            // Claims to be returned
2625            unbond(deps.as_mut(), 1000, 0, 0, height_delta, 0);
2626            unbond(deps.as_mut(), 500, 600, 0, height_delta, 1);
2627
2628            // Clams not yet expired
2629            unbond(deps.as_mut(), 200, 300, 400, height_delta, 2);
2630            unbond(deps.as_mut(), 700, 0, 0, height_delta, 3);
2631            unbond(deps.as_mut(), 0, 100, 50, height_delta, 4);
2632
2633            let mut env = mock_env();
2634            env.block.height += height_delta;
2635            env.block.time = env.block.time.plus_seconds(UNBONDING_DURATION + 1);
2636
2637            let resp = end_block(deps.as_mut(), env).unwrap();
2638            assert_undelegates(resp, vec![(USER1, 1500), (USER2, 600)]);
2639        }
2640
2641        #[test]
2642        fn claim_returned_once_liquid() {
2643            let mut deps = mock_deps_tgrade();
2644            do_instantiate(deps.as_mut(), 5);
2645
2646            bond_liquid(deps.as_mut(), 12_000, 7_500, 4_000, 1);
2647            let height_delta = 2;
2648
2649            // Claims to be returned
2650            unbond(deps.as_mut(), 1000, 0, 0, height_delta, 0);
2651            unbond(deps.as_mut(), 500, 600, 0, height_delta, 1);
2652
2653            // Clams not yet expired
2654            unbond(deps.as_mut(), 200, 300, 400, height_delta, 2);
2655
2656            let mut env = mock_env();
2657            env.block.height += height_delta;
2658            env.block.time = env.block.time.plus_seconds(UNBONDING_DURATION + 1);
2659
2660            let resp = end_block(deps.as_mut(), env).unwrap();
2661            assert_sends(resp, vec![(USER1, 1500), (USER2, 600)]);
2662
2663            // Some additional claims
2664            unbond(deps.as_mut(), 700, 0, 0, height_delta, 3);
2665            unbond(deps.as_mut(), 0, 100, 50, height_delta, 4);
2666
2667            let mut env = mock_env();
2668            env.block.height += height_delta;
2669            env.block.time = env.block.time.plus_seconds(UNBONDING_DURATION + 3);
2670
2671            // Expected that claims at time offset 2 and 3 are returned (0 and 1 are already
2672            // returned, 4 is not yet expired)
2673            let resp = end_block(deps.as_mut(), env).unwrap();
2674            assert_sends(resp, vec![(USER1, 900), (USER2, 300), (USER3, 400)]);
2675        }
2676
2677        #[test]
2678        fn claim_returned_once_vesting() {
2679            let mut deps = mock_deps_tgrade();
2680            do_instantiate(deps.as_mut(), 5);
2681
2682            bond_vesting(deps.as_mut(), 12_000, 7_500, 4_000, 1);
2683            let height_delta = 2;
2684
2685            // Claims to be returned
2686            unbond(deps.as_mut(), 1000, 0, 0, height_delta, 0);
2687            unbond(deps.as_mut(), 500, 600, 0, height_delta, 1);
2688
2689            // Clams not yet expired
2690            unbond(deps.as_mut(), 200, 300, 400, height_delta, 2);
2691
2692            let mut env = mock_env();
2693            env.block.height += height_delta;
2694            env.block.time = env.block.time.plus_seconds(UNBONDING_DURATION + 1);
2695
2696            let resp = end_block(deps.as_mut(), env).unwrap();
2697            assert_undelegates(resp, vec![(USER1, 1500), (USER2, 600)]);
2698
2699            // Some additional claims
2700            unbond(deps.as_mut(), 700, 0, 0, height_delta, 3);
2701            unbond(deps.as_mut(), 0, 100, 50, height_delta, 4);
2702
2703            let mut env = mock_env();
2704            env.block.height += height_delta;
2705            env.block.time = env.block.time.plus_seconds(UNBONDING_DURATION + 3);
2706
2707            // Expected that claims at time offset 2 and 3 are returned (0 and 1 are already
2708            // returned, 4 is not yet expired)
2709            let resp = end_block(deps.as_mut(), env).unwrap();
2710            assert_undelegates(resp, vec![(USER1, 900), (USER2, 300), (USER3, 400)]);
2711        }
2712
2713        #[test]
2714        fn up_to_limit_claims_returned_liquid() {
2715            let mut deps = mock_deps_tgrade();
2716            do_instantiate(deps.as_mut(), 2);
2717
2718            bond_liquid(deps.as_mut(), 12_000, 7_500, 4_000, 1);
2719            let height_delta = 2;
2720
2721            // Claims to be returned
2722            unbond(deps.as_mut(), 1000, 500, 0, height_delta, 0);
2723            unbond(deps.as_mut(), 0, 600, 0, height_delta, 1);
2724            unbond(deps.as_mut(), 200, 0, 0, height_delta, 2);
2725            unbond(deps.as_mut(), 0, 0, 300, height_delta, 3);
2726
2727            // Even if all claims are already expired, only two of them (time offset 0) should be
2728            // returned
2729            let mut env = mock_env();
2730            env.block.height += height_delta;
2731            env.block.time = env.block.time.plus_seconds(UNBONDING_DURATION + 3);
2732
2733            let resp = end_block(deps.as_mut(), env).unwrap();
2734            assert_sends(resp, vec![(USER1, 1000), (USER2, 500)]);
2735
2736            // Then on next block next batch is returned (time offset 1 and 2)
2737            let mut env = mock_env();
2738            env.block.height += height_delta;
2739            env.block.time = env.block.time.plus_seconds(UNBONDING_DURATION + 4);
2740
2741            let resp = end_block(deps.as_mut(), env).unwrap();
2742            assert_sends(resp, vec![(USER1, 200), (USER2, 600)]);
2743
2744            // Some additional claims
2745            unbond(deps.as_mut(), 700, 0, 0, height_delta, 5);
2746            unbond(deps.as_mut(), 0, 100, 50, height_delta, 6);
2747
2748            // Claims are returned in batches
2749            let mut env = mock_env();
2750            env.block.height += height_delta;
2751            env.block.time = env.block.time.plus_seconds(UNBONDING_DURATION + 6);
2752
2753            // offset 3 and 5
2754            let resp = end_block(deps.as_mut(), env).unwrap();
2755            assert_sends(resp, vec![(USER1, 700), (USER3, 300)]);
2756
2757            let mut env = mock_env();
2758            env.block.height += height_delta;
2759            env.block.time = env.block.time.plus_seconds(UNBONDING_DURATION + 6);
2760
2761            // offset 6
2762            let resp = end_block(deps.as_mut(), env).unwrap();
2763            assert_sends(resp, vec![(USER2, 100), (USER3, 50)]);
2764        }
2765
2766        #[test]
2767        fn up_to_limit_claims_returned_vesting() {
2768            let mut deps = mock_deps_tgrade();
2769            do_instantiate(deps.as_mut(), 2);
2770
2771            bond_vesting(deps.as_mut(), 12_000, 7_500, 4_000, 1);
2772            let height_delta = 2;
2773
2774            // Claims to be returned
2775            unbond(deps.as_mut(), 1000, 500, 0, height_delta, 0);
2776            unbond(deps.as_mut(), 0, 600, 0, height_delta, 1);
2777            unbond(deps.as_mut(), 200, 0, 0, height_delta, 2);
2778            unbond(deps.as_mut(), 0, 0, 300, height_delta, 3);
2779
2780            // Even if all claims are already expired, only two of them (time offset 0) should be
2781            // returned
2782            let mut env = mock_env();
2783            env.block.height += height_delta;
2784            env.block.time = env.block.time.plus_seconds(UNBONDING_DURATION + 3);
2785
2786            let resp = end_block(deps.as_mut(), env).unwrap();
2787            assert_undelegates(resp, vec![(USER1, 1000), (USER2, 500)]);
2788
2789            // Then on next block next batch is returned (time offset 1 and 2)
2790            let mut env = mock_env();
2791            env.block.height += height_delta;
2792            env.block.time = env.block.time.plus_seconds(UNBONDING_DURATION + 4);
2793
2794            let resp = end_block(deps.as_mut(), env).unwrap();
2795            assert_undelegates(resp, vec![(USER1, 200), (USER2, 600)]);
2796
2797            // Some additional claims
2798            unbond(deps.as_mut(), 700, 0, 0, height_delta, 5);
2799            unbond(deps.as_mut(), 0, 100, 50, height_delta, 6);
2800
2801            // Claims are returned in batches
2802            let mut env = mock_env();
2803            env.block.height += height_delta;
2804            env.block.time = env.block.time.plus_seconds(UNBONDING_DURATION + 6);
2805
2806            // offset 3 and 5
2807            let resp = end_block(deps.as_mut(), env).unwrap();
2808            assert_undelegates(resp, vec![(USER1, 700), (USER3, 300)]);
2809
2810            let mut env = mock_env();
2811            env.block.height += height_delta;
2812            env.block.time = env.block.time.plus_seconds(UNBONDING_DURATION + 6);
2813
2814            // offset 6
2815            let resp = end_block(deps.as_mut(), env).unwrap();
2816            assert_undelegates(resp, vec![(USER2, 100), (USER3, 50)]);
2817        }
2818
2819        #[test]
2820        fn unbound_with_invalid_denom_fails_liquid() {
2821            let mut deps = mock_deps_tgrade();
2822            do_instantiate(deps.as_mut(), 2);
2823
2824            bond_liquid(deps.as_mut(), 5_000, 0, 0, 1);
2825            let height_delta = 2;
2826
2827            let mut env = mock_env();
2828            env.block.height += height_delta;
2829
2830            let msg = ExecuteMsg::Unbond {
2831                tokens: coin(5_000, "invalid"),
2832            };
2833            let info = mock_info(USER1, &[]);
2834            let err = execute(deps.as_mut(), env, info, msg).unwrap_err();
2835
2836            assert_eq!(ContractError::InvalidDenom {}, err);
2837        }
2838
2839        #[test]
2840        fn unbound_with_invalid_denom_fails_vesting() {
2841            let mut deps = mock_deps_tgrade();
2842            do_instantiate(deps.as_mut(), 2);
2843
2844            bond_vesting(deps.as_mut(), 5_000, 0, 0, 1);
2845            let height_delta = 2;
2846
2847            let mut env = mock_env();
2848            env.block.height += height_delta;
2849
2850            let msg = ExecuteMsg::Unbond {
2851                tokens: coin(5_000, "invalid"),
2852            };
2853            let info = mock_info(USER1, &[]);
2854            let err = execute(deps.as_mut(), env, info, msg).unwrap_err();
2855
2856            assert_eq!(ContractError::InvalidDenom {}, err);
2857        }
2858    }
2859}