|
| 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