cw_multi_test/
bank.rs

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