1#[cfg(not(feature = "library"))]
2use cosmwasm_std::entry_point;
3use cosmwasm_std::{
4 coin, to_binary, Addr, BankMsg, Binary, Decimal, Deps, DepsMut, DistributionMsg, Env,
5 MessageInfo, QuerierWrapper, Response, StakingMsg, StdError, StdResult, Uint128, WasmMsg,
6};
7
8use cw2::set_contract_version;
9use cw20_base::allowances::{
10 execute_burn_from, execute_decrease_allowance, execute_increase_allowance, execute_send_from,
11 execute_transfer_from, query_allowance,
12};
13use cw20_base::contract::{
14 execute_burn, execute_mint, execute_send, execute_transfer, query_balance, query_token_info,
15};
16use cw20_base::state::{MinterData, TokenInfo, TOKEN_INFO};
17
18use crate::error::ContractError;
19use crate::msg::{ExecuteMsg, InstantiateMsg, InvestmentResponse, QueryMsg};
20use crate::state::{InvestmentInfo, Supply, CLAIMS, INVESTMENT, TOTAL_SUPPLY};
21
22const FALLBACK_RATIO: Decimal = Decimal::one();
23
24const CONTRACT_NAME: &str = "crates.io:cw20-staking";
26const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
27
28#[cfg_attr(not(feature = "library"), entry_point)]
29pub fn instantiate(
30 deps: DepsMut,
31 env: Env,
32 info: MessageInfo,
33 msg: InstantiateMsg,
34) -> Result<Response, ContractError> {
35 set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
36
37 let vals = deps.querier.query_all_validators()?;
39 if !vals.iter().any(|v| v.address == msg.validator) {
40 return Err(ContractError::NotInValidatorSet {
41 validator: msg.validator,
42 });
43 }
44
45 let data = TokenInfo {
47 name: msg.name,
48 symbol: msg.symbol,
49 decimals: msg.decimals,
50 total_supply: Uint128::zero(),
51 mint: Some(MinterData {
53 minter: env.contract.address,
54 cap: None,
55 }),
56 };
57 TOKEN_INFO.save(deps.storage, &data)?;
58
59 let denom = deps.querier.query_bonded_denom()?;
60 let invest = InvestmentInfo {
61 owner: info.sender,
62 exit_tax: msg.exit_tax,
63 unbonding_period: msg.unbonding_period,
64 bond_denom: denom,
65 validator: msg.validator,
66 min_withdrawal: msg.min_withdrawal,
67 };
68 INVESTMENT.save(deps.storage, &invest)?;
69
70 let supply = Supply::default();
72 TOTAL_SUPPLY.save(deps.storage, &supply)?;
73
74 Ok(Response::default())
75}
76
77#[cfg_attr(not(feature = "library"), entry_point)]
78pub fn execute(
79 deps: DepsMut,
80 env: Env,
81 info: MessageInfo,
82 msg: ExecuteMsg,
83) -> Result<Response, ContractError> {
84 match msg {
85 ExecuteMsg::Bond {} => bond(deps, env, info),
86 ExecuteMsg::Unbond { amount } => unbond(deps, env, info, amount),
87 ExecuteMsg::Claim {} => claim(deps, env, info),
88 ExecuteMsg::Reinvest {} => reinvest(deps, env, info),
89 ExecuteMsg::_BondAllTokens {} => _bond_all_tokens(deps, env, info),
90
91 ExecuteMsg::Transfer { recipient, amount } => {
93 Ok(execute_transfer(deps, env, info, recipient, amount)?)
94 }
95 ExecuteMsg::Burn { amount } => Ok(execute_burn(deps, env, info, amount)?),
96 ExecuteMsg::Send {
97 contract,
98 amount,
99 msg,
100 } => Ok(execute_send(deps, env, info, contract, amount, msg)?),
101 ExecuteMsg::IncreaseAllowance {
102 spender,
103 amount,
104 expires,
105 } => Ok(execute_increase_allowance(
106 deps, env, info, spender, amount, expires,
107 )?),
108 ExecuteMsg::DecreaseAllowance {
109 spender,
110 amount,
111 expires,
112 } => Ok(execute_decrease_allowance(
113 deps, env, info, spender, amount, expires,
114 )?),
115 ExecuteMsg::TransferFrom {
116 owner,
117 recipient,
118 amount,
119 } => Ok(execute_transfer_from(
120 deps, env, info, owner, recipient, amount,
121 )?),
122 ExecuteMsg::BurnFrom { owner, amount } => {
123 Ok(execute_burn_from(deps, env, info, owner, amount)?)
124 }
125 ExecuteMsg::SendFrom {
126 owner,
127 contract,
128 amount,
129 msg,
130 } => Ok(execute_send_from(
131 deps, env, info, owner, contract, amount, msg,
132 )?),
133 }
134}
135
136fn get_bonded(querier: &QuerierWrapper, contract: &Addr) -> Result<Uint128, ContractError> {
139 let bonds = querier.query_all_delegations(contract)?;
140 if bonds.is_empty() {
141 return Ok(Uint128::zero());
142 }
143 let denom = bonds[0].amount.denom.as_str();
144 bonds.iter().fold(Ok(Uint128::zero()), |racc, d| {
145 let acc = racc?;
146 if d.amount.denom.as_str() != denom {
147 Err(ContractError::DifferentBondDenom {
148 denom1: denom.into(),
149 denom2: d.amount.denom.to_string(),
150 })
151 } else {
152 Ok(acc + d.amount.amount)
153 }
154 })
155}
156
157fn assert_bonds(supply: &Supply, bonded: Uint128) -> Result<(), ContractError> {
158 if supply.bonded != bonded {
159 Err(ContractError::BondedMismatch {
160 stored: supply.bonded,
161 queried: bonded,
162 })
163 } else {
164 Ok(())
165 }
166}
167
168pub fn bond(deps: DepsMut, env: Env, info: MessageInfo) -> Result<Response, ContractError> {
169 let invest = INVESTMENT.load(deps.storage)?;
171 let payment = info
173 .funds
174 .iter()
175 .find(|x| x.denom == invest.bond_denom)
176 .ok_or_else(|| ContractError::EmptyBalance {
177 denom: invest.bond_denom.clone(),
178 })?;
179
180 let bonded = get_bonded(&deps.querier, &env.contract.address)?;
182
183 let mut supply = TOTAL_SUPPLY.load(deps.storage)?;
185 assert_bonds(&supply, bonded)?;
189 let to_mint = if supply.issued.is_zero() || bonded.is_zero() {
190 FALLBACK_RATIO * payment.amount
191 } else {
192 payment.amount.multiply_ratio(supply.issued, bonded)
193 };
194 supply.bonded = bonded + payment.amount;
195 supply.issued += to_mint;
196 TOTAL_SUPPLY.save(deps.storage, &supply)?;
197
198 let sub_info = MessageInfo {
200 sender: env.contract.address.clone(),
201 funds: vec![],
202 };
203 execute_mint(deps, env, sub_info, info.sender.to_string(), to_mint)?;
204
205 let res = Response::new()
207 .add_message(StakingMsg::Delegate {
208 validator: invest.validator,
209 amount: payment.clone(),
210 })
211 .add_attribute("action", "bond")
212 .add_attribute("from", info.sender)
213 .add_attribute("bonded", payment.amount)
214 .add_attribute("minted", to_mint);
215 Ok(res)
216}
217
218pub fn unbond(
219 mut deps: DepsMut,
220 env: Env,
221 info: MessageInfo,
222 amount: Uint128,
223) -> Result<Response, ContractError> {
224 let invest = INVESTMENT.load(deps.storage)?;
225 if amount < invest.min_withdrawal {
227 return Err(ContractError::UnbondTooSmall {
228 min_bonded: invest.min_withdrawal,
229 denom: invest.bond_denom,
230 });
231 }
232 let tax = amount * invest.exit_tax;
234
235 execute_burn(deps.branch(), env.clone(), info.clone(), amount)?;
237 if tax > Uint128::zero() {
238 let sub_info = MessageInfo {
239 sender: env.contract.address.clone(),
240 funds: vec![],
241 };
242 execute_mint(
244 deps.branch(),
245 env.clone(),
246 sub_info,
247 invest.owner.to_string(),
248 tax,
249 )?;
250 }
251
252 let bonded = get_bonded(&deps.querier, &env.contract.address)?;
255
256 let remainder = amount.checked_sub(tax).map_err(StdError::overflow)?;
258 let mut supply = TOTAL_SUPPLY.load(deps.storage)?;
259 assert_bonds(&supply, bonded)?;
263 let unbond = remainder.multiply_ratio(bonded, supply.issued);
264 supply.bonded = bonded.checked_sub(unbond).map_err(StdError::overflow)?;
265 supply.issued = supply
266 .issued
267 .checked_sub(remainder)
268 .map_err(StdError::overflow)?;
269 supply.claims += unbond;
270 TOTAL_SUPPLY.save(deps.storage, &supply)?;
271
272 CLAIMS.create_claim(
273 deps.storage,
274 &info.sender,
275 unbond,
276 invest.unbonding_period.after(&env.block),
277 )?;
278
279 let res = Response::new()
281 .add_message(StakingMsg::Undelegate {
282 validator: invest.validator,
283 amount: coin(unbond.u128(), &invest.bond_denom),
284 })
285 .add_attribute("action", "unbond")
286 .add_attribute("to", info.sender)
287 .add_attribute("unbonded", unbond)
288 .add_attribute("burnt", amount);
289 Ok(res)
290}
291
292pub fn claim(deps: DepsMut, env: Env, info: MessageInfo) -> Result<Response, ContractError> {
293 let invest = INVESTMENT.load(deps.storage)?;
295 let mut balance = deps
296 .querier
297 .query_balance(&env.contract.address, &invest.bond_denom)?;
298 if balance.amount < invest.min_withdrawal {
299 return Err(ContractError::BalanceTooSmall {});
300 }
301
302 let to_send =
305 CLAIMS.claim_tokens(deps.storage, &info.sender, &env.block, Some(balance.amount))?;
306 if to_send == Uint128::zero() {
307 return Err(ContractError::NothingToClaim {});
308 }
309
310 TOTAL_SUPPLY.update(deps.storage, |mut supply| -> StdResult<_> {
312 supply.claims = supply.claims.checked_sub(to_send)?;
313 Ok(supply)
314 })?;
315
316 balance.amount = to_send;
318 let res = Response::new()
319 .add_message(BankMsg::Send {
320 to_address: info.sender.to_string(),
321 amount: vec![balance],
322 })
323 .add_attribute("action", "claim")
324 .add_attribute("from", info.sender)
325 .add_attribute("amount", to_send);
326 Ok(res)
327}
328
329pub fn reinvest(deps: DepsMut, env: Env, _info: MessageInfo) -> Result<Response, ContractError> {
333 let contract_addr = env.contract.address;
334 let invest = INVESTMENT.load(deps.storage)?;
335 let msg = to_binary(&ExecuteMsg::_BondAllTokens {})?;
336
337 let res = Response::new()
339 .add_message(DistributionMsg::WithdrawDelegatorReward {
340 validator: invest.validator,
341 })
342 .add_message(WasmMsg::Execute {
343 contract_addr: contract_addr.to_string(),
344 msg,
345 funds: vec![],
346 });
347 Ok(res)
348}
349
350pub fn _bond_all_tokens(
351 deps: DepsMut,
352 env: Env,
353 info: MessageInfo,
354) -> Result<Response, ContractError> {
355 if info.sender != env.contract.address {
357 return Err(ContractError::Unauthorized {});
358 }
359
360 let invest = INVESTMENT.load(deps.storage)?;
362 let mut balance = deps
363 .querier
364 .query_balance(&env.contract.address, &invest.bond_denom)?;
365
366 match TOTAL_SUPPLY.update(deps.storage, |mut supply| -> StdResult<_> {
369 balance.amount = balance.amount.checked_sub(supply.claims)?;
370 balance.amount.checked_sub(invest.min_withdrawal)?;
372 supply.bonded += balance.amount;
373 Ok(supply)
374 }) {
375 Ok(_) => {}
376 Err(StdError::Overflow { .. }) => return Ok(Response::default()),
378 Err(e) => return Err(ContractError::Std(e)),
379 }
380
381 let res = Response::new()
383 .add_message(StakingMsg::Delegate {
384 validator: invest.validator,
385 amount: balance.clone(),
386 })
387 .add_attribute("action", "reinvest")
388 .add_attribute("bonded", balance.amount);
389 Ok(res)
390}
391
392#[cfg_attr(not(feature = "library"), entry_point)]
393pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
394 match msg {
395 QueryMsg::Claims { address } => {
397 to_binary(&CLAIMS.query_claims(deps, &deps.api.addr_validate(&address)?)?)
398 }
399 QueryMsg::Investment {} => to_binary(&query_investment(deps)?),
400 QueryMsg::TokenInfo {} => to_binary(&query_token_info(deps)?),
402 QueryMsg::Balance { address } => to_binary(&query_balance(deps, address)?),
403 QueryMsg::Allowance { owner, spender } => {
404 to_binary(&query_allowance(deps, owner, spender)?)
405 }
406 }
407}
408
409pub fn query_investment(deps: Deps) -> StdResult<InvestmentResponse> {
410 let invest = INVESTMENT.load(deps.storage)?;
411 let supply = TOTAL_SUPPLY.load(deps.storage)?;
412
413 let res = InvestmentResponse {
414 owner: invest.owner.to_string(),
415 exit_tax: invest.exit_tax,
416 validator: invest.validator,
417 min_withdrawal: invest.min_withdrawal,
418 token_supply: supply.issued,
419 staked_tokens: coin(supply.bonded.u128(), &invest.bond_denom),
420 nominal_value: if supply.issued.is_zero() {
421 FALLBACK_RATIO
422 } else {
423 Decimal::from_ratio(supply.bonded, supply.issued)
424 },
425 };
426 Ok(res)
427}
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432 use std::str::FromStr;
433
434 use cosmwasm_std::testing::{
435 mock_dependencies, mock_env, mock_info, MockQuerier, MOCK_CONTRACT_ADDR,
436 };
437 use cosmwasm_std::{
438 coins, Coin, CosmosMsg, Decimal, FullDelegation, OverflowError, OverflowOperation,
439 Validator,
440 };
441 use cw_controllers::Claim;
442 use cw_utils::{Duration, DAY, HOUR, WEEK};
443
444 fn sample_validator(addr: &str) -> Validator {
445 Validator {
446 address: addr.into(),
447 commission: Decimal::percent(3),
448 max_commission: Decimal::percent(10),
449 max_change_rate: Decimal::percent(1),
450 }
451 }
452
453 fn sample_delegation(addr: &str, amount: Coin) -> FullDelegation {
454 let can_redelegate = amount.clone();
455 let accumulated_rewards = coins(0, &amount.denom);
456 FullDelegation {
457 validator: addr.into(),
458 delegator: Addr::unchecked(MOCK_CONTRACT_ADDR),
459 amount,
460 can_redelegate,
461 accumulated_rewards,
462 }
463 }
464
465 fn set_validator(querier: &mut MockQuerier) {
466 querier.update_staking("ustake", &[sample_validator(DEFAULT_VALIDATOR)], &[]);
467 }
468
469 fn set_delegation(querier: &mut MockQuerier, amount: u128, denom: &str) {
470 querier.update_staking(
471 "ustake",
472 &[sample_validator(DEFAULT_VALIDATOR)],
473 &[sample_delegation(DEFAULT_VALIDATOR, coin(amount, denom))],
474 );
475 }
476
477 fn later(env: &Env, delta: Duration) -> Env {
479 let time_delta = match delta {
480 Duration::Time(t) => t,
481 _ => panic!("Must provide duration in time"),
482 };
483 let mut res = env.clone();
484 res.block.time = res.block.time.plus_seconds(time_delta);
485 res
486 }
487
488 const DEFAULT_VALIDATOR: &str = "default-validator";
489
490 fn default_instantiate(tax_percent: u64, min_withdrawal: u128) -> InstantiateMsg {
491 InstantiateMsg {
492 name: "Cool Derivative".to_string(),
493 symbol: "DRV".to_string(),
494 decimals: 9,
495 validator: String::from(DEFAULT_VALIDATOR),
496 unbonding_period: DAY * 3,
497 exit_tax: Decimal::percent(tax_percent),
498 min_withdrawal: Uint128::new(min_withdrawal),
499 }
500 }
501
502 fn get_balance<U: Into<String>>(deps: Deps, addr: U) -> Uint128 {
503 query_balance(deps, addr.into()).unwrap().balance
504 }
505
506 fn get_claims(deps: Deps, addr: &str) -> Vec<Claim> {
507 CLAIMS
508 .query_claims(deps, &Addr::unchecked(addr))
509 .unwrap()
510 .claims
511 }
512
513 #[test]
514 fn instantiation_with_missing_validator() {
515 let mut deps = mock_dependencies();
516 deps.querier
517 .update_staking("ustake", &[sample_validator("john")], &[]);
518
519 let creator = String::from("creator");
520 let msg = InstantiateMsg {
521 name: "Cool Derivative".to_string(),
522 symbol: "DRV".to_string(),
523 decimals: 9,
524 validator: String::from("my-validator"),
525 unbonding_period: WEEK,
526 exit_tax: Decimal::percent(2),
527 min_withdrawal: Uint128::new(50),
528 };
529 let info = mock_info(&creator, &[]);
530
531 let err = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap_err();
533 assert_eq!(
534 err,
535 ContractError::NotInValidatorSet {
536 validator: "my-validator".into()
537 }
538 );
539 }
540
541 #[test]
542 fn proper_instantiation() {
543 let mut deps = mock_dependencies();
544 deps.querier.update_staking(
545 "ustake",
546 &[
547 sample_validator("john"),
548 sample_validator("mary"),
549 sample_validator("my-validator"),
550 ],
551 &[],
552 );
553
554 let creator = String::from("creator");
555 let msg = InstantiateMsg {
556 name: "Cool Derivative".to_string(),
557 symbol: "DRV".to_string(),
558 decimals: 0,
559 validator: String::from("my-validator"),
560 unbonding_period: HOUR * 12,
561 exit_tax: Decimal::percent(2),
562 min_withdrawal: Uint128::new(50),
563 };
564 let info = mock_info(&creator, &[]);
565
566 let res = instantiate(deps.as_mut(), mock_env(), info, msg.clone()).unwrap();
568 assert_eq!(0, res.messages.len());
569
570 let token = query_token_info(deps.as_ref()).unwrap();
572 assert_eq!(&token.name, &msg.name);
573 assert_eq!(&token.symbol, &msg.symbol);
574 assert_eq!(token.decimals, msg.decimals);
575 assert_eq!(token.total_supply, Uint128::zero());
576
577 assert_eq!(get_balance(deps.as_ref(), &creator), Uint128::zero());
579 assert_eq!(get_claims(deps.as_ref(), &creator), vec![]);
581
582 let invest = query_investment(deps.as_ref()).unwrap();
584 assert_eq!(&invest.owner, &creator);
585 assert_eq!(&invest.validator, &msg.validator);
586 assert_eq!(invest.exit_tax, msg.exit_tax);
587 assert_eq!(invest.min_withdrawal, msg.min_withdrawal);
588
589 assert_eq!(invest.token_supply, Uint128::zero());
590 assert_eq!(invest.staked_tokens, coin(0, "ustake"));
591 assert_eq!(invest.nominal_value, Decimal::one());
592 }
593
594 #[test]
595 fn bonding_issues_tokens() {
596 let mut deps = mock_dependencies();
597 set_validator(&mut deps.querier);
598
599 let creator = String::from("creator");
600 let instantiate_msg = default_instantiate(2, 50);
601 let info = mock_info(&creator, &[]);
602
603 let res = instantiate(deps.as_mut(), mock_env(), info, instantiate_msg).unwrap();
605 assert_eq!(0, res.messages.len());
606
607 let bob = String::from("bob");
609 let bond_msg = ExecuteMsg::Bond {};
610 let info = mock_info(&bob, &[coin(10, "random"), coin(1000, "ustake")]);
611
612 let res = execute(deps.as_mut(), mock_env(), info, bond_msg).unwrap();
614 assert_eq!(1, res.messages.len());
615 let delegate = &res.messages[0];
616 match &delegate.msg {
617 CosmosMsg::Staking(StakingMsg::Delegate { validator, amount }) => {
618 assert_eq!(validator.as_str(), DEFAULT_VALIDATOR);
619 assert_eq!(amount, &coin(1000, "ustake"));
620 }
621 _ => panic!("Unexpected message: {:?}", delegate),
622 }
623
624 assert_eq!(get_balance(deps.as_ref(), &bob), Uint128::new(1000));
626
627 let invest = query_investment(deps.as_ref()).unwrap();
629 assert_eq!(invest.token_supply, Uint128::new(1000));
630 assert_eq!(invest.staked_tokens, coin(1000, "ustake"));
631 assert_eq!(invest.nominal_value, Decimal::one());
632
633 let token = query_token_info(deps.as_ref()).unwrap();
635 assert_eq!(token.total_supply, Uint128::new(1000));
636 }
637
638 #[test]
639 fn rebonding_changes_pricing() {
640 let mut deps = mock_dependencies();
641 set_validator(&mut deps.querier);
642
643 let creator = String::from("creator");
644 let instantiate_msg = default_instantiate(2, 50);
645 let info = mock_info(&creator, &[]);
646
647 let res = instantiate(deps.as_mut(), mock_env(), info, instantiate_msg).unwrap();
649 assert_eq!(0, res.messages.len());
650
651 let bob = String::from("bob");
653 let bond_msg = ExecuteMsg::Bond {};
654 let info = mock_info(&bob, &[coin(10, "random"), coin(1000, "ustake")]);
655 let res = execute(deps.as_mut(), mock_env(), info, bond_msg).unwrap();
656 assert_eq!(1, res.messages.len());
657
658 set_delegation(&mut deps.querier, 1000, "ustake");
660
661 let rebond_msg = ExecuteMsg::_BondAllTokens {};
663 let info = mock_info(MOCK_CONTRACT_ADDR, &[]);
664 deps.querier
665 .update_balance(MOCK_CONTRACT_ADDR, coins(500, "ustake"));
666 let _ = execute(deps.as_mut(), mock_env(), info, rebond_msg).unwrap();
667
668 set_delegation(&mut deps.querier, 1500, "ustake");
670
671 let invest = query_investment(deps.as_ref()).unwrap();
673 assert_eq!(invest.token_supply, Uint128::new(1000));
674 assert_eq!(invest.staked_tokens, coin(1500, "ustake"));
675 let ratio = Decimal::from_str("1.5").unwrap();
676 assert_eq!(invest.nominal_value, ratio);
677
678 let alice = String::from("alice");
680 let bond_msg = ExecuteMsg::Bond {};
681 let info = mock_info(&alice, &[coin(3000, "ustake")]);
682 let res = execute(deps.as_mut(), mock_env(), info, bond_msg).unwrap();
683 assert_eq!(1, res.messages.len());
684
685 set_delegation(&mut deps.querier, 3000, "ustake");
687
688 assert_eq!(get_balance(deps.as_ref(), &alice), Uint128::new(2000));
690
691 let invest = query_investment(deps.as_ref()).unwrap();
692 assert_eq!(invest.token_supply, Uint128::new(3000));
693 assert_eq!(invest.staked_tokens, coin(4500, "ustake"));
694 assert_eq!(invest.nominal_value, ratio);
695 }
696
697 #[test]
698 fn bonding_fails_with_wrong_denom() {
699 let mut deps = mock_dependencies();
700 set_validator(&mut deps.querier);
701
702 let creator = String::from("creator");
703 let instantiate_msg = default_instantiate(2, 50);
704 let info = mock_info(&creator, &[]);
705
706 let res = instantiate(deps.as_mut(), mock_env(), info, instantiate_msg).unwrap();
708 assert_eq!(0, res.messages.len());
709
710 let bob = String::from("bob");
712 let bond_msg = ExecuteMsg::Bond {};
713 let info = mock_info(&bob, &[coin(500, "photon")]);
714
715 let err = execute(deps.as_mut(), mock_env(), info, bond_msg).unwrap_err();
717 assert_eq!(
718 err,
719 ContractError::EmptyBalance {
720 denom: "ustake".to_string()
721 }
722 );
723 }
724
725 #[test]
726 fn unbonding_maintains_price_ratio() {
727 let mut deps = mock_dependencies();
728 set_validator(&mut deps.querier);
729
730 let creator = String::from("creator");
731 let instantiate_msg = default_instantiate(10, 50);
732 let info = mock_info(&creator, &[]);
733
734 let res = instantiate(deps.as_mut(), mock_env(), info, instantiate_msg).unwrap();
736 assert_eq!(0, res.messages.len());
737
738 let bob = String::from("bob");
740 let bond_msg = ExecuteMsg::Bond {};
741 let info = mock_info(&bob, &[coin(10, "random"), coin(1000, "ustake")]);
742 let res = execute(deps.as_mut(), mock_env(), info, bond_msg).unwrap();
743 assert_eq!(1, res.messages.len());
744
745 set_delegation(&mut deps.querier, 1000, "ustake");
747
748 let rebond_msg = ExecuteMsg::_BondAllTokens {};
751 let info = mock_info(MOCK_CONTRACT_ADDR, &[]);
752 deps.querier
753 .update_balance(MOCK_CONTRACT_ADDR, coins(500, "ustake"));
754 let _ = execute(deps.as_mut(), mock_env(), info, rebond_msg).unwrap();
755
756 set_delegation(&mut deps.querier, 1500, "ustake");
758 deps.querier.update_balance(MOCK_CONTRACT_ADDR, vec![]);
759
760 let unbond_msg = ExecuteMsg::Unbond {
762 amount: Uint128::new(600),
763 };
764 let info = mock_info(&creator, &[]);
765 let err = execute(deps.as_mut(), mock_env(), info, unbond_msg).unwrap_err();
766 assert_eq!(
767 err,
768 ContractError::Std(StdError::overflow(OverflowError::new(
769 OverflowOperation::Sub,
770 0,
771 600
772 )))
773 );
774
775 let unbond_msg = ExecuteMsg::Unbond {
779 amount: Uint128::new(600),
780 };
781 let owner_cut = Uint128::new(60);
782 let bobs_claim = Uint128::new(810);
783 let bobs_balance = Uint128::new(400);
784 let env = mock_env();
785 let info = mock_info(&bob, &[]);
786 let res = execute(deps.as_mut(), env.clone(), info, unbond_msg).unwrap();
787 assert_eq!(1, res.messages.len());
788 let delegate = &res.messages[0];
789 match &delegate.msg {
790 CosmosMsg::Staking(StakingMsg::Undelegate { validator, amount }) => {
791 assert_eq!(validator.as_str(), DEFAULT_VALIDATOR);
792 assert_eq!(amount, &coin(bobs_claim.u128(), "ustake"));
793 }
794 _ => panic!("Unexpected message: {:?}", delegate),
795 }
796
797 set_delegation(&mut deps.querier, 690, "ustake");
799
800 assert_eq!(get_balance(deps.as_ref(), &bob), bobs_balance);
802 assert_eq!(get_balance(deps.as_ref(), &creator), owner_cut);
803 let expected_claims = vec![Claim {
805 amount: bobs_claim,
806 release_at: (DAY * 3).after(&env.block),
807 }];
808 assert_eq!(expected_claims, get_claims(deps.as_ref(), &bob));
809
810 let ratio = Decimal::from_str("1.5").unwrap();
812
813 let invest = query_investment(deps.as_ref()).unwrap();
814 assert_eq!(invest.token_supply, bobs_balance + owner_cut);
815 assert_eq!(invest.staked_tokens, coin(690, "ustake")); assert_eq!(invest.nominal_value, ratio);
817 }
818
819 #[test]
820 fn claims_paid_out_properly() {
821 let mut deps = mock_dependencies();
822 set_validator(&mut deps.querier);
823
824 let creator = String::from("creator");
826 let instantiate_msg = default_instantiate(10, 50);
827 let info = mock_info(&creator, &[]);
828 instantiate(deps.as_mut(), mock_env(), info, instantiate_msg).unwrap();
829
830 let bob = String::from("bob");
832 let info = mock_info(&bob, &coins(1000, "ustake"));
833 execute(deps.as_mut(), mock_env(), info, ExecuteMsg::Bond {}).unwrap();
834 set_delegation(&mut deps.querier, 1000, "ustake");
835
836 let unbond_msg = ExecuteMsg::Unbond {
838 amount: Uint128::new(600),
839 };
840 let env = mock_env();
841 let info = mock_info(&bob, &[]);
842 execute(deps.as_mut(), env.clone(), info.clone(), unbond_msg).unwrap();
843 set_delegation(&mut deps.querier, 460, "ustake");
844
845 let bobs_claim = Uint128::new(540);
847 let original_claims = vec![Claim {
848 amount: bobs_claim,
849 release_at: (DAY * 3).after(&env.block),
850 }];
851 assert_eq!(original_claims, get_claims(deps.as_ref(), &bob));
852
853 let claim_ready = later(&env, (DAY * 3 + HOUR).unwrap());
855 let too_soon = later(&env, DAY);
856 let fail = execute(
857 deps.as_mut(),
858 claim_ready.clone(),
859 info.clone(),
860 ExecuteMsg::Claim {},
861 );
862 assert!(fail.is_err(), "{:?}", fail);
863
864 deps.querier
866 .update_balance(MOCK_CONTRACT_ADDR, coins(540, "ustake"));
867 let fail = execute(deps.as_mut(), too_soon, info.clone(), ExecuteMsg::Claim {});
868 assert!(fail.is_err(), "{:?}", fail);
869
870 let res = execute(deps.as_mut(), claim_ready, info, ExecuteMsg::Claim {}).unwrap();
872 assert_eq!(1, res.messages.len());
873 let payout = &res.messages[0];
874 match &payout.msg {
875 CosmosMsg::Bank(BankMsg::Send { to_address, amount }) => {
876 assert_eq!(amount, &coins(540, "ustake"));
877 assert_eq!(to_address, &bob);
878 }
879 _ => panic!("Unexpected message: {:?}", payout),
880 }
881
882 assert_eq!(get_claims(deps.as_ref(), &bob), vec![]);
884 }
885
886 #[test]
887 fn cw20_imports_work() {
888 let mut deps = mock_dependencies();
889 set_validator(&mut deps.querier);
890
891 let bob = String::from("bob");
893 let alice = String::from("alice");
894 let carl = String::from("carl");
895
896 let creator = String::from("creator");
898 let instantiate_msg = default_instantiate(2, 50);
899 let info = mock_info(&creator, &[]);
900 instantiate(deps.as_mut(), mock_env(), info, instantiate_msg).unwrap();
901
902 let info = mock_info(&bob, &[coin(10, "random"), coin(1000, "ustake")]);
904 execute(deps.as_mut(), mock_env(), info, ExecuteMsg::Bond {}).unwrap();
905
906 assert_eq!(get_balance(deps.as_ref(), &bob), Uint128::new(1000));
908
909 let bob_info = mock_info(&bob, &[]);
911 let transfer = ExecuteMsg::Transfer {
912 recipient: carl.clone(),
913 amount: Uint128::new(200),
914 };
915 execute(deps.as_mut(), mock_env(), bob_info.clone(), transfer).unwrap();
916 assert_eq!(get_balance(deps.as_ref(), &bob), Uint128::new(800));
917 assert_eq!(get_balance(deps.as_ref(), &carl), Uint128::new(200));
918
919 let allow = ExecuteMsg::IncreaseAllowance {
921 spender: alice.clone(),
922 amount: Uint128::new(350),
923 expires: None,
924 };
925 execute(deps.as_mut(), mock_env(), bob_info.clone(), allow).unwrap();
926 assert_eq!(get_balance(deps.as_ref(), &bob), Uint128::new(800));
927 assert_eq!(get_balance(deps.as_ref(), &alice), Uint128::zero());
928 assert_eq!(
929 query_allowance(deps.as_ref(), bob.clone(), alice.clone())
930 .unwrap()
931 .allowance,
932 Uint128::new(350)
933 );
934
935 let self_pay = ExecuteMsg::TransferFrom {
937 owner: bob.clone(),
938 recipient: alice.clone(),
939 amount: Uint128::new(250),
940 };
941 let alice_info = mock_info(&alice, &[]);
942 execute(deps.as_mut(), mock_env(), alice_info, self_pay).unwrap();
943 assert_eq!(get_balance(deps.as_ref(), &bob), Uint128::new(550));
944 assert_eq!(get_balance(deps.as_ref(), &alice), Uint128::new(250));
945 assert_eq!(
946 query_allowance(deps.as_ref(), bob.clone(), alice)
947 .unwrap()
948 .allowance,
949 Uint128::new(100)
950 );
951
952 let burn_too_much = ExecuteMsg::Burn {
954 amount: Uint128::new(1000),
955 };
956 let failed = execute(deps.as_mut(), mock_env(), bob_info.clone(), burn_too_much);
957 assert!(failed.is_err());
958 assert_eq!(get_balance(deps.as_ref(), &bob), Uint128::new(550));
959 let burn = ExecuteMsg::Burn {
960 amount: Uint128::new(130),
961 };
962 execute(deps.as_mut(), mock_env(), bob_info, burn).unwrap();
963 assert_eq!(get_balance(deps.as_ref(), &bob), Uint128::new(420));
964 }
965}