cw20_staking/
contract.rs

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
24// version info for migration info
25const 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    // ensure the validator is registered
38    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    // store token info using cw20-base format
46    let data = TokenInfo {
47        name: msg.name,
48        symbol: msg.symbol,
49        decimals: msg.decimals,
50        total_supply: Uint128::zero(),
51        // set self as minter, so we can properly execute mint and burn
52        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    // set supply to 0
71    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        // these all come from cw20-base to implement the cw20 standard
92        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
136// get_bonded returns the total amount of delegations from contract
137// it ensures they are all the same denom
138fn 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    // ensure we have the proper denom
170    let invest = INVESTMENT.load(deps.storage)?;
171    // payment finds the proper coin (or throws an error)
172    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    // bonded is the total number of tokens we have delegated from this address
181    let bonded = get_bonded(&deps.querier, &env.contract.address)?;
182
183    // calculate to_mint and update total supply
184    let mut supply = TOTAL_SUPPLY.load(deps.storage)?;
185    // TODO: this is just a safety assertion - do we keep it, or remove caching?
186    // in the end supply is just there to cache the (expected) results of get_bonded() so we don't
187    // have expensive queries everywhere
188    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    // call into cw20-base to mint the token, call as self as no one else is allowed
199    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    // bond them to the validator
206    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    // ensure it is big enough to care
226    if amount < invest.min_withdrawal {
227        return Err(ContractError::UnbondTooSmall {
228            min_bonded: invest.min_withdrawal,
229            denom: invest.bond_denom,
230        });
231    }
232    // calculate tax and remainer to unbond
233    let tax = amount * invest.exit_tax;
234
235    // burn from the original caller
236    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        // call into cw20-base to mint tokens to owner, call as self as no one else is allowed
243        execute_mint(
244            deps.branch(),
245            env.clone(),
246            sub_info,
247            invest.owner.to_string(),
248            tax,
249        )?;
250    }
251
252    // re-calculate bonded to ensure we have real values
253    // bonded is the total number of tokens we have delegated from this address
254    let bonded = get_bonded(&deps.querier, &env.contract.address)?;
255
256    // calculate how many native tokens this is worth and update supply
257    let remainder = amount.checked_sub(tax).map_err(StdError::overflow)?;
258    let mut supply = TOTAL_SUPPLY.load(deps.storage)?;
259    // TODO: this is just a safety assertion - do we keep it, or remove caching?
260    // in the end supply is just there to cache the (expected) results of get_bonded() so we don't
261    // have expensive queries everywhere
262    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    // unbond them
280    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    // find how many tokens the contract has
294    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    // check how much to send - min(balance, claims[sender]), and reduce the claim
303    // Ensure we have enough balance to cover this and only send some claims if that is all we can cover
304    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    // update total supply (lower claim)
311    TOTAL_SUPPLY.update(deps.storage, |mut supply| -> StdResult<_> {
312        supply.claims = supply.claims.checked_sub(to_send)?;
313        Ok(supply)
314    })?;
315
316    // transfer tokens to the sender
317    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
329/// reinvest will withdraw all pending rewards,
330/// then issue a callback to itself via _bond_all_tokens
331/// to reinvest the new earnings (and anything else that accumulated)
332pub 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    // and bond them to the validator
338    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    // this is just meant as a call-back to ourself
356    if info.sender != env.contract.address {
357        return Err(ContractError::Unauthorized {});
358    }
359
360    // find how many tokens we have to bond
361    let invest = INVESTMENT.load(deps.storage)?;
362    let mut balance = deps
363        .querier
364        .query_balance(&env.contract.address, &invest.bond_denom)?;
365
366    // we deduct pending claims from our account balance before reinvesting.
367    // if there is not enough funds, we just return a no-op
368    match TOTAL_SUPPLY.update(deps.storage, |mut supply| -> StdResult<_> {
369        balance.amount = balance.amount.checked_sub(supply.claims)?;
370        // this just triggers the "no op" case if we don't have min_withdrawal left to reinvest
371        balance.amount.checked_sub(invest.min_withdrawal)?;
372        supply.bonded += balance.amount;
373        Ok(supply)
374    }) {
375        Ok(_) => {}
376        // if it is below the minimum, we do a no-op (do not revert other state from withdrawal)
377        Err(StdError::Overflow { .. }) => return Ok(Response::default()),
378        Err(e) => return Err(ContractError::Std(e)),
379    }
380
381    // and bond them to the validator
382    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        // custom queries
396        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        // inherited from cw20-base
401        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    // just a test helper, forgive the panic
478    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        // make sure we can instantiate with this
532        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        // make sure we can instantiate with this
567        let res = instantiate(deps.as_mut(), mock_env(), info, msg.clone()).unwrap();
568        assert_eq!(0, res.messages.len());
569
570        // token info is proper
571        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        // no balance
578        assert_eq!(get_balance(deps.as_ref(), &creator), Uint128::zero());
579        // no claims
580        assert_eq!(get_claims(deps.as_ref(), &creator), vec![]);
581
582        // investment info correct
583        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        // make sure we can instantiate with this
604        let res = instantiate(deps.as_mut(), mock_env(), info, instantiate_msg).unwrap();
605        assert_eq!(0, res.messages.len());
606
607        // let's bond some tokens now
608        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        // try to bond and make sure we trigger delegation
613        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        // bob got 1000 DRV for 1000 stake at a 1.0 ratio
625        assert_eq!(get_balance(deps.as_ref(), &bob), Uint128::new(1000));
626
627        // investment info correct (updated supply)
628        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        // token info also properly updated
634        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        // make sure we can instantiate with this
648        let res = instantiate(deps.as_mut(), mock_env(), info, instantiate_msg).unwrap();
649        assert_eq!(0, res.messages.len());
650
651        // let's bond some tokens now
652        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        // update the querier with new bond
659        set_delegation(&mut deps.querier, 1000, "ustake");
660
661        // fake a reinvestment (this must be sent by the contract itself)
662        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        // update the querier with new bond
669        set_delegation(&mut deps.querier, 1500, "ustake");
670
671        // we should now see 1000 issues and 1500 bonded (and a price of 1.5)
672        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        // we bond some other tokens and get a different issuance price (maintaining the ratio)
679        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        // update the querier with new bond
686        set_delegation(&mut deps.querier, 3000, "ustake");
687
688        // alice should have gotten 2000 DRV for the 3000 stake, keeping the ratio at 1.5
689        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        // make sure we can instantiate with this
707        let res = instantiate(deps.as_mut(), mock_env(), info, instantiate_msg).unwrap();
708        assert_eq!(0, res.messages.len());
709
710        // let's bond some tokens now
711        let bob = String::from("bob");
712        let bond_msg = ExecuteMsg::Bond {};
713        let info = mock_info(&bob, &[coin(500, "photon")]);
714
715        // try to bond and make sure we trigger delegation
716        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        // make sure we can instantiate with this
735        let res = instantiate(deps.as_mut(), mock_env(), info, instantiate_msg).unwrap();
736        assert_eq!(0, res.messages.len());
737
738        // let's bond some tokens now
739        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        // update the querier with new bond
746        set_delegation(&mut deps.querier, 1000, "ustake");
747
748        // fake a reinvestment (this must be sent by the contract itself)
749        // after this, we see 1000 issues and 1500 bonded (and a price of 1.5)
750        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        // update the querier with new bond, lower balance
757        set_delegation(&mut deps.querier, 1500, "ustake");
758        deps.querier.update_balance(MOCK_CONTRACT_ADDR, vec![]);
759
760        // creator now tries to unbond these tokens - this must fail
761        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        // bob unbonds 600 tokens at 10% tax...
776        // 60 are taken and send to the owner
777        // 540 are unbonded in exchange for 540 * 1.5 = 810 native tokens
778        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        // update the querier with new bond, lower balance
798        set_delegation(&mut deps.querier, 690, "ustake");
799
800        // check balances
801        assert_eq!(get_balance(deps.as_ref(), &bob), bobs_balance);
802        assert_eq!(get_balance(deps.as_ref(), &creator), owner_cut);
803        // proper claims
804        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        // supplies updated, ratio the same (1.5)
811        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")); // 1500 - 810
816        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        // create contract
825        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        // bond some tokens
831        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        // unbond part of them
837        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        // ensure claims are proper
846        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        // bob cannot exercise claims without enough balance
854        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        // provide the balance, but claim not yet mature - also prohibited
865        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        // this should work with cash and claims ready
871        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        // claims have been removed
883        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        // set the actors... bob stakes, sends coins to carl, and gives allowance to alice
892        let bob = String::from("bob");
893        let alice = String::from("alice");
894        let carl = String::from("carl");
895
896        // create the contract
897        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        // bond some tokens to create a balance
903        let info = mock_info(&bob, &[coin(10, "random"), coin(1000, "ustake")]);
904        execute(deps.as_mut(), mock_env(), info, ExecuteMsg::Bond {}).unwrap();
905
906        // bob got 1000 DRV for 1000 stake at a 1.0 ratio
907        assert_eq!(get_balance(deps.as_ref(), &bob), Uint128::new(1000));
908
909        // send coins to carl
910        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        // allow alice
920        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        // alice takes some for herself
936        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        // burn some, but not too much
953        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}