andromeda_finance/
timelock.rs

1use andromeda_std::{
2    amp::recipient::Recipient,
3    andr_exec, andr_instantiate, andr_instantiate_modules, andr_query,
4    common::{merge_coins, MillisecondsExpiration},
5    error::ContractError,
6};
7use cosmwasm_schema::{cw_serde, QueryResponses};
8use cosmwasm_std::{ensure, Api, BlockInfo, Coin};
9
10#[cw_serde]
11/// Enum used to specify the condition which must be met in order for the Escrow to unlock.
12pub enum EscrowCondition {
13    /// Requires a given time or block height to be reached.
14    Expiration(MillisecondsExpiration),
15    /// Requires a minimum amount of funds to be deposited.
16    MinimumFunds(Vec<Coin>),
17}
18
19#[cw_serde]
20/// Struct used to define funds being held in Escrow
21pub struct Escrow {
22    /// Funds being held within the Escrow
23    pub coins: Vec<Coin>,
24    /// Optional condition for the Escrow
25    pub condition: Option<EscrowCondition>,
26    /// The recipient of the funds once Condition is satisfied
27    pub recipient: Recipient,
28    /// Used for indexing.
29    pub recipient_addr: String,
30}
31
32impl Escrow {
33    /// Used to check the validity of an Escrow before it is stored.
34    ///
35    /// * Escrowed funds cannot be empty
36    /// * The Escrow recipient must be a valid address
37    /// * Expiration cannot be "Never" or before current time/block
38    pub fn validate(&self, api: &dyn Api, block: &BlockInfo) -> Result<(), ContractError> {
39        ensure!(
40            !self.coins.is_empty(),
41            ContractError::InvalidFunds {
42                msg: "At least one coin should be sent".to_string(),
43            }
44        );
45        ensure!(
46            api.addr_validate(&self.recipient_addr).is_ok(),
47            ContractError::InvalidAddress {}
48        );
49
50        if let Some(EscrowCondition::MinimumFunds(funds)) = &self.condition {
51            ensure!(
52                !funds.is_empty(),
53                ContractError::InvalidFunds {
54                    msg: "Minumum funds must not be empty".to_string(),
55                }
56            );
57            let mut funds: Vec<Coin> = funds.clone();
58            funds.sort_by(|a, b| a.denom.cmp(&b.denom));
59            for i in 0..funds.len() - 1 {
60                ensure!(
61                    funds[i].denom != funds[i + 1].denom,
62                    ContractError::DuplicateCoinDenoms {}
63                );
64            }
65            // Explicitly stop here as it is alright if the Escrow is unlocked in this case, ie,
66            // the intially deposited funds are greater or equal to the minimum imposed by this
67            // condition.
68            return Ok(());
69        }
70
71        ensure!(
72            self.is_locked(block)? || self.condition.is_none(),
73            ContractError::ExpirationInPast {}
74        );
75        Ok(())
76    }
77
78    /// Checks if the unlock condition has been met.
79    pub fn is_locked(&self, block: &BlockInfo) -> Result<bool, ContractError> {
80        match &self.condition {
81            None => Ok(false),
82            Some(condition) => match condition {
83                EscrowCondition::Expiration(expiration) => Ok(!expiration.is_in_past(block)),
84                EscrowCondition::MinimumFunds(funds) => {
85                    Ok(!self.min_funds_deposited(funds.clone()))
86                }
87            },
88        }
89    }
90
91    /// Checks if funds deposited in escrow are a subset of `required_funds`. In practice this is
92    /// used for the `EscrowCondition::MinimumFunds(funds)` condition.
93    fn min_funds_deposited(&self, required_funds: Vec<Coin>) -> bool {
94        required_funds.iter().all(|required_coin| {
95            self.coins.iter().any(|deposited_coin| {
96                deposited_coin.denom == required_coin.denom
97                    && required_coin.amount <= deposited_coin.amount
98            })
99        })
100    }
101
102    /// Adds coins in `coins_to_add` to `self.coins` by merging those of the same denom and
103    /// otherwise appending.
104    ///
105    /// ## Arguments
106    /// * `&mut self`    - Mutable reference to an instance of Escrow
107    /// * `coins_to_add` - The `Vec<Coin>` to add, it is assumed that it contains no coins of the
108    ///                    same denom
109    ///
110    /// Returns nothing as it is done in place.
111    pub fn add_funds(&mut self, coins_to_add: Vec<Coin>) {
112        self.coins = merge_coins(self.coins.to_vec(), coins_to_add);
113    }
114}
115
116#[andr_instantiate]
117#[andr_instantiate_modules]
118#[cw_serde]
119pub struct InstantiateMsg {}
120
121#[andr_exec]
122#[cw_serde]
123pub enum ExecuteMsg {
124    /// Hold funds in Escrow
125    HoldFunds {
126        condition: Option<EscrowCondition>,
127        recipient: Option<Recipient>,
128    },
129    /// Release funds all held in Escrow for the given recipient
130    ReleaseFunds {
131        recipient_addr: Option<String>,
132        start_after: Option<String>,
133        limit: Option<u32>,
134    },
135    ReleaseSpecificFunds {
136        owner: String,
137        recipient_addr: Option<String>,
138    },
139}
140#[andr_query]
141#[cw_serde]
142#[derive(QueryResponses)]
143pub enum QueryMsg {
144    /// Queries funds held by an address
145    #[returns(GetLockedFundsResponse)]
146    GetLockedFunds { owner: String, recipient: String },
147    /// Queries the funds for the given recipient.
148    #[returns(GetLockedFundsForRecipientResponse)]
149    GetLockedFundsForRecipient {
150        recipient: String,
151        start_after: Option<String>,
152        limit: Option<u32>,
153    },
154}
155
156#[cw_serde]
157#[serde(rename_all = "snake_case")]
158pub struct GetLockedFundsResponse {
159    pub funds: Option<Escrow>,
160}
161
162#[cw_serde]
163#[serde(rename_all = "snake_case")]
164pub struct GetLockedFundsForRecipientResponse {
165    pub funds: Vec<Escrow>,
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use andromeda_std::common::Milliseconds;
172    use cosmwasm_std::testing::mock_dependencies;
173    use cosmwasm_std::{coin, Timestamp};
174
175    #[test]
176    fn test_validate() {
177        let deps = mock_dependencies();
178        let condition = EscrowCondition::Expiration(Milliseconds::from_seconds(101));
179        let coins = vec![coin(100u128, "uluna")];
180        let recipient = Recipient::from_string("owner");
181
182        let valid_escrow = Escrow {
183            recipient: recipient.clone(),
184            coins: coins.clone(),
185            condition: Some(condition.clone()),
186            recipient_addr: "owner".to_string(),
187        };
188        let block = BlockInfo {
189            height: 1000,
190            time: Timestamp::from_seconds(100),
191            chain_id: "foo".to_string(),
192        };
193        valid_escrow.validate(deps.as_ref().api, &block).unwrap();
194
195        let valid_escrow = Escrow {
196            recipient: recipient.clone(),
197            coins: coins.clone(),
198            condition: None,
199            recipient_addr: "owner".to_string(),
200        };
201        let block = BlockInfo {
202            height: 1000,
203            time: Timestamp::from_seconds(3333),
204            chain_id: "foo".to_string(),
205        };
206        valid_escrow.validate(deps.as_ref().api, &block).unwrap();
207
208        let invalid_recipient_escrow = Escrow {
209            recipient: Recipient::from_string(String::default()),
210            coins: coins.clone(),
211            condition: Some(condition.clone()),
212            recipient_addr: String::default(),
213        };
214
215        let resp = invalid_recipient_escrow
216            .validate(deps.as_ref().api, &block)
217            .unwrap_err();
218        assert_eq!(ContractError::InvalidAddress {}, resp);
219
220        let invalid_coins_escrow = Escrow {
221            recipient: recipient.clone(),
222            coins: vec![],
223            condition: Some(condition),
224            recipient_addr: "owner".to_string(),
225        };
226
227        let resp = invalid_coins_escrow
228            .validate(deps.as_ref().api, &block)
229            .unwrap_err();
230        assert_eq!(
231            ContractError::InvalidFunds {
232                msg: "At least one coin should be sent".to_string()
233            },
234            resp
235        );
236
237        let invalid_time_escrow = Escrow {
238            recipient,
239            coins,
240            condition: Some(EscrowCondition::Expiration(Milliseconds::from_seconds(0))),
241            recipient_addr: "owner".to_string(),
242        };
243        let block = BlockInfo {
244            height: 1000,
245            time: Timestamp::from_seconds(1),
246            chain_id: "foo".to_string(),
247        };
248        assert_eq!(
249            ContractError::ExpirationInPast {},
250            invalid_time_escrow
251                .validate(deps.as_ref().api, &block)
252                .unwrap_err()
253        );
254    }
255
256    #[test]
257    fn test_validate_funds_condition() {
258        let deps = mock_dependencies();
259        let recipient = Recipient::from_string("owner");
260
261        let valid_escrow = Escrow {
262            recipient: recipient.clone(),
263            coins: vec![coin(100, "uluna")],
264            condition: Some(EscrowCondition::MinimumFunds(vec![
265                coin(100, "uusd"),
266                coin(100, "uluna"),
267            ])),
268            recipient_addr: "owner".to_string(),
269        };
270        let block = BlockInfo {
271            height: 1000,
272            time: Timestamp::from_seconds(4444),
273            chain_id: "foo".to_string(),
274        };
275        valid_escrow.validate(deps.as_ref().api, &block).unwrap();
276
277        // Funds exceed minimum
278        let valid_escrow = Escrow {
279            recipient: recipient.clone(),
280            coins: vec![coin(200, "uluna")],
281            condition: Some(EscrowCondition::MinimumFunds(vec![coin(100, "uluna")])),
282            recipient_addr: "owner".to_string(),
283        };
284        valid_escrow.validate(deps.as_ref().api, &block).unwrap();
285
286        // Empty funds
287        let invalid_escrow = Escrow {
288            recipient: recipient.clone(),
289            coins: vec![coin(100, "uluna")],
290            condition: Some(EscrowCondition::MinimumFunds(vec![])),
291            recipient_addr: "owner".to_string(),
292        };
293        assert_eq!(
294            ContractError::InvalidFunds {
295                msg: "Minumum funds must not be empty".to_string(),
296            },
297            invalid_escrow
298                .validate(deps.as_ref().api, &block)
299                .unwrap_err()
300        );
301
302        // Duplicate funds
303        let invalid_escrow = Escrow {
304            recipient,
305            coins: vec![coin(100, "uluna")],
306            condition: Some(EscrowCondition::MinimumFunds(vec![
307                coin(100, "uusd"),
308                coin(100, "uluna"),
309                coin(200, "uusd"),
310            ])),
311            recipient_addr: "owner".to_string(),
312        };
313        assert_eq!(
314            ContractError::DuplicateCoinDenoms {},
315            invalid_escrow
316                .validate(deps.as_ref().api, &block)
317                .unwrap_err()
318        );
319    }
320
321    #[test]
322    fn test_min_funds_deposited() {
323        let recipient = Recipient::from_string("owner");
324        let escrow = Escrow {
325            recipient: recipient.clone(),
326            coins: vec![coin(100, "uluna")],
327            condition: None,
328            recipient_addr: "owner".to_string(),
329        };
330        assert!(!escrow.min_funds_deposited(vec![coin(100, "uusd")]));
331
332        let escrow = Escrow {
333            recipient: recipient.clone(),
334            coins: vec![coin(100, "uluna")],
335            condition: None,
336            recipient_addr: "owner".to_string(),
337        };
338        assert!(!escrow.min_funds_deposited(vec![coin(100, "uusd"), coin(100, "uluna")]));
339
340        let escrow = Escrow {
341            recipient: recipient.clone(),
342            coins: vec![coin(100, "uluna")],
343            condition: None,
344            recipient_addr: "owner".to_string(),
345        };
346        assert!(escrow.min_funds_deposited(vec![coin(100, "uluna")]));
347
348        let escrow = Escrow {
349            recipient,
350            coins: vec![coin(200, "uluna")],
351            condition: None,
352            recipient_addr: "owner".to_string(),
353        };
354        assert!(escrow.min_funds_deposited(vec![coin(100, "uluna")]));
355    }
356
357    #[test]
358    fn test_add_funds() {
359        let mut escrow = Escrow {
360            coins: vec![coin(100, "uusd"), coin(100, "uluna")],
361            condition: None,
362            recipient: Recipient::from_string(""),
363            recipient_addr: "".to_string(),
364        };
365        let funds_to_add = vec![coin(25, "uluna"), coin(50, "uusd"), coin(100, "ucad")];
366
367        escrow.add_funds(funds_to_add);
368        assert_eq!(
369            vec![coin(150, "uusd"), coin(125, "uluna"), coin(100, "ucad")],
370            escrow.coins
371        );
372    }
373}