cw_multi_test/
bank.rs

1use crate::app::CosmosRouter;
2use crate::error::{bail, AnyResult};
3use crate::executor::AppResponse;
4use crate::module::Module;
5use crate::prefixed_storage::typed_prefixed_storage::{
6    StoragePrefix, TypedPrefixedStorage, TypedPrefixedStorageMut,
7};
8use cosmwasm_std::{
9    coin, to_json_binary, Addr, AllBalanceResponse, Api, BalanceResponse, BankMsg, BankQuery,
10    Binary, BlockInfo, Coin, DenomMetadata, Event, Querier, Storage,
11};
12#[cfg(feature = "cosmwasm_1_3")]
13use cosmwasm_std::{AllDenomMetadataResponse, DenomMetadataResponse};
14#[cfg(feature = "cosmwasm_1_1")]
15use cosmwasm_std::{Order, StdResult, SupplyResponse, Uint128};
16use cw_storage_plus::Map;
17use cw_utils::NativeBalance;
18use itertools::Itertools;
19use schemars::JsonSchema;
20
21/// Collection of bank balances.
22const BALANCES: Map<&Addr, NativeBalance> = Map::new("balances");
23
24/// Collection of metadata for denomination.
25const DENOM_METADATA: Map<String, DenomMetadata> = Map::new("metadata");
26
27/// A message representing privileged actions in bank module.
28#[derive(Clone, Debug, PartialEq, Eq, JsonSchema)]
29pub enum BankSudo {
30    /// Minting privileged action.
31    Mint {
32        /// Destination address the tokens will be minted for.
33        to_address: String,
34        /// Amount of the minted tokens.
35        amount: Vec<Coin>,
36    },
37}
38
39/// This trait defines the interface for simulating banking operations.
40///
41/// In the test environment, it is essential for testing financial transactions,
42/// like transfers and balance checks, within your smart contracts.
43/// This trait implements all of these functionalities.
44pub trait Bank: Module<ExecT = BankMsg, QueryT = BankQuery, SudoT = BankSudo> {}
45
46/// A structure representing a default bank keeper.
47///
48/// Manages financial interactions in CosmWasm tests, such as simulating token transactions
49/// and account balances. This is particularly important for contracts that deal with financial
50/// operations in the Cosmos ecosystem.
51#[derive(Default)]
52pub struct BankKeeper {}
53
54impl StoragePrefix for BankKeeper {
55    const NAMESPACE: &'static [u8] = b"bank";
56}
57type BankStorage<'a> = TypedPrefixedStorage<'a, BankKeeper>;
58type BankStorageMut<'a> = TypedPrefixedStorageMut<'a, BankKeeper>;
59
60impl BankKeeper {
61    /// Creates a new instance of a bank keeper with default settings.
62    pub fn new() -> Self {
63        Self::default()
64    }
65
66    /// Administration function for adjusting bank accounts in genesis.
67    pub fn init_balance(
68        &self,
69        storage: &mut dyn Storage,
70        account: &Addr,
71        amount: Vec<Coin>,
72    ) -> AnyResult<()> {
73        let mut bank_storage = BankStorageMut::new(storage);
74        self.set_balance(&mut bank_storage, account, amount)
75    }
76
77    /// Administration function for adjusting bank accounts.
78    fn set_balance(
79        &self,
80        storage: &mut BankStorageMut,
81        account: &Addr,
82        amount: Vec<Coin>,
83    ) -> AnyResult<()> {
84        let mut balance = NativeBalance(amount);
85        balance.normalize();
86        BALANCES
87            .save(storage, account, &balance)
88            .map_err(Into::into)
89    }
90
91    /// Administration function for adjusting denomination metadata.
92    pub fn set_denom_metadata(
93        &self,
94        storage: &mut dyn Storage,
95        denom: String,
96        metadata: DenomMetadata,
97    ) -> AnyResult<()> {
98        DENOM_METADATA
99            .save(storage, denom, &metadata)
100            .map_err(Into::into)
101    }
102
103    /// Returns balance for specified address.
104    fn get_balance(&self, storage: &BankStorage, addr: &Addr) -> AnyResult<Vec<Coin>> {
105        let val = BALANCES.may_load(storage, addr)?;
106        Ok(val.unwrap_or_default().into_vec())
107    }
108
109    #[cfg(feature = "cosmwasm_1_1")]
110    fn get_supply(&self, storage: &BankStorage, denom: String) -> AnyResult<Coin> {
111        let supply: Uint128 = BALANCES
112            .range(storage, None, None, Order::Ascending)
113            .collect::<StdResult<Vec<_>>>()?
114            .into_iter()
115            .map(|a| a.1)
116            .fold(Uint128::zero(), |accum, item| {
117                let mut subtotal = Uint128::zero();
118                for coin in item.into_vec() {
119                    if coin.denom == denom {
120                        subtotal += coin.amount;
121                    }
122                }
123                accum + subtotal
124            });
125        Ok(coin(supply.into(), denom))
126    }
127
128    fn send(
129        &self,
130        storage: &mut BankStorageMut,
131        from_address: Addr,
132        to_address: Addr,
133        amount: Vec<Coin>,
134    ) -> AnyResult<()> {
135        self.burn(storage, from_address, amount.clone())?;
136        self.mint(storage, to_address, amount)
137    }
138
139    fn mint(
140        &self,
141        storage: &mut BankStorageMut,
142        to_address: Addr,
143        amount: Vec<Coin>,
144    ) -> AnyResult<()> {
145        let amount = self.normalize_amount(amount)?;
146        let b = self.get_balance(&storage.borrow(), &to_address)?;
147        let b = NativeBalance(b) + NativeBalance(amount);
148        self.set_balance(storage, &to_address, b.into_vec())
149    }
150
151    fn burn(
152        &self,
153        storage: &mut BankStorageMut,
154        from_address: Addr,
155        amount: Vec<Coin>,
156    ) -> AnyResult<()> {
157        let amount = self.normalize_amount(amount)?;
158        let a = self.get_balance(&storage.borrow(), &from_address)?;
159        let a = (NativeBalance(a) - amount)?;
160        self.set_balance(storage, &from_address, a.into_vec())
161    }
162
163    /// Filters out all `0` value coins and returns an error if the resulting vector is empty.
164    fn normalize_amount(&self, amount: Vec<Coin>) -> AnyResult<Vec<Coin>> {
165        let res: Vec<_> = amount.into_iter().filter(|x| !x.amount.is_zero()).collect();
166        if res.is_empty() {
167            bail!("Cannot transfer empty coins amount")
168        } else {
169            Ok(res)
170        }
171    }
172}
173
174fn coins_to_string(coins: &[Coin]) -> String {
175    coins
176        .iter()
177        .map(|c| format!("{}{}", c.amount, c.denom))
178        .join(",")
179}
180
181impl Bank for BankKeeper {}
182
183impl Module for BankKeeper {
184    type ExecT = BankMsg;
185    type QueryT = BankQuery;
186    type SudoT = BankSudo;
187
188    fn execute<ExecC, QueryC>(
189        &self,
190        _api: &dyn Api,
191        storage: &mut dyn Storage,
192        _router: &dyn CosmosRouter<ExecC = ExecC, QueryC = QueryC>,
193        _block: &BlockInfo,
194        sender: Addr,
195        msg: BankMsg,
196    ) -> AnyResult<AppResponse> {
197        let mut bank_storage_mut = BankStorageMut::new(storage);
198        match msg {
199            BankMsg::Send { to_address, amount } => {
200                // see https://github.com/cosmos/cosmos-sdk/blob/v0.42.7/x/bank/keeper/send.go#L142-L147
201                let events = vec![Event::new("transfer")
202                    .add_attribute("recipient", &to_address)
203                    .add_attribute("sender", &sender)
204                    .add_attribute("amount", coins_to_string(&amount))];
205                self.send(
206                    &mut bank_storage_mut,
207                    sender,
208                    Addr::unchecked(to_address),
209                    amount,
210                )?;
211                Ok(AppResponse {
212                    events,
213                    ..Default::default()
214                })
215            }
216            BankMsg::Burn { amount } => {
217                // burn doesn't seem to emit any events
218                self.burn(&mut bank_storage_mut, sender, amount)?;
219                Ok(AppResponse::default())
220            }
221            other => unimplemented!("bank message: {other:?}"),
222        }
223    }
224
225    fn query(
226        &self,
227        api: &dyn Api,
228        storage: &dyn Storage,
229        _querier: &dyn Querier,
230        _block: &BlockInfo,
231        request: BankQuery,
232    ) -> AnyResult<Binary> {
233        let bank_storage = BankStorage::new(storage);
234        match request {
235            #[allow(deprecated)]
236            BankQuery::AllBalances { address } => {
237                let address = api.addr_validate(&address)?;
238                let amount = self.get_balance(&bank_storage, &address)?;
239                let res = AllBalanceResponse::new(amount);
240                to_json_binary(&res).map_err(Into::into)
241            }
242            BankQuery::Balance { address, denom } => {
243                let address = api.addr_validate(&address)?;
244                let all_amounts = self.get_balance(&bank_storage, &address)?;
245                let amount = all_amounts
246                    .into_iter()
247                    .find(|c| c.denom == denom)
248                    .unwrap_or_else(|| coin(0, denom));
249                let res = BalanceResponse::new(amount);
250                to_json_binary(&res).map_err(Into::into)
251            }
252            #[cfg(feature = "cosmwasm_1_1")]
253            BankQuery::Supply { denom } => {
254                let amount = self.get_supply(&bank_storage, denom)?;
255                let res = SupplyResponse::new(amount);
256                to_json_binary(&res).map_err(Into::into)
257            }
258            #[cfg(feature = "cosmwasm_1_3")]
259            BankQuery::DenomMetadata { denom } => {
260                let meta = DENOM_METADATA.may_load(storage, denom)?.unwrap_or_default();
261                let res = DenomMetadataResponse::new(meta);
262                to_json_binary(&res).map_err(Into::into)
263            }
264            #[cfg(feature = "cosmwasm_1_3")]
265            BankQuery::AllDenomMetadata { pagination: _ } => {
266                let mut metadata = vec![];
267                for key in DENOM_METADATA.keys(storage, None, None, Order::Ascending) {
268                    metadata.push(DENOM_METADATA.may_load(storage, key?)?.unwrap_or_default());
269                }
270                let res = AllDenomMetadataResponse::new(metadata, None);
271                to_json_binary(&res).map_err(Into::into)
272            }
273            other => unimplemented!("bank query: {:?}", other),
274        }
275    }
276
277    fn sudo<ExecC, QueryC>(
278        &self,
279        api: &dyn Api,
280        storage: &mut dyn Storage,
281        _router: &dyn CosmosRouter<ExecC = ExecC, QueryC = QueryC>,
282        _block: &BlockInfo,
283        msg: BankSudo,
284    ) -> AnyResult<AppResponse> {
285        let mut bank_storage_mut = BankStorageMut::new(storage);
286        match msg {
287            BankSudo::Mint { to_address, amount } => {
288                let to_address = api.addr_validate(&to_address)?;
289                self.mint(&mut bank_storage_mut, to_address, amount)?;
290                Ok(AppResponse::default())
291            }
292        }
293    }
294}
295
296#[cfg(test)]
297mod test {
298    use super::*;
299
300    use crate::app::MockRouter;
301    use cosmwasm_std::testing::{mock_env, MockApi, MockQuerier, MockStorage};
302    use cosmwasm_std::{coins, from_json, Empty, StdError};
303
304    fn query_balance(
305        bank: &BankKeeper,
306        api: &dyn Api,
307        store: &dyn Storage,
308        rcpt: &Addr,
309    ) -> Vec<Coin> {
310        #[allow(deprecated)]
311        let req = BankQuery::AllBalances {
312            address: rcpt.clone().into(),
313        };
314        let block = mock_env().block;
315        let querier: MockQuerier<Empty> = MockQuerier::new(&[]);
316
317        let raw = bank.query(api, store, &querier, &block, req).unwrap();
318        let res: AllBalanceResponse = from_json(raw).unwrap();
319        res.amount
320    }
321
322    #[test]
323    #[cfg(feature = "cosmwasm_1_1")]
324    fn get_set_balance() {
325        let api = MockApi::default();
326        let mut store = MockStorage::new();
327        let block = mock_env().block;
328        let querier: MockQuerier<Empty> = MockQuerier::new(&[]);
329        let router = MockRouter::default();
330
331        let owner = api.addr_make("owner");
332        let rcpt = api.addr_make("receiver");
333        let init_funds = vec![coin(100, "eth"), coin(20, "btc")];
334        let norm = vec![coin(20, "btc"), coin(100, "eth")];
335
336        // set money
337        let bank = BankKeeper::new();
338        bank.init_balance(&mut store, &owner, init_funds).unwrap();
339
340        let bank_storage = BankStorage::new(&store);
341
342        // get balance work
343        let rich = bank.get_balance(&bank_storage, &owner).unwrap();
344        assert_eq!(rich, norm);
345        let poor = bank.get_balance(&bank_storage, &rcpt).unwrap();
346        assert_eq!(poor, vec![]);
347
348        // proper queries work
349        #[allow(deprecated)]
350        let req = BankQuery::AllBalances {
351            address: owner.clone().into(),
352        };
353        let raw = bank.query(&api, &store, &querier, &block, req).unwrap();
354        let res: AllBalanceResponse = from_json(raw).unwrap();
355        assert_eq!(res.amount, norm);
356
357        #[allow(deprecated)]
358        let req = BankQuery::AllBalances {
359            address: rcpt.clone().into(),
360        };
361        let raw = bank.query(&api, &store, &querier, &block, req).unwrap();
362        let res: AllBalanceResponse = from_json(raw).unwrap();
363        assert_eq!(res.amount, vec![]);
364
365        let req = BankQuery::Balance {
366            address: owner.clone().into(),
367            denom: "eth".into(),
368        };
369        let raw = bank.query(&api, &store, &querier, &block, req).unwrap();
370        let res: BalanceResponse = from_json(raw).unwrap();
371        assert_eq!(res.amount, coin(100, "eth"));
372
373        let req = BankQuery::Balance {
374            address: owner.into(),
375            denom: "foobar".into(),
376        };
377        let raw = bank.query(&api, &store, &querier, &block, req).unwrap();
378        let res: BalanceResponse = from_json(raw).unwrap();
379        assert_eq!(res.amount, coin(0, "foobar"));
380
381        let req = BankQuery::Balance {
382            address: rcpt.clone().into(),
383            denom: "eth".into(),
384        };
385        let raw = bank.query(&api, &store, &querier, &block, req).unwrap();
386        let res: BalanceResponse = from_json(raw).unwrap();
387        assert_eq!(res.amount, coin(0, "eth"));
388
389        // Query total supply of a denom
390        let req = BankQuery::Supply {
391            denom: "eth".into(),
392        };
393        let raw = bank.query(&api, &store, &querier, &block, req).unwrap();
394        let res: SupplyResponse = from_json(raw).unwrap();
395        assert_eq!(res.amount, coin(100, "eth"));
396
397        // Mint tokens for recipient account
398        let msg = BankSudo::Mint {
399            to_address: rcpt.to_string(),
400            amount: norm.clone(),
401        };
402        bank.sudo(&api, &mut store, &router, &block, msg).unwrap();
403
404        // Check that the recipient account has the expected balance
405        #[allow(deprecated)]
406        let req = BankQuery::AllBalances {
407            address: rcpt.into(),
408        };
409        let raw = bank.query(&api, &store, &querier, &block, req).unwrap();
410        let res: AllBalanceResponse = from_json(raw).unwrap();
411        assert_eq!(res.amount, norm);
412
413        // Check that the total supply of a denom is updated
414        let req = BankQuery::Supply {
415            denom: "eth".into(),
416        };
417        let raw = bank.query(&api, &store, &querier, &block, req).unwrap();
418        let res: SupplyResponse = from_json(raw).unwrap();
419        assert_eq!(res.amount, coin(200, "eth"));
420    }
421
422    #[test]
423    fn send_coins() {
424        let api = MockApi::default();
425        let mut store = MockStorage::new();
426        let block = mock_env().block;
427        let router = MockRouter::default();
428
429        let owner = api.addr_make("owner");
430        let rcpt = api.addr_make("receiver");
431        let init_funds = vec![coin(20, "btc"), coin(100, "eth")];
432        let rcpt_funds = vec![coin(5, "btc")];
433
434        // set money
435        let bank = BankKeeper::new();
436        bank.init_balance(&mut store, &owner, init_funds).unwrap();
437        bank.init_balance(&mut store, &rcpt, rcpt_funds).unwrap();
438
439        // send both tokens
440        let to_send = vec![coin(30, "eth"), coin(5, "btc")];
441        let msg = BankMsg::Send {
442            to_address: rcpt.clone().into(),
443            amount: to_send,
444        };
445        bank.execute(
446            &api,
447            &mut store,
448            &router,
449            &block,
450            owner.clone(),
451            msg.clone(),
452        )
453        .unwrap();
454        let rich = query_balance(&bank, &api, &store, &owner);
455        assert_eq!(vec![coin(15, "btc"), coin(70, "eth")], rich);
456        let poor = query_balance(&bank, &api, &store, &rcpt);
457        assert_eq!(vec![coin(10, "btc"), coin(30, "eth")], poor);
458
459        // can send from any account with funds
460        bank.execute(&api, &mut store, &router, &block, rcpt.clone(), msg)
461            .unwrap();
462
463        // cannot send too much
464        let msg = BankMsg::Send {
465            to_address: rcpt.into(),
466            amount: coins(20, "btc"),
467        };
468        bank.execute(&api, &mut store, &router, &block, owner.clone(), msg)
469            .unwrap_err();
470
471        let rich = query_balance(&bank, &api, &store, &owner);
472        assert_eq!(vec![coin(15, "btc"), coin(70, "eth")], rich);
473    }
474
475    #[test]
476    fn burn_coins() {
477        let api = MockApi::default();
478        let mut store = MockStorage::new();
479        let block = mock_env().block;
480        let router = MockRouter::default();
481
482        let owner = api.addr_make("owner");
483        let rcpt = api.addr_make("recipient");
484        let init_funds = vec![coin(20, "btc"), coin(100, "eth")];
485
486        // set money
487        let bank = BankKeeper::new();
488        bank.init_balance(&mut store, &owner, init_funds).unwrap();
489
490        // burn both tokens
491        let to_burn = vec![coin(30, "eth"), coin(5, "btc")];
492        let msg = BankMsg::Burn { amount: to_burn };
493        bank.execute(&api, &mut store, &router, &block, owner.clone(), msg)
494            .unwrap();
495        let rich = query_balance(&bank, &api, &store, &owner);
496        assert_eq!(vec![coin(15, "btc"), coin(70, "eth")], rich);
497
498        // cannot burn too much
499        let msg = BankMsg::Burn {
500            amount: coins(20, "btc"),
501        };
502        let err = bank
503            .execute(&api, &mut store, &router, &block, owner.clone(), msg)
504            .unwrap_err();
505        assert!(matches!(err.downcast().unwrap(), StdError::Overflow { .. }));
506
507        let rich = query_balance(&bank, &api, &store, &owner);
508        assert_eq!(vec![coin(15, "btc"), coin(70, "eth")], rich);
509
510        // cannot burn from empty account
511        let msg = BankMsg::Burn {
512            amount: coins(1, "btc"),
513        };
514        let err = bank
515            .execute(&api, &mut store, &router, &block, rcpt, msg)
516            .unwrap_err();
517        assert!(matches!(err.downcast().unwrap(), StdError::Overflow { .. }));
518    }
519
520    #[test]
521    #[cfg(feature = "cosmwasm_1_3")]
522    fn set_get_denom_metadata_should_work() {
523        let api = MockApi::default();
524        let mut store = MockStorage::new();
525        let block = mock_env().block;
526        let querier: MockQuerier<Empty> = MockQuerier::new(&[]);
527        let bank = BankKeeper::new();
528        // set metadata for Ether
529        let denom_eth_name = "eth".to_string();
530        bank.set_denom_metadata(
531            &mut store,
532            denom_eth_name.clone(),
533            DenomMetadata {
534                name: denom_eth_name.clone(),
535                ..Default::default()
536            },
537        )
538        .unwrap();
539        // query metadata
540        let req = BankQuery::DenomMetadata {
541            denom: denom_eth_name.clone(),
542        };
543        let raw = bank.query(&api, &store, &querier, &block, req).unwrap();
544        let res: DenomMetadataResponse = from_json(raw).unwrap();
545        assert_eq!(res.metadata.name, denom_eth_name);
546    }
547
548    #[test]
549    #[cfg(feature = "cosmwasm_1_3")]
550    fn set_get_all_denom_metadata_should_work() {
551        let api = MockApi::default();
552        let mut store = MockStorage::new();
553        let block = mock_env().block;
554        let querier: MockQuerier<Empty> = MockQuerier::new(&[]);
555        let bank = BankKeeper::new();
556        // set metadata for Bitcoin
557        let denom_btc_name = "btc".to_string();
558        bank.set_denom_metadata(
559            &mut store,
560            denom_btc_name.clone(),
561            DenomMetadata {
562                name: denom_btc_name.clone(),
563                ..Default::default()
564            },
565        )
566        .unwrap();
567        // set metadata for Ether
568        let denom_eth_name = "eth".to_string();
569        bank.set_denom_metadata(
570            &mut store,
571            denom_eth_name.clone(),
572            DenomMetadata {
573                name: denom_eth_name.clone(),
574                ..Default::default()
575            },
576        )
577        .unwrap();
578        // query metadata
579        let req = BankQuery::AllDenomMetadata { pagination: None };
580        let raw = bank.query(&api, &store, &querier, &block, req).unwrap();
581        let res: AllDenomMetadataResponse = from_json(raw).unwrap();
582        assert_eq!(res.metadata[0].name, denom_btc_name);
583        assert_eq!(res.metadata[1].name, denom_eth_name);
584    }
585
586    #[test]
587    fn fail_on_zero_values() {
588        let api = MockApi::default();
589        let mut store = MockStorage::new();
590        let block = mock_env().block;
591        let router = MockRouter::default();
592
593        let owner = api.addr_make("owner");
594        let rcpt = api.addr_make("recipient");
595        let init_funds = vec![coin(5000, "atom"), coin(100, "eth")];
596
597        // set money
598        let bank = BankKeeper::new();
599        bank.init_balance(&mut store, &owner, init_funds).unwrap();
600
601        // can send normal amounts
602        let msg = BankMsg::Send {
603            to_address: rcpt.to_string(),
604            amount: coins(100, "atom"),
605        };
606        bank.execute(&api, &mut store, &router, &block, owner.clone(), msg)
607            .unwrap();
608
609        // fails send on no coins
610        let msg = BankMsg::Send {
611            to_address: rcpt.to_string(),
612            amount: vec![],
613        };
614        bank.execute(&api, &mut store, &router, &block, owner.clone(), msg)
615            .unwrap_err();
616
617        // fails send on 0 coins
618        let msg = BankMsg::Send {
619            to_address: rcpt.to_string(),
620            amount: coins(0, "atom"),
621        };
622        bank.execute(&api, &mut store, &router, &block, owner.clone(), msg)
623            .unwrap_err();
624
625        // fails burn on no coins
626        let msg = BankMsg::Burn { amount: vec![] };
627        bank.execute(&api, &mut store, &router, &block, owner.clone(), msg)
628            .unwrap_err();
629
630        // fails burn on 0 coins
631        let msg = BankMsg::Burn {
632            amount: coins(0, "atom"),
633        };
634        bank.execute(&api, &mut store, &router, &block, owner, msg)
635            .unwrap_err();
636
637        // can mint via sudo
638        let msg = BankSudo::Mint {
639            to_address: rcpt.to_string(),
640            amount: coins(4321, "atom"),
641        };
642        bank.sudo(&api, &mut store, &router, &block, msg).unwrap();
643
644        // mint fails with 0 tokens
645        let msg = BankSudo::Mint {
646            to_address: rcpt.to_string(),
647            amount: coins(0, "atom"),
648        };
649        bank.sudo(&api, &mut store, &router, &block, msg)
650            .unwrap_err();
651
652        // mint fails with no tokens
653        let msg = BankSudo::Mint {
654            to_address: rcpt.to_string(),
655            amount: vec![],
656        };
657        bank.sudo(&api, &mut store, &router, &block, msg)
658            .unwrap_err();
659    }
660}