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