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
36const CONTRACT_NAME: &str = "crates.io:tg4-stake";
38const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
39
40#[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 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#[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 if !ADMIN.is_admin(deps.as_ref(), &info.sender)? {
110 PREAUTH_HOOKS.use_auth(deps.storage)?;
111 }
112
113 HOOKS.add_hook(deps.storage, deps.api.addr_validate(&hook)?)?;
115
116 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 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 HOOKS.remove_hook(deps.storage, hook_addr)?;
139
140 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 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 let new_vesting_stake =
176 STAKE_VESTING.update(deps.storage, &info.sender, |stake| -> StdResult<_> {
177 Ok(stake.unwrap_or_default() + vesting_amount)
178 })?;
179 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 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 let cfg = CONFIG.load(deps.storage)?;
215
216 if cfg.denom != denom {
217 return Err(ContractError::InvalidDenom {});
218 }
219
220 let stake = STAKE
222 .may_load(deps.storage, &info.sender)?
223 .unwrap_or_default();
224 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 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 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 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 if !ADMIN.is_admin(deps.as_ref(), &info.sender)? {
273 PREAUTH_SLASHING.use_auth(deps.storage)?;
274 }
275
276 SLASHERS.add_slasher(deps.storage, deps.api.addr_validate(&slasher)?)?;
278
279 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 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 SLASHERS.remove_slasher(deps.storage, slasher_addr)?;
302
303 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 liquid_stake.is_none() && vesting_stake.is_none() {
334 return Ok(Response::new());
335 }
336
337 let mut res = Response::new()
339 .add_attribute("action", "slash")
340 .add_attribute("addr", &addr)
341 .add_attribute("sender", info.sender);
342
343 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 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 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 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 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
396pub 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 let new = calc_points(new_stake, cfg);
419 let old = members().may_load(storage, &sender)?.map(|mi| mi.points);
420
421 if new == old {
423 return Ok(vec![]);
424 }
425 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 TOTAL.update(storage, |total| -> StdResult<_> {
433 Ok(total + new.unwrap_or_default() - old.unwrap_or_default())
434 })?;
435
436 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 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
653const 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 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 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 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 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 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 #[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 if height.is_none() {
958 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 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); }
976 }
977
978 #[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 #[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_users(deps.as_ref(), None, None, None, None);
1012
1013 bond_liquid(deps.as_mut(), 12_000, 7_500, 4_000, 1);
1015
1016 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 bond_liquid(deps.as_mut(), 0, 7_600, 1_200, 2);
1022
1023 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 assert_users(deps.as_ref(), None, None, None, Some(height + 1)); assert_users(deps.as_ref(), Some(12), Some(7), None, Some(height + 2)); assert_users(deps.as_ref(), Some(12), Some(15), Some(5), Some(height + 3));
1031 }
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_users(deps.as_ref(), None, None, None, None);
1042
1043 bond_vesting(deps.as_mut(), 12_000, 7_500, 4_000, 1);
1045
1046 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 bond_vesting(deps.as_mut(), 0, 7_600, 1_200, 2);
1052
1053 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 assert_users(deps.as_ref(), None, None, None, Some(height + 1)); assert_users(deps.as_ref(), Some(12), Some(7), None, Some(height + 2)); assert_users(deps.as_ref(), Some(12), Some(15), Some(5), Some(height + 3));
1061 }
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_users(deps.as_ref(), None, None, None, None);
1072
1073 bond_liquid(deps.as_mut(), 12_000, 7_500, 4_000, 1);
1075
1076 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 bond_vesting(deps.as_mut(), 0, 7_600, 1_200, 2);
1082
1083 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 assert_users(deps.as_ref(), None, None, None, Some(height + 1)); assert_users(deps.as_ref(), Some(12), Some(7), None, Some(height + 2)); assert_users(deps.as_ref(), Some(12), Some(15), Some(5), Some(height + 3));
1091 }
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_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 let members = list_members(deps.as_ref(), None, Some(1)).unwrap().members;
1131 assert_eq!(members.len(), 1);
1132 assert_eq!(
1134 members,
1135 vec![Member {
1136 addr: USER1.into(),
1137 points: 12,
1138 start_height: None,
1139 },]
1140 );
1141
1142 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_eq!(
1150 members,
1151 vec![Member {
1152 addr: USER2.into(),
1153 points: 7,
1154 start_height: None,
1155 },]
1156 );
1157
1158 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_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 let members = list_members_by_points(deps.as_ref(), None, Some(1))
1201 .unwrap()
1202 .members;
1203 assert_eq!(members.len(), 1);
1204 assert_eq!(
1206 members,
1207 vec![Member {
1208 addr: USER1.into(),
1209 points: 11,
1210 start_height: None,
1211 },]
1212 );
1213
1214 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_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 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 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 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 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_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 bond(deps.as_mut(), (500, 100), (100, 0), (0, 2_222), 3);
1290
1291 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 assert_users(deps.as_ref(), None, None, None, Some(height + 1)); assert_users(deps.as_ref(), Some(12), Some(7), Some(6), Some(height + 2)); assert_users(deps.as_ref(), Some(7), None, Some(5), Some(height + 3)); assert_users(deps.as_ref(), Some(8), Some(5), Some(7), Some(height + 4)); 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 let mut deps = mock_deps_tgrade();
1324 default_instantiate(deps.as_mut());
1325 bond(deps.as_mut(), (1_000, 10_000), (6_000, 0), (0, 0), 1);
1327
1328 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 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 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 bond(deps.as_mut(), (4_000, 7_500), (7_500, 0), (3_000, 1_000), 1);
1362 let height_delta = 2;
1363 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 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 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 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 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 let mut env3 = mock_env();
1451 env3.block.time = env3.block.time.plus_seconds(UNBONDING_DURATION);
1452 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 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 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 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 unbond(deps.as_mut(), 0, 600, 0, 30, 0);
1528 unbond(deps.as_mut(), 0, 1_005, 0, 50, 0);
1529
1530 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 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 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 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 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 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 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 let err = execute(deps.as_mut(), mock_env(), admin_info.clone(), add_msg).unwrap_err();
1610 assert_eq!(err, HookError::HookAlreadyRegistered {}.into());
1611
1612 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 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 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 assert!(expected_liquid.len() <= 1);
1689 assert!(expected_vesting.len() <= 1);
1690
1691 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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_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 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 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 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 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 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 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 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 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 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 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 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 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 let mut deps = mock_deps_tgrade();
2169 do_instantiate(deps.as_mut(), Uint128::new(100), Uint128::zero(), 5, 0);
2170
2171 bond_liquid(deps.as_mut(), 50, 1, 102, 1);
2174 assert_users(deps.as_ref(), Some(0), Some(0), Some(1), None);
2175
2176 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 let mut deps = mock_deps_tgrade();
2185 do_instantiate(deps.as_mut(), Uint128::new(100), Uint128::zero(), 5, 0);
2186
2187 bond_vesting(deps.as_mut(), 50, 1, 102, 1);
2190 assert_users(deps.as_ref(), Some(0), Some(0), Some(1), None);
2191
2192 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 let mut deps = mock_deps_tgrade();
2201 do_instantiate(deps.as_mut(), Uint128::new(100), Uint128::zero(), 5, 0);
2202
2203 bond_liquid(deps.as_mut(), 25, 1, 102, 1);
2206 assert_users(deps.as_ref(), Some(0), Some(0), Some(1), None);
2207
2208 bond_vesting(deps.as_mut(), 25, 1, 102, 2);
2211 assert_users(deps.as_ref(), Some(0), Some(0), Some(2), None);
2212
2213 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 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 let claims = get_claims(deps.as_ref(), Addr::unchecked(USER1), Some(6), None);
2242 assert_eq!(claims.len(), 6);
2243 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 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 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 #[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 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 let sends: Vec<_> = sends.into_iter().map(|(_, _, msg)| msg).collect();
2315
2316 expected_transfers.sort_unstable();
2318
2319 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 #[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 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 let undelegates: Vec<_> = undelegates.into_iter().map(|(_, _, msg)| msg).collect();
2368
2369 expected_transfers.sort_unstable();
2371
2372 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 #[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_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 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 unbond(deps.as_mut(), 1000, 0, 0, height_delta, 0);
2601 unbond(deps.as_mut(), 500, 600, 0, height_delta, 1);
2602
2603 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 unbond(deps.as_mut(), 1000, 0, 0, height_delta, 0);
2626 unbond(deps.as_mut(), 500, 600, 0, height_delta, 1);
2627
2628 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 unbond(deps.as_mut(), 1000, 0, 0, height_delta, 0);
2651 unbond(deps.as_mut(), 500, 600, 0, height_delta, 1);
2652
2653 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 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 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 unbond(deps.as_mut(), 1000, 0, 0, height_delta, 0);
2687 unbond(deps.as_mut(), 500, 600, 0, height_delta, 1);
2688
2689 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 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 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 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 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 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 unbond(deps.as_mut(), 700, 0, 0, height_delta, 5);
2746 unbond(deps.as_mut(), 0, 100, 50, height_delta, 6);
2747
2748 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 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 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 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 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 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 unbond(deps.as_mut(), 700, 0, 0, height_delta, 5);
2799 unbond(deps.as_mut(), 0, 100, 50, height_delta, 6);
2800
2801 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 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 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}