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
27impl<'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 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 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 if msg.sender.contains('/') {
89 bail!("Invalid creator address, creator address cannot contains '/'");
90 }
91 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}