cw20_stake/math.rs
1use std::{convert::TryInto, ops::Div};
2
3use cosmwasm_std::{Uint128, Uint256};
4
5/// Computes the amount to add to an address' staked balance when
6/// staking.
7///
8/// # Arguments
9///
10/// * `staked_total` - The number of tokens that have been staked.
11/// * `balance` - The number of tokens the contract has (staked_total + rewards).
12/// * `sent` - The number of tokens the user has sent to be staked.
13pub(crate) fn amount_to_stake(staked_total: Uint128, balance: Uint128, sent: Uint128) -> Uint128 {
14 if staked_total.is_zero() || balance.is_zero() {
15 sent
16 } else {
17 staked_total
18 .full_mul(sent)
19 .div(Uint256::from(balance))
20 .try_into()
21 .unwrap() // balance := staked_total + rewards
22 // => balance >= staked_total
23 // => staked_total / balance <= 1
24 // => staked_total * sent / balance <= sent
25 // => we can safely unwrap here as sent fits into a u128 by construction.
26 }
27}
28
29/// Computes the number of tokens to return to an address when
30/// claiming.
31///
32/// # Arguments
33///
34/// * `staked_total` - The number of tokens that have been staked.
35/// * `balance` - The number of tokens the contract has (staked_total + rewards).
36/// * `ask` - The number of tokens being claimed.
37///
38/// # Invariants
39///
40/// These must be checked by the caller. If checked, this function is
41/// guarenteed not to panic.
42///
43/// 1. staked_total != 0.
44/// 2. ask + balance <= 2^128
45/// 3. ask <= staked_total
46///
47/// For information on the panic conditions for math, see:
48/// <https://rust-lang.github.io/rfcs/0560-integer-overflow.html>
49pub(crate) fn amount_to_claim(staked_total: Uint128, balance: Uint128, ask: Uint128) -> Uint128 {
50 // we know that:
51 //
52 // 1. cw20's max supply is 2^128
53 // 2. balance := staked_total + rewards
54 //
55 // for non-malicious inputs:
56 //
57 // 3. 1 => ask + balance <= 2^128
58 // 4. ask <= staked_total
59 // 5. staked_total != 0
60 // 6. 4 => ask / staked_total <= 1
61 // 7. 3 => balance <= 2^128
62 // 8. 6 + 7 => ask / staked_total * balance <= 2^128
63 //
64 // which, as addition and division are communative, proves that
65 // ask * balance / staked_total will fit into a 128 bit integer.
66 ask.full_mul(balance)
67 .div(Uint256::from(staked_total))
68 .try_into()
69 .unwrap()
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75
76 #[test]
77 fn test_amount_to_stake_no_overflow() {
78 let sent = Uint128::new(2);
79 let balance = Uint128::MAX - sent;
80
81 let overflows_naively = sent.checked_mul(balance).is_err();
82 assert!(overflows_naively);
83
84 // will panic and fail the test if we've done this wrong.
85 amount_to_stake(balance, balance, sent);
86 }
87
88 #[test]
89 fn test_amount_to_stake_with_zeros() {
90 let sent = Uint128::new(42);
91 let balance = Uint128::zero();
92 let amount = amount_to_stake(balance, balance, sent);
93 assert_eq!(amount, sent);
94 }
95
96 #[test]
97 fn test_amount_to_claim_no_overflow() {
98 let ask = Uint128::new(2);
99 let balance = Uint128::MAX - ask;
100
101 let overflows_naively = ask.checked_mul(balance).is_err();
102 assert!(overflows_naively);
103
104 amount_to_claim(balance, balance, ask);
105 }
106
107 // check that our invariants are indeed invariants.
108
109 #[test]
110 #[should_panic(expected = "attempt to divide by zero")]
111 fn test_amount_to_claim_invariant_one() {
112 let ask = Uint128::new(2);
113 let balance = Uint128::zero();
114
115 amount_to_claim(balance, balance, ask);
116 }
117
118 #[test]
119 #[should_panic(expected = "ConversionOverflowError")]
120 fn test_amount_to_claim_invariant_two() {
121 // Could end up in a situation like this if there are a lot of
122 // rewards, but very few staked tokens.
123 let ask = Uint128::new(2);
124 let balance = Uint128::MAX;
125 let staked_total = Uint128::new(1);
126
127 amount_to_claim(staked_total, balance, ask);
128 }
129
130 #[test]
131 #[should_panic(expected = "ConversionOverflowError")]
132 fn test_amount_to_claim_invariant_three() {
133 let ask = Uint128::new(2);
134 let balance = Uint128::MAX;
135 let staked_total = Uint128::new(1);
136
137 amount_to_claim(staked_total, balance, ask);
138 }
139}