andromeda_modules/rates.rs
1use andromeda_std::{
2 amp::recipient::Recipient, andr_exec, andr_instantiate, andr_query, error::ContractError,
3};
4use cosmwasm_schema::{cw_serde, QueryResponses};
5use cosmwasm_std::{ensure, Coin, Decimal, Fraction, QuerierWrapper};
6
7#[andr_instantiate]
8#[cw_serde]
9pub struct InstantiateMsg {
10 pub rates: Vec<RateInfo>,
11}
12
13#[andr_exec]
14#[cw_serde]
15pub enum ExecuteMsg {
16 UpdateRates { rates: Vec<RateInfo> },
17}
18
19#[andr_query]
20#[cw_serde]
21#[derive(QueryResponses)]
22pub enum QueryMsg {
23 #[returns(PaymentsResponse)]
24 Payments {},
25}
26
27#[cw_serde]
28pub struct PaymentsResponse {
29 pub payments: Vec<RateInfo>,
30}
31
32#[cw_serde]
33pub struct RateInfo {
34 pub rate: Rate,
35 pub is_additive: bool,
36 pub description: Option<String>,
37 pub recipients: Vec<Recipient>,
38}
39
40#[cw_serde]
41/// An enum used to define various types of fees
42pub enum Rate {
43 /// A flat rate fee
44 Flat(Coin),
45 /// A percentage fee
46 Percent(PercentRate),
47 // External(PrimitivePointer),
48}
49
50#[cw_serde] // This is added such that both Rate::Flat and Rate::Percent have the same level of nesting which
51 // makes it easier to work with on the frontend.
52pub struct PercentRate {
53 pub percent: Decimal,
54}
55
56impl From<Decimal> for Rate {
57 fn from(decimal: Decimal) -> Self {
58 Rate::Percent(PercentRate { percent: decimal })
59 }
60}
61
62impl Rate {
63 /// Validates that a given rate is non-zero. It is expected that the Rate is not an
64 /// External Rate.
65 pub fn is_non_zero(&self) -> Result<bool, ContractError> {
66 match self {
67 Rate::Flat(coin) => Ok(!coin.amount.is_zero()),
68 Rate::Percent(PercentRate { percent }) => Ok(!percent.is_zero()),
69 // Rate::External(_) => Err(ContractError::UnexpectedExternalRate {}),
70 }
71 }
72
73 /// Validates `self` and returns an "unwrapped" version of itself wherein if it is an External
74 /// Rate, the actual rate value is retrieved from the Primitive Contract.
75 pub fn validate(&self, querier: &QuerierWrapper) -> Result<Rate, ContractError> {
76 let rate = self.clone().get_rate(querier)?;
77 ensure!(rate.is_non_zero()?, ContractError::InvalidRate {});
78
79 if let Rate::Percent(PercentRate { percent }) = rate {
80 ensure!(percent <= Decimal::one(), ContractError::InvalidRate {});
81 }
82
83 Ok(rate)
84 }
85
86 /// If `self` is Flat or Percent it returns itself. Otherwise it queries the primitive contract
87 /// and retrieves the actual Flat or Percent rate.
88 fn get_rate(self, _querier: &QuerierWrapper) -> Result<Rate, ContractError> {
89 match self {
90 Rate::Flat(_) => Ok(self),
91 Rate::Percent(_) => Ok(self),
92 // Rate::External(primitive_pointer) => {
93 // let primitive = primitive_pointer.into_value(querier)?;
94 // match primitive {
95 // None => Err(ContractError::ParsingError {
96 // err: "Stored primitive is None".to_string(),
97 // }),
98 // Some(primitive) => match primitive {
99 // Primitive::Coin(coin) => Ok(Rate::Flat(coin)),
100 // Primitive::Decimal(value) => Ok(Rate::from(value)),
101 // _ => Err(ContractError::ParsingError {
102 // err: "Stored rate is not a coin or Decimal".to_string(),
103 // }),
104 // },
105 // }
106 // }
107 }
108 }
109}
110
111/// An attribute struct used for any events that involve a payment
112pub struct PaymentAttribute {
113 /// The amount paid
114 pub amount: Coin,
115 /// The address the payment was made to
116 pub receiver: String,
117}
118
119impl ToString for PaymentAttribute {
120 fn to_string(&self) -> String {
121 format!("{}<{}", self.receiver, self.amount)
122 }
123}
124
125/// Calculates a fee amount given a `Rate` and payment amount.
126///
127/// ## Arguments
128/// * `fee_rate` - The `Rate` of the fee to be paid
129/// * `payment` - The amount used to calculate the fee
130///
131/// Returns the fee amount in a `Coin` struct.
132pub fn calculate_fee(fee_rate: Rate, payment: &Coin) -> Result<Coin, ContractError> {
133 match fee_rate {
134 Rate::Flat(rate) => Ok(Coin::new(rate.amount.u128(), rate.denom)),
135 Rate::Percent(PercentRate { percent }) => {
136 // [COM-03] Make sure that fee_rate between 0 and 100.
137 ensure!(
138 // No need for rate >=0 due to type limits (Question: Should add or remove?)
139 percent <= Decimal::one() && !percent.is_zero(),
140 ContractError::InvalidRate {}
141 );
142 let mut fee_amount = payment.amount * percent;
143
144 // Always round any remainder up and prioritise the fee receiver.
145 // Inverse of percent will always exist.
146 let reversed_fee = fee_amount * percent.inv().unwrap();
147 if payment.amount > reversed_fee {
148 // [COM-1] Added checked add to fee_amount rather than direct increment
149 fee_amount = fee_amount.checked_add(1u128.into())?;
150 }
151 Ok(Coin::new(fee_amount.u128(), payment.denom.clone()))
152 } // Rate::External(_) => Err(ContractError::UnexpectedExternalRate {}),
153 }
154}
155
156#[cfg(test)]
157mod tests {
158
159 use cosmwasm_std::{coin, Uint128};
160
161 use super::*;
162
163 // #[test]
164 // fn test_validate_external_rate() {
165 // let deps = mock_dependencies_custom(&[]);
166
167 // let rate = Rate::External(PrimitivePointer {
168 // address: MOCK_PRIMITIVE_CONTRACT.to_owned(),
169
170 // key: Some("percent".to_string()),
171 // });
172 // let validated_rate = rate.validate(&deps.as_ref().querier).unwrap();
173 // let expected_rate = Rate::from(Decimal::percent(1));
174 // assert_eq!(expected_rate, validated_rate);
175
176 // let rate = Rate::External(PrimitivePointer {
177 // address: MOCK_PRIMITIVE_CONTRACT.to_owned(),
178 // key: Some("flat".to_string()),
179 // });
180 // let validated_rate = rate.validate(&deps.as_ref().querier).unwrap();
181 // let expected_rate = Rate::Flat(coin(1u128, "uusd"));
182 // assert_eq!(expected_rate, validated_rate);
183 // }
184
185 #[test]
186 fn test_calculate_fee() {
187 let payment = coin(101, "uluna");
188 let expected = Ok(coin(5, "uluna"));
189 let fee = Rate::from(Decimal::percent(4));
190
191 let received = calculate_fee(fee, &payment);
192
193 assert_eq!(expected, received);
194
195 assert_eq!(expected, received);
196
197 let payment = coin(125, "uluna");
198 let fee = Rate::Flat(Coin {
199 amount: Uint128::from(5_u128),
200 denom: "uluna".to_string(),
201 });
202
203 let received = calculate_fee(fee, &payment);
204
205 assert_eq!(expected, received);
206 }
207}