croncat_sdk_manager/
types.rs

1use cosmwasm_schema::cw_serde;
2use cosmwasm_std::{Addr, Coin, StdError, StdResult, Uint128};
3use croncat_sdk_core::types::GasPrice;
4use cw20::Cw20CoinVerified;
5
6use crate::error::SdkError;
7
8#[cw_serde]
9pub struct TaskBalanceResponse {
10    pub balance: Option<TaskBalance>,
11}
12#[cw_serde]
13pub struct TaskBalance {
14    pub native_balance: Uint128,
15    pub cw20_balance: Option<Cw20CoinVerified>,
16    pub ibc_balance: Option<Coin>,
17}
18
19impl TaskBalance {
20    pub fn verify_enough_attached(
21        &self,
22        native_required: Uint128,
23        cw20_required: Option<Cw20CoinVerified>,
24        ibc_required: Option<Coin>,
25        recurring: bool,
26        native_denom: &str,
27    ) -> Result<(), SdkError> {
28        let multiplier = if recurring {
29            Uint128::new(2)
30        } else {
31            Uint128::new(1)
32        };
33        if self.native_balance < native_required * multiplier {
34            return Err(SdkError::NotEnoughNative {
35                denom: native_denom.to_owned(),
36                lack: native_required * multiplier - self.native_balance,
37            });
38        }
39        self.verify_enough_cw20(cw20_required, multiplier)?;
40        match (ibc_required, &self.ibc_balance) {
41            (Some(req), Some(attached)) => {
42                if req.denom != attached.denom {
43                    return Err(SdkError::NotEnoughNative {
44                        denom: req.denom,
45                        lack: req.amount * multiplier,
46                    });
47                }
48                if attached.amount < req.amount * multiplier {
49                    return Err(SdkError::NotEnoughNative {
50                        denom: req.denom,
51                        lack: req.amount * multiplier - attached.amount,
52                    });
53                }
54            }
55            (Some(req), None) => {
56                return Err(SdkError::NotEnoughNative {
57                    denom: req.denom,
58                    lack: req.amount * multiplier,
59                })
60            }
61            // Dont want untracked or differing CW20s from required
62            (None, Some(_)) => {
63                return Err(SdkError::NonRequiredDenom {});
64            }
65            // nothing attached, nothing required
66            (None, None) => (),
67        }
68        Ok(())
69    }
70
71    pub fn verify_enough_cw20(
72        &self,
73        cw20_required: Option<Cw20CoinVerified>,
74        multiplier: Uint128,
75    ) -> Result<(), SdkError> {
76        match (cw20_required, &self.cw20_balance) {
77            (Some(req), Some(attached)) => {
78                if req.address != attached.address {
79                    return Err(SdkError::NotEnoughCw20 {
80                        addr: req.address.into_string(),
81                        lack: req.amount * multiplier,
82                    });
83                }
84                if attached.amount < req.amount * multiplier {
85                    return Err(SdkError::NotEnoughCw20 {
86                        addr: req.address.into_string(),
87                        lack: req.amount * multiplier - attached.amount,
88                    });
89                }
90                Ok(())
91            }
92            (Some(req), None) => Err(SdkError::NotEnoughCw20 {
93                addr: req.address.into_string(),
94                lack: req.amount * multiplier,
95            }),
96            // Dont want untracked or differing CW20s from required
97            (None, Some(_)) => Err(SdkError::NonRequiredDenom {}),
98            // nothing attached, nothing required
99            (None, None) => Ok(()),
100        }
101    }
102
103    pub fn sub_coin(&mut self, coin: &Coin, native_denom: &str) -> StdResult<()> {
104        if coin.denom == native_denom {
105            self.native_balance = self
106                .native_balance
107                .checked_sub(coin.amount)
108                .map_err(|_| StdError::generic_err("Not enough native balance for operation"))?;
109        } else {
110            match &mut self.ibc_balance {
111                Some(task_coin) if task_coin.denom == coin.denom => {
112                    task_coin.amount = task_coin.amount.checked_sub(coin.amount).map_err(|_| {
113                        StdError::generic_err("Not enough ibc balance for operation")
114                    })?;
115                }
116                _ => {
117                    return Err(StdError::generic_err("No balance found for operation"));
118                }
119            }
120        }
121        Ok(())
122    }
123
124    pub fn sub_cw20(&mut self, cw20: &Cw20CoinVerified) -> StdResult<()> {
125        match &mut self.cw20_balance {
126            Some(task_cw20) if task_cw20.address == cw20.address => {
127                // task_cw20.amount = task_cw20.amount.checked_sub(cw20.amount)?;
128                task_cw20.amount = task_cw20
129                    .amount
130                    .checked_sub(cw20.amount)
131                    .map_err(|_| StdError::generic_err("Not enough cw20 balance for operation"))?;
132            }
133            _ => {
134                // If addresses doesn't match it means we have zero coins
135                // Uint128::zero().checked_sub(cw20.amount)?;
136                return Err(StdError::GenericErr {
137                    msg: "Not enough cw20 balance for operation".to_string(),
138                });
139            }
140        }
141        Ok(())
142    }
143}
144
145#[cw_serde]
146pub struct Config {
147    // Runtime
148    pub owner_addr: Addr,
149
150    /// A multisig admin whose sole responsibility is to pause the contract in event of emergency.
151    /// Must be a different contract address than DAO, cannot be a regular keypair
152    /// Does not have the ability to unpause, must rely on the DAO to assess the situation and act accordingly
153    pub pause_admin: Addr,
154
155    /// Address of the croncat_factory
156    pub croncat_factory_addr: Addr,
157
158    /// Key to query address of the tasks
159    pub croncat_tasks_key: (String, [u8; 2]),
160    /// Key to query address of the agents
161    pub croncat_agents_key: (String, [u8; 2]),
162
163    // Economics
164    pub agent_fee: u16,
165    pub treasury_fee: u16,
166    pub gas_price: GasPrice,
167
168    // Treasury
169    pub treasury_addr: Option<Addr>,
170    pub cw20_whitelist: Vec<Addr>,
171    pub native_denom: String,
172
173    // The default query limit
174    pub limit: u64,
175}
176
177#[cw_serde]
178pub struct UpdateConfig {
179    pub agent_fee: Option<u16>,
180    pub treasury_fee: Option<u16>,
181    pub gas_price: Option<GasPrice>,
182    pub croncat_tasks_key: Option<(String, [u8; 2])>,
183    pub croncat_agents_key: Option<(String, [u8; 2])>,
184    pub treasury_addr: Option<String>,
185    /// Add supported cw20s
186    /// That's seems unfair to undo support of cw20's after user already created a task with it
187    pub cw20_whitelist: Option<Vec<String>>,
188}
189
190#[cfg(test)]
191mod test {
192    use cosmwasm_std::{coin, Addr, Coin, Uint128};
193    use cw20::Cw20CoinVerified;
194
195    use crate::SdkError;
196
197    use super::{GasPrice, TaskBalance};
198
199    #[test]
200    fn gas_price_validation() {
201        assert!(!GasPrice {
202            numerator: 0,
203            denominator: 1,
204            gas_adjustment_numerator: 1
205        }
206        .is_valid());
207
208        assert!(!GasPrice {
209            numerator: 1,
210            denominator: 0,
211            gas_adjustment_numerator: 1
212        }
213        .is_valid());
214
215        assert!(!GasPrice {
216            numerator: 1,
217            denominator: 1,
218            gas_adjustment_numerator: 0
219        }
220        .is_valid());
221
222        assert!(GasPrice {
223            numerator: 1,
224            denominator: 1,
225            gas_adjustment_numerator: 1
226        }
227        .is_valid());
228    }
229
230    #[test]
231    fn gas_price_calculate_test() {
232        // Test with default values
233        let gas_price_wrapper = GasPrice::default();
234        let gas_price = 0.04;
235        let gas_adjustments = 1.5;
236
237        let gas = 200_000;
238        let expected = gas as f64 * gas_adjustments * gas_price;
239        assert_eq!(expected as u128, gas_price_wrapper.calculate(gas).unwrap());
240
241        let gas = 160_000;
242        let expected = gas as f64 * gas_adjustments * gas_price;
243        assert_eq!(expected as u128, gas_price_wrapper.calculate(gas).unwrap());
244
245        let gas = 1_234_000;
246        let expected = gas as f64 * gas_adjustments * gas_price;
247        assert_eq!(expected as u128, gas_price_wrapper.calculate(gas).unwrap());
248
249        // Check custom works
250        let gas_price_wrapper = GasPrice {
251            numerator: 25,
252            denominator: 100,
253            gas_adjustment_numerator: 120,
254        };
255        let gas_price = 0.25;
256        let gas_adjustments = 1.2;
257
258        let gas = 200_000;
259        let expected = gas as f64 * gas_adjustments * gas_price;
260        assert_eq!(expected as u128, gas_price_wrapper.calculate(gas).unwrap());
261
262        let gas = 160_000;
263        let expected = gas as f64 * gas_adjustments * gas_price;
264        assert_eq!(expected as u128, gas_price_wrapper.calculate(gas).unwrap());
265
266        let gas = 1_234_000;
267        let expected = gas as f64 * gas_adjustments * gas_price;
268        assert_eq!(expected as u128, gas_price_wrapper.calculate(gas).unwrap());
269    }
270
271    #[test]
272    fn verify_enough_attached_ok_test() {
273        let native_balance = Uint128::from(100u64);
274        let cw20 = Cw20CoinVerified {
275            address: Addr::unchecked("addr"),
276            amount: Uint128::from(100u64),
277        };
278        let ibc_coin = coin(100, "ibc");
279
280        // Test when cw20_balance and ibc_balance are None
281        let task_balance = TaskBalance {
282            native_balance,
283            cw20_balance: None,
284            ibc_balance: None,
285        };
286        assert!(task_balance
287            .verify_enough_attached(Uint128::from(100u64), None, None, false, "denom")
288            .is_ok());
289        assert!(task_balance
290            .verify_enough_attached(Uint128::from(50u64), None, None, true, "denom")
291            .is_ok());
292
293        // Test with cw20_balance and ibc_balance
294        let task_balance = TaskBalance {
295            native_balance,
296            cw20_balance: Some(cw20.clone()),
297            ibc_balance: Some(ibc_coin.clone()),
298        };
299        // We're now validating you're not adding tokens that never get used, #noMoreBlackHoles
300        assert!(task_balance
301            .verify_enough_attached(Uint128::from(100u64), None, None, false, "denom")
302            .is_err());
303        assert!(task_balance
304            .verify_enough_attached(Uint128::from(50u64), None, None, true, "denom")
305            .is_err());
306        assert!(task_balance
307            .verify_enough_attached(
308                Uint128::from(100u64),
309                Some(cw20),
310                Some(ibc_coin),
311                false,
312                "denom"
313            )
314            .is_ok());
315        assert!(task_balance
316            .verify_enough_attached(
317                Uint128::from(50u64),
318                Some(Cw20CoinVerified {
319                    address: Addr::unchecked("addr"),
320                    amount: Uint128::from(50u64),
321                }),
322                Some(coin(50, "ibc")),
323                true,
324                "denom"
325            )
326            .is_ok());
327    }
328
329    #[test]
330    fn verify_enough_attached_err_test() {
331        let native_balance = Uint128::from(100u64);
332        let cw20 = Cw20CoinVerified {
333            address: Addr::unchecked("addr"),
334            amount: Uint128::from(100u64),
335        };
336        let ibc_coin = coin(100, "ibc");
337
338        // Test when cw20_balance and ibc_balance are None, native_balance is not sufficient
339        let task_balance = TaskBalance {
340            native_balance,
341            cw20_balance: None,
342            ibc_balance: None,
343        };
344        assert_eq!(
345            task_balance
346                .verify_enough_attached(Uint128::from(101u64), None, None, false, "denom")
347                .unwrap_err(),
348            SdkError::NotEnoughNative {
349                denom: "denom".to_owned(),
350                lack: 1u64.into(),
351            }
352        );
353        assert_eq!(
354            task_balance
355                .verify_enough_attached(native_balance, Some(cw20.clone()), None, false, "denom")
356                .unwrap_err(),
357            SdkError::NotEnoughCw20 {
358                addr: "addr".to_owned(),
359                lack: 100u64.into(),
360            }
361        );
362        assert_eq!(
363            task_balance
364                .verify_enough_attached(
365                    native_balance,
366                    None,
367                    Some(ibc_coin.clone()),
368                    false,
369                    "denom"
370                )
371                .unwrap_err(),
372            SdkError::NotEnoughNative {
373                denom: "ibc".to_owned(),
374                lack: 100u64.into(),
375            }
376        );
377
378        // Test when cw20_balance or ibc_balance are not sufficient
379        let task_balance = TaskBalance {
380            native_balance,
381            cw20_balance: Some(cw20.clone()),
382            ibc_balance: Some(ibc_coin.clone()),
383        };
384        // cw20_balance is not sufficient
385        assert_eq!(
386            task_balance
387                .verify_enough_attached(
388                    Uint128::from(100u64),
389                    Some(Cw20CoinVerified {
390                        address: Addr::unchecked("addr"),
391                        amount: Uint128::from(101u64),
392                    }),
393                    Some(ibc_coin.clone()),
394                    false,
395                    "denom"
396                )
397                .unwrap_err(),
398            SdkError::NotEnoughCw20 {
399                addr: "addr".to_owned(),
400                lack: 1u64.into(),
401            }
402        );
403        // cw20_balance has another address
404        assert_eq!(
405            task_balance
406                .verify_enough_attached(
407                    Uint128::from(100u64),
408                    Some(Cw20CoinVerified {
409                        address: Addr::unchecked("addr2"),
410                        amount: Uint128::from(100u64),
411                    }),
412                    Some(ibc_coin),
413                    false,
414                    "denom"
415                )
416                .unwrap_err(),
417            SdkError::NotEnoughCw20 {
418                addr: "addr2".to_owned(),
419                lack: 100u64.into(),
420            }
421        );
422        // ibc_balance is not sufficient
423        assert_eq!(
424            task_balance
425                .verify_enough_attached(
426                    Uint128::from(100u64),
427                    Some(cw20.clone()),
428                    Some(coin(101, "ibc")),
429                    false,
430                    "denom"
431                )
432                .unwrap_err(),
433            SdkError::NotEnoughNative {
434                denom: "ibc".to_owned(),
435                lack: 1u64.into(),
436            }
437        );
438        // ibc_balance has another denom
439        assert_eq!(
440            task_balance
441                .verify_enough_attached(
442                    Uint128::from(100u64),
443                    Some(cw20),
444                    Some(coin(100, "ibc2")),
445                    false,
446                    "denom"
447                )
448                .unwrap_err(),
449            SdkError::NotEnoughNative {
450                denom: "ibc2".to_owned(),
451                lack: 100u64.into(),
452            }
453        );
454    }
455
456    #[test]
457    fn sub_coin_test() {
458        let native_balance = Uint128::from(100u64);
459        let ibc_coin = coin(100, "ibc");
460
461        let mut task_balance = TaskBalance {
462            native_balance,
463            cw20_balance: None,
464            ibc_balance: Some(ibc_coin.clone()),
465        };
466
467        task_balance
468            .sub_coin(&coin(10, "native"), "native")
469            .unwrap();
470        assert_eq!(
471            task_balance,
472            TaskBalance {
473                native_balance: Uint128::from(90u64),
474                cw20_balance: None,
475                ibc_balance: Some(ibc_coin),
476            }
477        );
478
479        task_balance.sub_coin(&coin(1, "ibc"), "native").unwrap();
480        assert_eq!(
481            task_balance,
482            TaskBalance {
483                native_balance: Uint128::from(90u64),
484                cw20_balance: None,
485                ibc_balance: Some(coin(99, "ibc")),
486            }
487        );
488
489        assert!(task_balance
490            .sub_coin(&coin(91, "native"), "native")
491            .is_err());
492
493        assert!(task_balance.sub_coin(&coin(100, "ibc"), "native").is_err());
494
495        assert!(task_balance
496            .sub_coin(&coin(100, "wrong"), "native")
497            .is_err());
498    }
499
500    #[test]
501    fn sub_cw20_test() {
502        let native_balance = Uint128::from(100u64);
503        let cw20 = Cw20CoinVerified {
504            address: Addr::unchecked("addr"),
505            amount: Uint128::from(100u64),
506        };
507
508        let mut task_balance = TaskBalance {
509            native_balance,
510            cw20_balance: Some(cw20),
511            ibc_balance: None,
512        };
513
514        task_balance
515            .sub_cw20(&Cw20CoinVerified {
516                address: Addr::unchecked("addr"),
517                amount: Uint128::from(10u64),
518            })
519            .unwrap();
520        assert_eq!(
521            task_balance,
522            TaskBalance {
523                native_balance,
524                cw20_balance: Some(Cw20CoinVerified {
525                    address: Addr::unchecked("addr"),
526                    amount: Uint128::from(90u64),
527                }),
528                ibc_balance: None,
529            }
530        );
531
532        assert!(task_balance
533            .sub_cw20(&Cw20CoinVerified {
534                address: Addr::unchecked("addr"),
535                amount: Uint128::from(91u64),
536            })
537            .is_err());
538
539        assert!(task_balance
540            .sub_cw20(&Cw20CoinVerified {
541                address: Addr::unchecked("addr2"),
542                amount: Uint128::from(1u64),
543            })
544            .is_err());
545    }
546
547    #[test]
548    fn test_sub_coin_success() {
549        let native_denom = "native";
550        let ibc_denom = "ibc";
551
552        let mut task_balance = TaskBalance {
553            native_balance: Uint128::new(100),
554            cw20_balance: None,
555            ibc_balance: Some(Coin {
556                denom: ibc_denom.to_string(),
557                amount: Uint128::new(200),
558            }),
559        };
560
561        let coin_native = Coin {
562            denom: native_denom.to_string(),
563            amount: Uint128::new(50),
564        };
565
566        let coin_ibc = Coin {
567            denom: ibc_denom.to_string(),
568            amount: Uint128::new(100),
569        };
570
571        task_balance.sub_coin(&coin_native, native_denom).unwrap();
572        task_balance.sub_coin(&coin_ibc, native_denom).unwrap();
573
574        assert_eq!(task_balance.native_balance, Uint128::new(50));
575        assert_eq!(task_balance.ibc_balance.unwrap().amount, Uint128::new(100));
576    }
577
578    #[test]
579    fn test_sub_coin_overflow() {
580        let native_denom = "native";
581        let ibc_denom = "ibc";
582
583        let mut task_balance = TaskBalance {
584            native_balance: Uint128::new(100),
585            cw20_balance: None,
586            ibc_balance: Some(Coin {
587                denom: ibc_denom.to_string(),
588                amount: Uint128::new(200),
589            }),
590        };
591
592        let coin_native_overflow = Coin {
593            denom: native_denom.to_string(),
594            amount: Uint128::new(150),
595        };
596
597        let coin_ibc_overflow = Coin {
598            denom: ibc_denom.to_string(),
599            amount: Uint128::new(300),
600        };
601
602        let coin_nonexistent = Coin {
603            denom: "nonexistent".to_string(),
604            amount: Uint128::new(100),
605        };
606
607        assert!(task_balance
608            .sub_coin(&coin_native_overflow, native_denom)
609            .is_err());
610        assert!(task_balance
611            .sub_coin(&coin_ibc_overflow, native_denom)
612            .is_err());
613        assert!(task_balance
614            .sub_coin(&coin_nonexistent, native_denom)
615            .is_err());
616    }
617
618    #[test]
619    fn test_sub_cw20_success() {
620        let cw20_address = Addr::unchecked("cw20_address");
621        let mut task_balance = TaskBalance {
622            cw20_balance: Some(Cw20CoinVerified {
623                address: cw20_address.clone(),
624                amount: Uint128::from(100u128),
625            }),
626            native_balance: Uint128::zero(),
627            ibc_balance: None,
628        };
629
630        let cw20 = Cw20CoinVerified {
631            address: cw20_address,
632            amount: Uint128::from(50u128),
633        };
634
635        assert!(task_balance.sub_cw20(&cw20).is_ok());
636        assert_eq!(
637            task_balance.cw20_balance.unwrap().amount,
638            Uint128::from(50u128)
639        );
640    }
641
642    #[test]
643    fn test_sub_cw20_insufficient_balance() {
644        let cw20_address = Addr::unchecked("cw20_address");
645        let mut task_balance = TaskBalance {
646            cw20_balance: Some(Cw20CoinVerified {
647                address: cw20_address.clone(),
648                amount: Uint128::from(100u128),
649            }),
650            native_balance: Uint128::zero(),
651            ibc_balance: None,
652        };
653
654        let cw20 = Cw20CoinVerified {
655            address: cw20_address,
656            amount: Uint128::from(200u128),
657        };
658
659        assert!(task_balance.sub_cw20(&cw20).is_err());
660    }
661
662    #[test]
663    fn test_sub_cw20_address_not_found() {
664        let cw20_address = Addr::unchecked("cw20_address");
665        let cw20_address_2 = Addr::unchecked("cw20_address_2");
666        let mut task_balance = TaskBalance {
667            cw20_balance: Some(Cw20CoinVerified {
668                address: cw20_address,
669                amount: Uint128::from(100u128),
670            }),
671            native_balance: Uint128::zero(),
672            ibc_balance: None,
673        };
674
675        let cw20 = Cw20CoinVerified {
676            address: cw20_address_2,
677            amount: Uint128::from(50u128),
678        };
679
680        assert!(task_balance.sub_cw20(&cw20).is_err());
681    }
682}