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