abstract_std/objects/
fee.rs

1use cosmwasm_std::{Addr, Coin, CosmosMsg, Decimal, MessageInfo, Uint128};
2use cw_asset::Asset;
3
4use crate::{error::AbstractError, AbstractResult};
5
6/// A wrapper around Fee to help handle fee logic.
7/// Use this with `Chargeable` trait in the SDK to charge fees on asset structs.
8#[cosmwasm_schema::cw_serde]
9pub struct UsageFee {
10    fee: Fee,
11    recipient: Addr,
12}
13
14impl UsageFee {
15    pub fn new(share: Decimal, recipient: Addr) -> AbstractResult<Self> {
16        let fee = Fee::new(share)?;
17        Ok(Self { fee, recipient })
18    }
19
20    pub fn set_share(&mut self, share: Decimal) -> AbstractResult<()> {
21        self.fee = Fee::new(share)?;
22        Ok(())
23    }
24
25    pub fn share(&self) -> Decimal {
26        self.fee.share()
27    }
28
29    pub fn compute(&self, amount: Uint128) -> Uint128 {
30        amount.mul_floor(self.share())
31    }
32
33    pub fn recipient(&self) -> Addr {
34        self.recipient.clone()
35    }
36
37    pub fn set_recipient(&mut self, recipient: Addr) {
38        self.recipient = recipient;
39    }
40}
41
42/// A wrapper around Decimal to help handle fractional fees.
43#[cosmwasm_schema::cw_serde]
44#[derive(Copy)]
45pub struct Fee {
46    /// fraction of asset to take as fee.
47    share: Decimal,
48}
49
50impl Fee {
51    pub fn new(share: Decimal) -> AbstractResult<Self> {
52        if share >= Decimal::percent(100) {
53            return Err(AbstractError::Fee(
54                "fee share must be lesser than 100%".to_string(),
55            ));
56        }
57        Ok(Fee { share })
58    }
59
60    pub fn compute(&self, amount: Uint128) -> Uint128 {
61        amount.mul_floor(self.share)
62    }
63
64    pub fn msg(&self, asset: Asset, recipient: Addr) -> AbstractResult<CosmosMsg> {
65        asset.transfer_msg(recipient).map_err(Into::into)
66    }
67
68    pub fn share(&self) -> Decimal {
69        self.share
70    }
71}
72
73/// A wrapper around Coin to help handle fixed fees (with multiples).
74#[cosmwasm_schema::cw_serde]
75pub struct FixedFee {
76    /// Fee to be paid for a unit of a service
77    fee: Coin,
78}
79
80impl FixedFee {
81    /// Creates a wrapped coin to allow charging a fee
82    pub fn new(fee: &Coin) -> Self {
83        FixedFee { fee: fee.clone() }
84    }
85    /// Allows to collect the fee multiple times
86    /// E.g., for namespaces, you want to charge the number of claimed namespaces times the fee for 1 namespace
87    pub fn quantity(mut self, qty: u128) -> Self {
88        self.fee.amount *= Uint128::from(qty);
89        self
90    }
91
92    /// Validates that the sent funds correspond exactly to the fixed fee
93    /// Returns the fee object (a.k.a. self) for later use (e.g. transferring the paid fee to another address)
94    pub fn assert_payment(self, msg_info: &MessageInfo) -> AbstractResult<Coin> {
95        if self.fee.amount.is_zero() {
96            return Ok(self.fee);
97        }
98        if msg_info.funds.len() != 1
99            || msg_info.funds[0].denom != self.fee.denom
100            || self.fee.amount != msg_info.funds[0].amount
101        {
102            return Err(AbstractError::Fee(format!(
103                "Invalid fee payment sent. Expected {}, sent {:?}",
104                self.fee, msg_info.funds
105            )));
106        }
107        Ok(self.fee)
108    }
109
110    /// Validates that the sent funds include at least the fixed fee
111    /// This mutates the msg_info so that the rest of the message execution doesn't include those funds anymore.
112    /// This acts as a toll on the sent funds
113    /// Returns the fee object (a.k.a. self) for later use (e.g. transferring the paid fee to another address)
114    pub fn charge(self, msg_info: &mut MessageInfo) -> AbstractResult<Coin> {
115        if self.fee.amount.is_zero() {
116            return Ok(self.fee);
117        }
118        let original_funds = msg_info.funds.clone();
119
120        // We find the fee inside the msg_info
121        let funds_to_use = msg_info
122            .funds
123            .iter_mut()
124            .find(|f| f.denom == self.fee.denom)
125            .ok_or(AbstractError::Fee(format!(
126                "Invalid fee payment sent. Expected {}, sent {:?}",
127                self.fee, original_funds
128            )))?;
129
130        if funds_to_use.amount < self.fee.amount {
131            return Err(AbstractError::Fee(format!(
132                "Invalid fee payment sent. Expected {}, sent {:?}",
133                self.fee, original_funds
134            )));
135        }
136
137        funds_to_use.amount -= self.fee.amount;
138        Ok(self.fee)
139    }
140    pub fn fee(&self) -> Coin {
141        self.fee.clone()
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    mod fee {
150        use super::*;
151
152        #[coverage_helper::test]
153        fn test_fee_manual_construction() {
154            let fee = Fee {
155                share: Decimal::percent(20u64),
156            };
157            let deposit = Uint128::from(1000000u64);
158            let deposit_fee = fee.compute(deposit);
159            assert_eq!(deposit_fee, Uint128::from(200000u64));
160        }
161
162        #[coverage_helper::test]
163        fn test_fee_new() {
164            let fee = Fee::new(Decimal::percent(20u64)).unwrap();
165            let deposit = Uint128::from(1000000u64);
166            let deposit_fee = fee.compute(deposit);
167            assert_eq!(deposit_fee, Uint128::from(200000u64));
168        }
169
170        #[coverage_helper::test]
171        fn test_fee_new_gte_100() {
172            let fee = Fee::new(Decimal::percent(100u64));
173            assert!(fee.is_err());
174            let fee = Fee::new(Decimal::percent(101u64));
175            assert!(fee.is_err());
176        }
177
178        #[coverage_helper::test]
179        fn test_fee_share() {
180            let expected_percent = 20u64;
181            let fee = Fee::new(Decimal::percent(expected_percent)).unwrap();
182            assert_eq!(fee.share(), Decimal::percent(expected_percent));
183        }
184
185        #[coverage_helper::test]
186        fn test_fee_msg() {
187            let fee = Fee::new(Decimal::percent(20u64)).unwrap();
188            let asset = Asset::native("uusd", Uint128::from(1000000u64));
189
190            let recipient = Addr::unchecked("recipient");
191            let msg = fee.msg(asset.clone(), recipient.clone()).unwrap();
192            assert_eq!(msg, asset.transfer_msg(recipient).unwrap(),);
193        }
194    }
195    mod transfer_fee {
196        use cosmwasm_std::{coin, coins, testing::message_info};
197
198        use super::*;
199
200        #[coverage_helper::test]
201        fn test_transfer_fee_new() {
202            let fee = UsageFee::new(Decimal::percent(20u64), Addr::unchecked("recipient")).unwrap();
203            let deposit = Uint128::from(1000000u64);
204            let deposit_fee = fee.compute(deposit);
205            assert_eq!(deposit_fee, Uint128::from(200000u64));
206        }
207
208        #[coverage_helper::test]
209        fn test_transfer_fee_share() {
210            let expected_percent = 20u64;
211            let fee = UsageFee::new(
212                Decimal::percent(expected_percent),
213                Addr::unchecked("recipient"),
214            )
215            .unwrap();
216            assert_eq!(fee.share(), Decimal::percent(expected_percent));
217        }
218
219        #[coverage_helper::test]
220        fn test_transfer_fee_msg() {
221            let fee = UsageFee::new(Decimal::percent(20u64), Addr::unchecked("recipient")).unwrap();
222            let asset = Asset::native("uusd", Uint128::from(1000000u64));
223
224            let recipient = Addr::unchecked("recipient");
225            let msg = fee.fee.msg(asset.clone(), recipient.clone()).unwrap();
226            assert_eq!(msg, asset.transfer_msg(recipient).unwrap(),);
227        }
228
229        #[coverage_helper::test]
230        fn test_transfer_fee_new_gte_100() {
231            let fee = UsageFee::new(Decimal::percent(100u64), Addr::unchecked("recipient"));
232            assert!(fee.is_err());
233            let fee = UsageFee::new(Decimal::percent(101u64), Addr::unchecked("recipient"));
234            assert!(fee.is_err());
235        }
236
237        #[coverage_helper::test]
238        fn test_transfer_fee_set_recipient() {
239            let mut fee =
240                UsageFee::new(Decimal::percent(20u64), Addr::unchecked("recipient")).unwrap();
241            let new_recipient = Addr::unchecked("new_recipient");
242            fee.set_recipient(new_recipient.clone());
243            assert_eq!(fee.recipient(), Addr::unchecked(new_recipient));
244        }
245        #[coverage_helper::test]
246        fn test_transfer_fee_set_share() {
247            let mut fee =
248                UsageFee::new(Decimal::percent(20u64), Addr::unchecked("recipient")).unwrap();
249            let new_share = Decimal::percent(10u64);
250            fee.set_share(new_share).unwrap();
251            assert_eq!(fee.share(), new_share);
252        }
253        #[coverage_helper::test]
254        fn test_loose_fee_validation() {
255            let fee = FixedFee::new(&coin(45, "ujunox"));
256            let mut info = message_info(&Addr::unchecked("anyone"), &coins(47, "ujunox"));
257            fee.charge(&mut info).unwrap();
258            assert_eq!(info.funds, coins(2, "ujunox"));
259        }
260    }
261}