Skip to content

Commit 1c5660f

Browse files
authored
Add a document for adding middleware to pallet-ibc (#377)
* Add a document for adding middleware to pallet-ibc * improve doc * rewrite "Potential issues" & reformat * mention `on_timeout_packet`
1 parent 72c42bd commit 1c5660f

File tree

1 file changed

+104
-0
lines changed

1 file changed

+104
-0
lines changed

docs/middleware.md

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
## Adding a middleware on pallet-ibc
2+
3+
### Overview
4+
5+
The middleware is a module that wraps another module and adds some additional functionality to it. In the context of
6+
IBC, it means that the middleware is a module that is executed before/after the IBC module, and can access the packet
7+
data, take fees, send new packets etc. The middleware should be considered not a separate module, but rather a part of
8+
the module that
9+
it's wrapping.
10+
11+
To add a middleware for some IBC application (e.g., ics-20 - transfer app), one needs to create a new struct that
12+
implements the `ibc::Module` trait and include it in the chain of other modules in runtime. Another important
13+
step is to make sure that the middleware [satisfies](#potential-issues) IBC packet flow to avoid unexpected behavior
14+
of your app (for example, a possibility for double-spending).
15+
16+
### Implementing `Module` trait and adding to runtime
17+
18+
As an example, we'll take a look at the existing module for taking fees on transfers, `ics20_fee` (`contracts/pallet-ibc/src/ics20_fee`).
19+
First, we need to create a struct, that contains an `inner` field, which is of type of the next module in the chain, so
20+
we can call it before/after our module execution is finished.
21+
22+
```rust
23+
pub struct Ics20ServiceCharge<S: Module> {
24+
inner: S,
25+
}
26+
```
27+
28+
The `Module` contains various amount of callback methods that we can use, but probably the most important ones are
29+
`on_recv_packet`, `on_acknowledgement_packet` and `on_timeout_packet`. In our case, we want to take fee from the user
30+
when the packet arrives and the transfer already happened, so the implementation will look something like this:
31+
32+
```rust
33+
impl<S: Module> Module for Ics20ServiceCharge<S> {
34+
fn on_recv_packet(
35+
&self,
36+
_ctx: &dyn ModuleCallbackContext,
37+
output: &mut ModuleOutputBuilder,
38+
packet: &mut Packet,
39+
relayer: &Signer,
40+
) -> Result<Acknowledgement, Error> {
41+
let mut ctx = Context::<T>::default();
42+
let ack = self.inner.on_recv_packet(&mut ctx, output, packet, relayer)?;
43+
let _ = Self::process_fee(&mut ctx, packet, &ack).map_err(|e| {
44+
log::error!(target: "pallet_ibc", "Error processing fee: {:?}", e);
45+
});
46+
Ok(ack)
47+
}
48+
49+
// ...
50+
}
51+
```
52+
53+
As you can see, here we're first propagating the call to the inner module (which will be the `ics20` app, eventually),
54+
and only then taking the fee. We could do it the other way around, but it would require additional checks to be made
55+
(like, the token should exist, the amount should be correct, etc.). You may also notice, that the error from `process_fee`
56+
function is ignored. This is because we don't want to fail the whole chain of calls if the fee is not taken, because it
57+
may lead to critical problems (more in [Potential issues](#potential-issues) section).
58+
59+
The other methods should just call the inner module, like this:
60+
61+
```rust
62+
impl<S: Module> Module for Ics20ServiceCharge<S> {
63+
fn on_acknowledgement_packet(
64+
&mut self,
65+
ctx: &dyn ModuleCallbackContext,
66+
output: &mut ModuleOutputBuilder,
67+
packet: &mut Packet,
68+
acknowledgement: &Acknowledgement,
69+
relayer: &Signer,
70+
) -> Result<(), Ics04Error> {
71+
self.inner
72+
.on_acknowledgement_packet(ctx, output, packet, acknowledgement, relayer)
73+
}
74+
// ...
75+
}
76+
```
77+
78+
The final step is to add the module to the runtime. In the pallet's Config, you can find `Router` associated type, which
79+
in a concrete implementation may look like
80+
81+
```rust
82+
pub struct Router {
83+
ics20: ics20::memo::Memo<ics20_fee::Ics20ServiceCharge<ics20::IbcModule>>,
84+
}
85+
```
86+
87+
The modules here are nested in each other, forming a chain. In this case (assuming that the Memo module is executed
88+
after the inner),
89+
the flow will be `ics20 -> ics20_fee -> memo`.
90+
91+
### Potential issues
92+
93+
1. It's important to understand that the middleware is not a separate module, but rather a part of the module that it's
94+
wrapping. This means that the middleware should not fail the execution after the execution of the `inner` module has
95+
succeeded, because it may lead to unexpected behavior of the app. In our example, if the middleware fails (by
96+
throwing an error), the packet won't be acknowledged, but the transfer will still happen. This means that even that
97+
the tokens were transferred,
98+
the whole packet is considered as failed and the tokens on the source chain will be returned back to the sender's
99+
account. That's why we're ignoring the error from `process_fee` function, because we don't want the packet to fail
100+
when the transfer already happened.
101+
102+
2. Another important thing to note is that the middleware should not change the packet data, because it may lead to
103+
the same problem as above. For example, if the middleware changes the amount of tokens in the packet, the recipient
104+
will receive more tokens than the sender sent.

0 commit comments

Comments
 (0)