Skip to content

Commit d6c5a8e

Browse files
committed
Added Funds chapter.
1 parent 75223ab commit d6c5a8e

File tree

1 file changed

+343
-0
lines changed

1 file changed

+343
-0
lines changed
Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
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

Comments
 (0)