|  | 
|  | 1 | +--- | 
|  | 2 | +title: Handling funds | 
|  | 3 | +sidebar_position: 9 | 
|  | 4 | +--- | 
|  | 5 | + | 
|  | 6 | +# Dealing with funds | 
|  | 7 | + | 
|  | 8 | +When you hear "smart contracts", you think "blockchain". When you hear blockchain, you often think | 
|  | 9 | +of cryptocurrencies. It is not the same, but crypto assets, or as we often call them: tokens, are | 
|  | 10 | +very closely connected to the blockchain. CosmWasm has a notion of a native token. Native tokens are | 
|  | 11 | +assets managed by the blockchain core instead of smart contracts. Often such assets have some | 
|  | 12 | +special meaning, like being used for paying | 
|  | 13 | +[gas fees](https://docs.cosmos.network/v0.53/learn/beginner/gas-fees) or | 
|  | 14 | +[staking](https://en.wikipedia.org/wiki/Proof_of_stake) for consensus algorithm, but can be just | 
|  | 15 | +arbitrary assets. | 
|  | 16 | + | 
|  | 17 | +Native tokens are assigned to their owners but can be transferred. Everything that has an address in | 
|  | 18 | +the blockchain is eligible to have its native tokens. As a consequence - tokens can be assigned to | 
|  | 19 | +smart contracts! Every message sent to the smart contract can have some funds sent with it. In this | 
|  | 20 | +chapter, we will take advantage of that and create a way to reward hard work performed by admins. We | 
|  | 21 | +will create a new message - `Donate`, which will be used by anyone to donate some funds to admins, | 
|  | 22 | +divided equally. | 
|  | 23 | + | 
|  | 24 | +## Preparing messages | 
|  | 25 | + | 
|  | 26 | +Traditionally we need to prepare our messages. We need to create a new `ExecuteMsg` variant, but we | 
|  | 27 | +will also modify the `Instantiate` message a bit - we need to have some way of defining the name of | 
|  | 28 | +a native token we would use for donations. It would be possible to allow users to send any tokens | 
|  | 29 | +they want, but we want to simplify things for now. | 
|  | 30 | + | 
|  | 31 | +```rust title="src/msg.rs" {7,14} | 
|  | 32 | +use cosmwasm_std::Addr; | 
|  | 33 | +use serde::{Deserialize, Serialize}; | 
|  | 34 | + | 
|  | 35 | +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] | 
|  | 36 | +pub struct InstantiateMsg { | 
|  | 37 | +    pub admins: Vec<String>, | 
|  | 38 | +    pub donation_denom: String, | 
|  | 39 | +} | 
|  | 40 | + | 
|  | 41 | +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] | 
|  | 42 | +pub enum ExecuteMsg { | 
|  | 43 | +    AddMembers { admins: Vec<String> }, | 
|  | 44 | +    Leave {}, | 
|  | 45 | +    Donate {}, | 
|  | 46 | +} | 
|  | 47 | + | 
|  | 48 | +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] | 
|  | 49 | +pub struct GreetResp { | 
|  | 50 | +    pub message: String, | 
|  | 51 | +} | 
|  | 52 | + | 
|  | 53 | +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] | 
|  | 54 | +pub struct AdminsListResp { | 
|  | 55 | +    pub admins: Vec<Addr>, | 
|  | 56 | +} | 
|  | 57 | + | 
|  | 58 | +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] | 
|  | 59 | +pub enum QueryMsg { | 
|  | 60 | +    Greet {}, | 
|  | 61 | +    AdminsList {}, | 
|  | 62 | +} | 
|  | 63 | +``` | 
|  | 64 | + | 
|  | 65 | +We also need to add a new state part, to keep the `donation_denom`: | 
|  | 66 | + | 
|  | 67 | +```rust title="src/state.rs" {5} | 
|  | 68 | +use cosmwasm_std::Addr; | 
|  | 69 | +use cw_storage_plus::Item; | 
|  | 70 | + | 
|  | 71 | +pub const ADMINS: Item<Vec<Addr>> = Item::new("admins"); | 
|  | 72 | +pub const DONATION_DENOM: Item<String> = Item::new("donation_denom"); | 
|  | 73 | +``` | 
|  | 74 | + | 
|  | 75 | +And instantiate it properly: | 
|  | 76 | + | 
|  | 77 | +```rust title="src/contract.rs" {3,18} | 
|  | 78 | +use crate::error::ContractError; | 
|  | 79 | +use crate::msg::{AdminsListResp, ExecuteMsg, GreetResp, InstantiateMsg, QueryMsg}; | 
|  | 80 | +use crate::state::{ADMINS, DONATION_DENOM}; | 
|  | 81 | +use cosmwasm_std::{to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; | 
|  | 82 | + | 
|  | 83 | +pub fn instantiate( | 
|  | 84 | +    deps: DepsMut, | 
|  | 85 | +    _env: Env, | 
|  | 86 | +    _info: MessageInfo, | 
|  | 87 | +    msg: InstantiateMsg, | 
|  | 88 | +) -> StdResult<Response> { | 
|  | 89 | +    let admins: StdResult<Vec<_>> = msg | 
|  | 90 | +        .admins | 
|  | 91 | +        .into_iter() | 
|  | 92 | +        .map(|addr| deps.api.addr_validate(&addr)) | 
|  | 93 | +        .collect(); | 
|  | 94 | +    ADMINS.save(deps.storage, &admins?)?; | 
|  | 95 | +    DONATION_DENOM.save(deps.storage, &msg.donation_denom?)?; | 
|  | 96 | + | 
|  | 97 | +    Ok(Response::new()) | 
|  | 98 | +} | 
|  | 99 | +``` | 
|  | 100 | + | 
|  | 101 | +What also needs some corrections are tests - instantiate messages have a new field. I leave it to | 
|  | 102 | +you as an exercise. Now we have everything we need to implement donating funds to admins. | 
|  | 103 | +First, a minor update to the `Cargo.toml`, we will use an additional utility crate: | 
|  | 104 | + | 
|  | 105 | +```toml title="Cargo.toml" {14} | 
|  | 106 | +[package] | 
|  | 107 | +name = "contract" | 
|  | 108 | +version = "0.1.0" | 
|  | 109 | +edition = "2021" | 
|  | 110 | + | 
|  | 111 | +[lib] | 
|  | 112 | +crate-type = ["cdylib"] | 
|  | 113 | + | 
|  | 114 | +[dependencies] | 
|  | 115 | +cosmwasm-std = { version = "2.1.4", features = ["staking"] } | 
|  | 116 | +serde = { version = "1.0.214", default-features = false, features = ["derive"] } | 
|  | 117 | +cw-storage-plus = "2.0.0" | 
|  | 118 | +thiserror = "2.0.3" | 
|  | 119 | +cw-utils = "2.0.0" | 
|  | 120 | + | 
|  | 121 | +[dev-dependencies] | 
|  | 122 | +cw-multi-test = "2.2.0" | 
|  | 123 | +``` | 
|  | 124 | + | 
|  | 125 | +Then we can implement the donate handler: | 
|  | 126 | + | 
|  | 127 | +```rust title="src/contract.rs" {22,33-55} | 
|  | 128 | +use crate::error::ContractError; | 
|  | 129 | +use crate::msg::{AdminsListResp, ExecuteMsg, GreetResp, InstantiateMsg, QueryMsg}; | 
|  | 130 | +use crate::state::{ADMINS, DONATION_DENOM}; | 
|  | 131 | +use cosmwasm_std::{ | 
|  | 132 | +    coins, to_binary, BankMsg, Binary, Deps, DepsMut, Env, Event, MessageInfo, | 
|  | 133 | +    Response, StdResult, | 
|  | 134 | +}; | 
|  | 135 | + | 
|  | 136 | +// ... | 
|  | 137 | + | 
|  | 138 | +pub fn execute( | 
|  | 139 | +    deps: DepsMut, | 
|  | 140 | +    _env: Env, | 
|  | 141 | +    info: MessageInfo, | 
|  | 142 | +    msg: ExecuteMsg, | 
|  | 143 | +) -> Result<Response, ContractError> { | 
|  | 144 | +    use ExecuteMsg::*; | 
|  | 145 | + | 
|  | 146 | +    match msg { | 
|  | 147 | +        AddMembers { admins } => exec::add_members(deps, info, admins), | 
|  | 148 | +        Leave {} => exec::leave(deps, info).map_err(Into::into), | 
|  | 149 | +        Donate {} => exec::donate(deps, info), | 
|  | 150 | +    } | 
|  | 151 | +} | 
|  | 152 | + | 
|  | 153 | +mod exec { | 
|  | 154 | +    use cosmwasm_std::{coins, BankMsg, Event}; | 
|  | 155 | + | 
|  | 156 | +    use super::*; | 
|  | 157 | + | 
|  | 158 | +    // ... | 
|  | 159 | + | 
|  | 160 | +    pub fn donate(deps: DepsMut, info: MessageInfo) -> Result<Response, ContractError> { | 
|  | 161 | +        let denom = DONATION_DENOM.load(deps.storage)?; | 
|  | 162 | +        let admins = ADMINS.load(deps.storage)?; | 
|  | 163 | + | 
|  | 164 | +        let donation = cw_utils::must_pay(&info, &denom)?.u128(); | 
|  | 165 | + | 
|  | 166 | +        let donation_per_admin = donation / (admins.len() as u128); | 
|  | 167 | + | 
|  | 168 | +        let messages = admins.into_iter().map(|admin| BankMsg::Send { | 
|  | 169 | +            to_address: admin.to_string(), | 
|  | 170 | +            amount: coins(donation_per_admin, &denom), | 
|  | 171 | +        }); | 
|  | 172 | + | 
|  | 173 | +        let resp = Response::new() | 
|  | 174 | +            .add_messages(messages) | 
|  | 175 | +            .add_attribute("action", "donate") | 
|  | 176 | +            .add_attribute("amount", donation.to_string()) | 
|  | 177 | +            .add_attribute("per_admin", donation_per_admin.to_string()); | 
|  | 178 | + | 
|  | 179 | +        Ok(resp) | 
|  | 180 | +    } | 
|  | 181 | +} | 
|  | 182 | +``` | 
|  | 183 | + | 
|  | 184 | +Sending the funds to another contract is performed by adding bank messages to the response. The | 
|  | 185 | +blockchain would expect any message which is returned in contract response as a part of an | 
|  | 186 | +execution. This design is related to an actor model implemented by CosmWasm. You can read about it | 
|  | 187 | +[here](../../core/architecture/actor-model), but for now, you can assume this is a way to handle | 
|  | 188 | +token transfers. Before sending tokens to admins, we have to calculate the amount of donation per | 
|  | 189 | +admin. It is done by searching funds for an entry describing our donation token and dividing the | 
|  | 190 | +number of tokens sent by the number of admins. Note that because the integral division is always | 
|  | 191 | +rounding down. | 
|  | 192 | + | 
|  | 193 | +As a consequence, it is possible that not all tokens sent as a donation would end up with no admins | 
|  | 194 | +accounts. Any leftover would be left on our contract account forever. There are plenty of ways of | 
|  | 195 | +dealing with this issue - figuring out one of them would be a great exercise. | 
|  | 196 | + | 
|  | 197 | +The last missing part is updating the `ContractError` - the `must_pay` call returns a | 
|  | 198 | +`cw_utils::PaymentError` which we can't convert to our error type yet: | 
|  | 199 | + | 
|  | 200 | +```rust title="src/error.rs" {2,11-12} | 
|  | 201 | +use cosmwasm_std::{Addr, StdError}; | 
|  | 202 | +use cw_utils::PaymentError; | 
|  | 203 | +use thiserror::Error; | 
|  | 204 | + | 
|  | 205 | +#[derive(Error, Debug, PartialEq)] | 
|  | 206 | +pub enum ContractError { | 
|  | 207 | +    #[error("{0}")] | 
|  | 208 | +    StdError(#[from] StdError), | 
|  | 209 | +    #[error("{sender} is not contract admin")] | 
|  | 210 | +    Unauthorized { sender: Addr }, | 
|  | 211 | +    #[error("Payment error: {0}")] | 
|  | 212 | +    Payment(#[from] PaymentError), | 
|  | 213 | +} | 
|  | 214 | +``` | 
|  | 215 | + | 
|  | 216 | +As you can see, to handle incoming funds, I used the utility function - I encourage you to take a | 
|  | 217 | +look at [its implementation](https://docs.rs/cw-utils/latest/src/cw_utils/payment.rs.html#32-39) - | 
|  | 218 | +this would give you a good understanding of how incoming funds are structured in `MessageInfo`. | 
|  | 219 | + | 
|  | 220 | +Now it's time to check if the funds are distributed correctly. The way for that is to write a test. | 
|  | 221 | + | 
|  | 222 | +```rust title="src/contract.rs" | 
|  | 223 | +// ... | 
|  | 224 | + | 
|  | 225 | +#[cfg(test)] | 
|  | 226 | +mod tests { | 
|  | 227 | +    use cosmwasm_std::coins; | 
|  | 228 | +    use cw_multi_test::{App, ContractWrapper, Executor, IntoAddr}; | 
|  | 229 | + | 
|  | 230 | +    use crate::msg::AdminsListResp; | 
|  | 231 | + | 
|  | 232 | +    use super::*; | 
|  | 233 | + | 
|  | 234 | +    #[test] | 
|  | 235 | +    fn donations() { | 
|  | 236 | +        let owner = "owner".into_addr(); | 
|  | 237 | +        let user = "user".into_addr(); | 
|  | 238 | +        let admin1 = "admin1".into_addr(); | 
|  | 239 | +        let admin2 = "admin2".into_addr(); | 
|  | 240 | + | 
|  | 241 | +        let mut app = App::new(|router, _, storage| { | 
|  | 242 | +            router | 
|  | 243 | +                .bank | 
|  | 244 | +                .init_balance(storage, &user, coins(5, "eth")) | 
|  | 245 | +                .unwrap() | 
|  | 246 | +        }); | 
|  | 247 | + | 
|  | 248 | +        let code = ContractWrapper::new(execute, instantiate, query); | 
|  | 249 | +        let code_id = app.store_code(Box::new(code)); | 
|  | 250 | + | 
|  | 251 | +        let addr = app | 
|  | 252 | +            .instantiate_contract( | 
|  | 253 | +                code_id, | 
|  | 254 | +                owner, | 
|  | 255 | +                &InstantiateMsg { | 
|  | 256 | +                    admins: vec![admin1.to_string(), admin2.to_string()], | 
|  | 257 | +                    donation_denom: "eth".to_owned(), | 
|  | 258 | +                }, | 
|  | 259 | +                &[], | 
|  | 260 | +                "Contract", | 
|  | 261 | +                None, | 
|  | 262 | +            ) | 
|  | 263 | +            .unwrap(); | 
|  | 264 | + | 
|  | 265 | +        app.execute_contract( | 
|  | 266 | +            user.clone(), | 
|  | 267 | +            addr.clone(), | 
|  | 268 | +            &ExecuteMsg::Donate {}, | 
|  | 269 | +            &coins(5, "eth"), | 
|  | 270 | +        ) | 
|  | 271 | +        .unwrap(); | 
|  | 272 | + | 
|  | 273 | +        assert_eq!( | 
|  | 274 | +            app.wrap() | 
|  | 275 | +                .query_balance(user.as_str(), "eth") | 
|  | 276 | +                .unwrap() | 
|  | 277 | +                .amount | 
|  | 278 | +                .u128(), | 
|  | 279 | +            0 | 
|  | 280 | +        ); | 
|  | 281 | + | 
|  | 282 | +        assert_eq!( | 
|  | 283 | +            app.wrap() | 
|  | 284 | +                .query_balance(&addr, "eth") | 
|  | 285 | +                .unwrap() | 
|  | 286 | +                .amount | 
|  | 287 | +                .u128(), | 
|  | 288 | +            1 | 
|  | 289 | +        ); | 
|  | 290 | + | 
|  | 291 | +        assert_eq!( | 
|  | 292 | +            app.wrap() | 
|  | 293 | +                .query_balance(admin1.as_str(), "eth") | 
|  | 294 | +                .unwrap() | 
|  | 295 | +                .amount | 
|  | 296 | +                .u128(), | 
|  | 297 | +            2 | 
|  | 298 | +        ); | 
|  | 299 | + | 
|  | 300 | +        assert_eq!( | 
|  | 301 | +            app.wrap() | 
|  | 302 | +                .query_balance(admin2.as_str(), "eth") | 
|  | 303 | +                .unwrap() | 
|  | 304 | +                .amount | 
|  | 305 | +                .u128(), | 
|  | 306 | +            2 | 
|  | 307 | +        ); | 
|  | 308 | +    } | 
|  | 309 | +} | 
|  | 310 | +``` | 
|  | 311 | + | 
|  | 312 | +Fairly simple. I don't particularly appreciate that every balance check is eight lines of code, but | 
|  | 313 | +it can be improved by enclosing this assertion into a separate function, probably with the | 
|  | 314 | +[`#[track_caller]`](https://doc.rust-lang.org/reference/attributes/diagnostics.html#the-track_caller-attribute) | 
|  | 315 | +attribute. | 
|  | 316 | + | 
|  | 317 | +The critical thing to talk about is how `app` creation changed. Because we need some initial tokens | 
|  | 318 | +on a `user` account, instead of using the default constructor, we have to provide it with an | 
|  | 319 | +initializer function. Unfortunately, even though the | 
|  | 320 | +[`new`](https://docs.rs/cw-multi-test/latest/cw_multi_test/struct.App.html#method.new) function is | 
|  | 321 | +not very complicated, it's not easy to use. What it takes as an argument is a closure with three | 
|  | 322 | +arguments - the [`Router`](https://docs.rs/cw-multi-test/latest/cw_multi_test/struct.Router.html) | 
|  | 323 | +with all modules supported by multi-test, the API object, and the state. This function is called | 
|  | 324 | +once during contract instantiation. The `router` object contains some generic fields - we are | 
|  | 325 | +interested in `bank` in particular. It has a type of | 
|  | 326 | +[`BankKeeper`](https://docs.rs/cw-multi-test/latest/cw_multi_test/struct.BankKeeper.html), where the | 
|  | 327 | +[`init_balance`](https://docs.rs/cw-multi-test/latest/cw_multi_test/struct.BankKeeper.html#method.init_balance) | 
|  | 328 | +function sits. | 
|  | 329 | + | 
|  | 330 | +## Plot Twist | 
|  | 331 | + | 
|  | 332 | +As we covered most of the important basics about building Rust smart contracts, I have a serious | 
|  | 333 | +exercise for you. | 
|  | 334 | + | 
|  | 335 | +The contract we built has an exploitable bug. All donations are distributed equally across admins. | 
|  | 336 | +However, every admin is eligible to add another admin. And nothing is preventing the admin from | 
|  | 337 | +adding himself to the list and receiving twice as many rewards as others! | 
|  | 338 | + | 
|  | 339 | +Try to write a test that detects such a bug, then fix it and ensure the bug nevermore occurs. | 
|  | 340 | + | 
|  | 341 | +Even if the admin cannot add the same address to the list, he can always create new accounts and add | 
|  | 342 | +them. Handling this kind of case is done by properly designing whole applications, which is out of | 
|  | 343 | +this chapter's scope. | 
0 commit comments