apollo_cw_multi_test/
token_factory.rs

1use std::str::FromStr;
2
3use anyhow::{bail, Ok};
4use cosmwasm_std::{
5    from_slice, Addr, Api, BankMsg, BankQuery, BlockInfo, Coin, Empty, Event, QueryRequest,
6    Storage, SupplyResponse, Uint128,
7};
8use osmosis_std::types::osmosis::tokenfactory::v1beta1::{
9    MsgBurn, MsgBurnResponse, MsgCreateDenom, MsgCreateDenomResponse, MsgMint, MsgMintResponse,
10};
11use regex::Regex;
12
13use crate::{
14    stargate::{StargateKeeper, StargateMessageHandler, StargateMsg},
15    AppResponse, BankSudo,
16};
17
18#[derive(Clone)]
19pub struct TokenFactory<'a> {
20    pub module_denom_prefix: &'a str,
21    pub max_subdenom_len: usize,
22    pub max_hrp_len: usize,
23    pub max_creator_len: usize,
24    pub denom_creation_fee: &'a str,
25}
26
27// impl<'a> From<ConstTokenFactory<> for TokenFactory<'a> {}
28
29// pub struct TokenFactory {
30//     pub module_denom_prefix: String,
31//     pub max_subdenom_len: usize,
32//     pub max_hrp_len: usize,
33//     pub max_creator_len: usize,
34//     pub denom_creation_fee: String,
35// }
36
37impl<'a> TokenFactory<'a> {
38    pub const fn new(
39        prefix: &'a str,
40        max_subdenom_len: usize,
41        max_hrp_len: usize,
42        max_creator_len: usize,
43        denom_creation_fee: &'a str,
44    ) -> Self {
45        Self {
46            module_denom_prefix: prefix,
47            max_subdenom_len,
48            max_hrp_len,
49            max_creator_len,
50            denom_creation_fee,
51        }
52    }
53}
54
55impl Default for TokenFactory<'_> {
56    fn default() -> Self {
57        Self::new("factory", 32, 16, 59 + 16, "10000000uosmo")
58    }
59}
60
61impl TokenFactory<'_> {
62    fn create_denom(
63        &self,
64        api: &dyn Api,
65        storage: &mut dyn Storage,
66        router: &dyn crate::CosmosRouter<ExecC = Empty, QueryC = Empty>,
67        block: &BlockInfo,
68        sender: Addr,
69        msg: StargateMsg,
70    ) -> anyhow::Result<AppResponse> {
71        let msg: MsgCreateDenom = msg.value.try_into()?;
72
73        //Validate subdenom length
74        if msg.subdenom.len() > self.max_subdenom_len {
75            bail!(
76                "Subdenom length is too long, max length is {}",
77                self.max_subdenom_len
78            );
79        }
80        // Validate creator length
81        if msg.sender.len() > self.max_creator_len {
82            bail!(
83                "Creator length is too long, max length is {}",
84                self.max_creator_len
85            );
86        }
87        // Validate creator address not contains '/'
88        if msg.sender.contains('/') {
89            bail!("Invalid creator address, creator address cannot contains '/'");
90        }
91        // Validate sender is the creator
92        if msg.sender != sender {
93            bail!("Invalid creator address, creator address must be the same as the sender");
94        }
95
96        let denom = format!(
97            "{}/{}/{}",
98            self.module_denom_prefix, msg.sender, msg.subdenom
99        );
100
101        println!("denom: {}", denom);
102
103        // Query supply of denom
104        let request = QueryRequest::Bank(BankQuery::Supply {
105            denom: denom.clone(),
106        });
107        let raw = router.query(api, storage, block, request)?;
108        let supply: SupplyResponse = from_slice(&raw)?;
109        println!("supply: {:?}", supply);
110        println!(
111            "supply.amount.amount.is_zero: {:?}",
112            supply.amount.amount.is_zero()
113        );
114        if !supply.amount.amount.is_zero() {
115            println!("bailing");
116            bail!("Subdenom already exists");
117        }
118
119        // Charge denom creation fee
120        let fee = coin_from_sdk_string(self.denom_creation_fee)?;
121        let fee_msg = BankMsg::Burn { amount: vec![fee] };
122        router.execute(api, storage, block, sender, fee_msg.into())?;
123
124        let create_denom_response = MsgCreateDenomResponse {
125            new_token_denom: denom.clone(),
126        };
127
128        let mut res = AppResponse::default();
129        res.events.push(
130            Event::new("create_denom")
131                .add_attribute("creator", msg.sender)
132                .add_attribute("new_token_denom", denom),
133        );
134        res.data = Some(create_denom_response.into());
135
136        Ok(res)
137    }
138
139    pub fn mint(
140        &self,
141        api: &dyn Api,
142        storage: &mut dyn Storage,
143        router: &dyn crate::CosmosRouter<ExecC = Empty, QueryC = Empty>,
144        block: &BlockInfo,
145        sender: Addr,
146        msg: StargateMsg,
147    ) -> anyhow::Result<AppResponse> {
148        let msg: MsgMint = msg.value.try_into()?;
149
150        let denom = msg.amount.clone().unwrap().denom;
151
152        // Validate sender
153        let parts = denom.split('/').collect::<Vec<_>>();
154        if parts[1] != sender {
155            bail!("Unauthorized mint. Not the creator of the denom.");
156        }
157        if sender != msg.sender {
158            bail!("Invalid sender. Sender in msg must be same as sender of transaction.");
159        }
160
161        // Validate denom
162        if parts.len() != 3 && parts[0] != self.module_denom_prefix {
163            bail!("Invalid denom");
164        }
165
166        let amount = Uint128::from_str(&msg.amount.unwrap().amount)?;
167        if amount.is_zero() {
168            bail!("Invalid zero amount");
169        }
170
171        // Mint through BankKeeper sudo method
172        let mint_msg = BankSudo::Mint {
173            to_address: sender.to_string(),
174            amount: vec![Coin {
175                denom: denom.clone(),
176                amount,
177            }],
178        };
179        router.sudo(api, storage, block, mint_msg.into())?;
180
181        let mut res = AppResponse::default();
182        let data = MsgMintResponse {};
183        res.data = Some(data.into());
184        res.events.push(
185            Event::new("tf_mint")
186                .add_attribute("mint_to_address", "sender")
187                .add_attribute("amount", amount.to_string()),
188        );
189        Ok(res)
190    }
191
192    pub fn burn(
193        &self,
194        api: &dyn Api,
195        storage: &mut dyn Storage,
196        router: &dyn crate::CosmosRouter<ExecC = Empty, QueryC = Empty>,
197        block: &BlockInfo,
198        sender: Addr,
199        msg: StargateMsg,
200    ) -> anyhow::Result<AppResponse> {
201        let msg: MsgBurn = msg.value.try_into()?;
202
203        // Validate sender
204        let denom = msg.amount.clone().unwrap().denom;
205        let parts = denom.split('/').collect::<Vec<_>>();
206        if parts[1] != sender {
207            bail!("Unauthorized burn. Not the creator of the denom.");
208        }
209        if sender != msg.sender {
210            bail!("Invalid sender. Sender in msg must be same as sender of transaction.");
211        }
212
213        // Validate denom
214        if parts.len() != 3 && parts[0] != self.module_denom_prefix {
215            bail!("Invalid denom");
216        }
217
218        let amount = Uint128::from_str(&msg.amount.unwrap().amount)?;
219        if amount.is_zero() {
220            bail!("Invalid zero amount");
221        }
222
223        // Burn through BankKeeper
224        let burn_msg = BankMsg::Burn {
225            amount: vec![Coin {
226                denom: denom.clone(),
227                amount,
228            }],
229        };
230        router.execute(api, storage, block, sender.clone(), burn_msg.into())?;
231
232        let mut res = AppResponse::default();
233        let data = MsgBurnResponse {};
234        res.data = Some(data.into());
235
236        res.events.push(
237            Event::new("tf_burn")
238                .add_attribute("burn_from_address", sender.to_string())
239                .add_attribute("amount", amount.to_string()),
240        );
241
242        Ok(res)
243    }
244}
245
246impl StargateMessageHandler<Empty, Empty> for TokenFactory<'_> {
247    fn execute(
248        &self,
249        api: &dyn Api,
250        storage: &mut dyn Storage,
251        router: &dyn crate::CosmosRouter<ExecC = Empty, QueryC = Empty>,
252        block: &BlockInfo,
253        sender: Addr,
254        msg: StargateMsg,
255    ) -> anyhow::Result<crate::AppResponse> {
256        match msg.type_url.as_str() {
257            MsgCreateDenom::TYPE_URL => self.create_denom(api, storage, router, block, sender, msg),
258            MsgMint::TYPE_URL => self.mint(api, storage, router, block, sender, msg),
259            MsgBurn::TYPE_URL => self.burn(api, storage, router, block, sender, msg),
260            _ => bail!("Unknown message type {}", msg.type_url),
261        }
262    }
263
264    fn register_msgs(&'static self, keeper: &mut StargateKeeper<Empty, Empty>) {
265        let token_factory_box = Box::new(self.clone());
266        for type_url in [
267            MsgCreateDenom::TYPE_URL,
268            MsgMint::TYPE_URL,
269            MsgBurn::TYPE_URL,
270        ] {
271            keeper.register_msg(type_url, token_factory_box.clone());
272        }
273    }
274}
275
276fn coin_from_sdk_string(sdk_string: &str) -> anyhow::Result<Coin> {
277    let denom_re = Regex::new(r"^[0-9]+[a-z]+$")?;
278    let ibc_re = Regex::new(r"^[0-9]+(ibc|IBC)/[0-9A-F]{64}$")?;
279    let factory_re = Regex::new(r"^[0-9]+factory/[0-9a-z]+/[0-9a-zA-Z]+$")?;
280
281    if !(denom_re.is_match(sdk_string)
282        || ibc_re.is_match(sdk_string)
283        || factory_re.is_match(sdk_string))
284    {
285        bail!("Invalid sdk string");
286    }
287
288    // Parse amount
289    let re = Regex::new(r"[0-9]+")?;
290    let amount = re.find(sdk_string).unwrap().as_str();
291    let amount = Uint128::from_str(amount)?;
292
293    // The denom is the rest of the string
294    let denom = sdk_string[amount.to_string().len()..].to_string();
295
296    Ok(Coin { denom, amount })
297}
298
299#[cfg(test)]
300mod tests {
301    use cosmwasm_std::{BalanceResponse, Binary, Coin};
302
303    use crate::{stargate::StargateKeeper, BasicAppBuilder, Executor};
304
305    use super::*;
306
307    use test_case::test_case;
308
309    const TOKEN_FACTORY: &TokenFactory =
310        &TokenFactory::new("factory", 32, 16, 59 + 16, "10000000uosmo");
311
312    #[test_case(Addr::unchecked("sender"), "subdenom", &["10000000uosmo"]; "valid denom")]
313    #[test_case(Addr::unchecked("sen/der"), "subdenom", &["10000000uosmo"] => panics ; "invalid creator address")]
314    #[test_case(Addr::unchecked("asdasdasdasdasdasdasdasdasdasdasdasdasdasdasd"), "subdenom", &["10000000uosmo"] => panics ; "creator address too long")]
315    #[test_case(Addr::unchecked("sender"), "subdenom", &["10000000uosmo", "100factory/sender/subdenom"] => panics ; "denom exists")]
316    #[test_case(Addr::unchecked("sender"), "subdenom", &["100000uosmo"] => panics ; "insufficient funds for fee")]
317    fn create_denom(sender: Addr, subdenom: &str, initial_coins: &[&str]) {
318        let initial_coins = initial_coins
319            .iter()
320            .map(|s| coin_from_sdk_string(s).unwrap())
321            .collect::<Vec<_>>();
322
323        let mut stargate_keeper = StargateKeeper::new();
324        TOKEN_FACTORY.register_msgs(&mut stargate_keeper);
325
326        let app = BasicAppBuilder::<Empty, Empty>::new()
327            .with_stargate(stargate_keeper)
328            .build(|router, _, storage| {
329                router
330                    .bank
331                    .init_balance(storage, &sender, initial_coins)
332                    .unwrap();
333            });
334
335        let msg = StargateMsg {
336            type_url: MsgCreateDenom::TYPE_URL.to_string(),
337            value: MsgCreateDenom {
338                sender: sender.to_string(),
339                subdenom: subdenom.to_string(),
340            }
341            .into(),
342        };
343
344        let res = app.execute(sender.clone(), msg.into()).unwrap();
345
346        res.assert_event(
347            &Event::new("create_denom")
348                .add_attribute("creator", "sender")
349                .add_attribute(
350                    "new_token_denom",
351                    format!(
352                        "{}/{}/{}",
353                        TOKEN_FACTORY.module_denom_prefix, sender, subdenom
354                    ),
355                ),
356        );
357
358        assert_eq!(
359            res.data.unwrap(),
360            Binary::from(MsgCreateDenomResponse {
361                new_token_denom: format!(
362                    "{}/{}/{}",
363                    TOKEN_FACTORY.module_denom_prefix, sender, subdenom
364                )
365            })
366        );
367    }
368
369    #[test_case(Addr::unchecked("sender"), Addr::unchecked("sender"), 1000u128 ; "valid mint")]
370    #[test_case(Addr::unchecked("sender"), Addr::unchecked("sender"), 0u128 => panics ; "zero amount")]
371    #[test_case(Addr::unchecked("sender"), Addr::unchecked("creator"), 1000u128 => panics ; "sender is not creator")]
372    fn mint(sender: Addr, creator: Addr, mint_amount: u128) {
373        let mut stargate_keeper = StargateKeeper::new();
374        TOKEN_FACTORY.register_msgs(&mut stargate_keeper);
375
376        let app = BasicAppBuilder::<Empty, Empty>::new()
377            .with_stargate(stargate_keeper)
378            .build(|_, _, _| {});
379
380        let msg = StargateMsg {
381            type_url: MsgMint::TYPE_URL.to_string(),
382            value: MsgMint {
383                sender: sender.to_string(),
384                amount: Some(
385                    Coin {
386                        denom: format!(
387                            "{}/{}/{}",
388                            TOKEN_FACTORY.module_denom_prefix, creator, "subdenom"
389                        ),
390                        amount: Uint128::from(mint_amount),
391                    }
392                    .into(),
393                ),
394                mint_to_address: sender.to_string(),
395            }
396            .into(),
397        };
398
399        let res = app.execute(sender.clone(), msg.into()).unwrap();
400
401        // Assert event
402        res.assert_event(
403            &Event::new("tf_mint")
404                .add_attribute("mint_to_address", sender.to_string())
405                .add_attribute("amount", "1000"),
406        );
407
408        // Query bank balance
409        let balance_query = BankQuery::Balance {
410            address: sender.to_string(),
411            denom: format!(
412                "{}/{}/{}",
413                TOKEN_FACTORY.module_denom_prefix, creator, "subdenom"
414            ),
415        };
416        let balance = app
417            .wrap()
418            .query::<BalanceResponse>(&balance_query.into())
419            .unwrap()
420            .amount
421            .amount;
422        assert_eq!(balance, Uint128::from(mint_amount));
423    }
424
425    #[test_case(Addr::unchecked("sender"), Addr::unchecked("sender"), 1000u128, 1000u128 ; "valid burn")]
426    #[test_case(Addr::unchecked("sender"), Addr::unchecked("sender"), 1000u128, 2000u128 ; "valid burn 2")]
427    #[test_case(Addr::unchecked("sender"), Addr::unchecked("creator"), 1000u128, 1000u128 => panics ; "sender is not creator")]
428    #[test_case(Addr::unchecked("sender"), Addr::unchecked("sender"), 0u128, 1000u128 => panics ; "zero amount")]
429    #[test_case(Addr::unchecked("sender"), Addr::unchecked("sender"), 2000u128, 1000u128 => panics ; "insufficient funds")]
430    fn burn(sender: Addr, creator: Addr, burn_amount: u128, initial_balance: u128) {
431        let mut stargate_keeper = StargateKeeper::new();
432        TOKEN_FACTORY.register_msgs(&mut stargate_keeper);
433
434        let tf_denom = format!(
435            "{}/{}/{}",
436            TOKEN_FACTORY.module_denom_prefix, creator, "subdenom"
437        );
438
439        let app = BasicAppBuilder::<Empty, Empty>::new()
440            .with_stargate(stargate_keeper)
441            .build(|router, _, storage| {
442                router
443                    .bank
444                    .init_balance(
445                        storage,
446                        &sender,
447                        vec![Coin {
448                            denom: tf_denom.clone(),
449                            amount: Uint128::from(initial_balance),
450                        }],
451                    )
452                    .unwrap();
453            });
454
455        // Execute burn
456        let msg = StargateMsg {
457            type_url: MsgBurn::TYPE_URL.to_string(),
458            value: MsgBurn {
459                sender: sender.to_string(),
460                amount: Some(
461                    Coin {
462                        denom: tf_denom.clone(),
463                        amount: Uint128::from(burn_amount),
464                    }
465                    .into(),
466                ),
467                burn_from_address: sender.to_string(),
468            }
469            .into(),
470        };
471        let res = app.execute(sender.clone(), msg.into()).unwrap();
472
473        // Assert event
474        res.assert_event(
475            &Event::new("tf_burn")
476                .add_attribute("burn_from_address", sender.to_string())
477                .add_attribute("amount", "1000"),
478        );
479
480        // Query bank balance
481        let balance_query = BankQuery::Balance {
482            address: sender.to_string(),
483            denom: tf_denom,
484        };
485        let balance = app
486            .wrap()
487            .query::<BalanceResponse>(&balance_query.into())
488            .unwrap()
489            .amount
490            .amount;
491        assert_eq!(balance.u128(), initial_balance - burn_amount);
492    }
493
494    #[test_case("uosmo" ; "native denom")]
495    #[test_case("IBC/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2" ; "ibc denom")]
496    #[test_case("IBC/27394FB092D2ECCD56123CA622B25F41E5EB2" => panics ; "invalid ibc denom")]
497    #[test_case("IB/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2" => panics ; "invalid ibc denom 2")]
498    #[test_case("factory/sender/subdenom" ; "token factory denom")]
499    #[test_case("factory/se1298der/subde192MAnom" ; "token factory denom 2")]
500    #[test_case("factor/sender/subdenom" => panics ; "invalid token factory denom")]
501    #[test_case("factory/sender/subdenom/extra" => panics ; "invalid token factory denom 2")]
502    fn test_coin_from_sdk_string(denom: &str) {
503        let sdk_string = format!("{}{}", 1000, denom);
504        let coin = coin_from_sdk_string(&sdk_string).unwrap();
505        assert_eq!(coin.denom, denom);
506        assert_eq!(coin.amount, Uint128::from(1000u128));
507    }
508}