ibc_middleware_overflow_receive/
lib.rs

1//! IBC middleware that sends amounts overflowing some target to another address.
2
3#![cfg_attr(not(test), no_std)]
4#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
5#![cfg_attr(
6    not(test),
7    deny(
8        missing_docs,
9        rust_2018_idioms,
10        clippy::string_to_string,
11        clippy::std_instead_of_core,
12        clippy::string_add,
13        clippy::str_to_string,
14        clippy::infinite_loop,
15        clippy::unwrap_used,
16        clippy::expect_used,
17        clippy::panic,
18        clippy::cfg_not_test,
19        clippy::as_conversions,
20        clippy::alloc_instead_of_core,
21        clippy::float_arithmetic,
22        clippy::empty_docs,
23        clippy::empty_line_after_doc_comments,
24        clippy::empty_line_after_outer_attr,
25        clippy::suspicious_doc_comments,
26        clippy::redundant_locals,
27        clippy::redundant_comparisons,
28        clippy::out_of_bounds_indexing,
29        clippy::empty_loop,
30        clippy::cast_sign_loss,
31        clippy::cast_possible_truncation,
32        clippy::cast_possible_wrap,
33        clippy::cast_lossless,
34        clippy::arithmetic_side_effects,
35        clippy::dbg_macro,
36        clippy::print_stdout,
37        clippy::print_stderr,
38        clippy::shadow_unrelated,
39        clippy::useless_attribute,
40        clippy::zero_repeat_side_effects,
41        clippy::builtin_type_shadow,
42        clippy::unreachable
43    )
44)]
45
46extern crate alloc;
47
48mod msg;
49#[cfg(test)]
50pub(crate) mod tests;
51
52use alloc::borrow::ToOwned;
53use alloc::format;
54use alloc::string::{String, ToString};
55use alloc::vec;
56use alloc::vec::Vec;
57use core::fmt;
58
59use ibc_app_transfer_types::packet::PacketData;
60use ibc_app_transfer_types::{is_receiver_chain_source, Coin, PrefixedDenom, TracePrefix};
61use ibc_core_channel_types::acknowledgement::{
62    Acknowledgement, AcknowledgementStatus, StatusValue as AckStatusValue,
63};
64use ibc_core_channel_types::channel::{Counterparty, Order};
65use ibc_core_channel_types::error::ChannelError;
66use ibc_core_channel_types::packet::Packet;
67use ibc_core_channel_types::Version;
68use ibc_core_host_types::identifiers::{ChannelId, ConnectionId, PortId};
69use ibc_core_router::module::Module as IbcCoreModule;
70use ibc_core_router_types::event::{ModuleEvent, ModuleEventAttribute};
71use ibc_core_router_types::module::ModuleExtras;
72use ibc_middleware_module::MiddlewareModule;
73use ibc_middleware_module_macros::from_middleware;
74use ibc_primitives::*;
75use serde::{Deserialize, Serialize};
76
77#[doc(inline)]
78pub use self::msg::PacketMetadata;
79
80/// Module name of the ORM.
81const MODULE: &str = "overflow-receive-middleware";
82
83#[derive(Debug)]
84enum MiddlewareError {
85    /// Error message.
86    Message(String),
87    /// Forward the call to the next middleware.
88    ForwardToNextMiddleware,
89}
90
91/// Context data required by the [`OverflowReceiveMiddleware`].
92pub trait OverflowRecvContext {
93    /// Metadata included in ICS-20 packet memos,
94    /// related with the overflow receive middleware.
95    type PacketMetadata: msg::PacketMetadata + Serialize + for<'de> Deserialize<'de> + Sized;
96
97    /// Error returned by fallible operations.
98    type Error: fmt::Display;
99
100    /// Mint coins.
101    fn mint_coins_execute(
102        &mut self,
103        receiver: &<Self::PacketMetadata as PacketMetadata>::AccountId,
104        coin: &Coin<PrefixedDenom>,
105    ) -> Result<(), Self::Error>;
106
107    /// Unescrow coins.
108    fn unescrow_coins_execute(
109        &mut self,
110        receiver: &<Self::PacketMetadata as PacketMetadata>::AccountId,
111        port: &PortId,
112        channel: &ChannelId,
113        coin: &Coin<PrefixedDenom>,
114    ) -> Result<(), Self::Error>;
115}
116
117/// Overflow receive middleware entrypoint, which intercepts compatible
118/// ICS-20 packets and sends funds to a new address in case they exceed
119/// some specified amount.
120#[derive(Debug)]
121pub struct OverflowReceiveMiddleware<M> {
122    next: M,
123}
124
125#[cfg_attr(coverage_nightly, coverage(off))]
126impl<M> OverflowReceiveMiddleware<M> {
127    /// Return an immutable ref to the next middleware.
128    pub fn next(&self) -> &M {
129        &self.next
130    }
131
132    /// Return a mutable ref to the next middleware.
133    pub fn next_mut(&mut self) -> &mut M {
134        &mut self.next
135    }
136
137    /// Wrap an existing middleware in the ORM.
138    pub const fn wrap(next: M) -> Self {
139        Self { next }
140    }
141}
142
143from_middleware! {
144    #[cfg_attr(coverage_nightly, coverage(off))]
145    impl<M> IbcCoreModule for OverflowReceiveMiddleware<M>
146    where
147        M: IbcCoreModule + OverflowRecvContext,
148}
149
150impl<M> OverflowReceiveMiddleware<M>
151where
152    M: IbcCoreModule + OverflowRecvContext,
153{
154    fn on_recv_packet_execute_inner(
155        &mut self,
156        extras: &mut ModuleExtras,
157        packet: &Packet,
158        relayer: &Signer,
159    ) -> Result<Option<Acknowledgement>, MiddlewareError> {
160        let (transfer_pkt, orm_metadata) =
161            decode_overflow_receive_msg::<M::PacketMetadata>(packet)?;
162
163        let (override_amount, remainder_amount) = match transfer_pkt
164            .token
165            .amount
166            .checked_sub((*orm_metadata.target_amount()).into())
167        {
168            Some(amt) if *amt != [0u64, 0, 0, 0] => ((*orm_metadata.target_amount()).into(), amt),
169            Some(_ /* = 0 */) => return Err(MiddlewareError::ForwardToNextMiddleware),
170            None => {
171                return Err(MiddlewareError::Message(format!(
172                    "Target amount ({}) is greater than the received amount ({})",
173                    orm_metadata.target_amount(),
174                    transfer_pkt.token.amount
175                )))
176            }
177        };
178
179        let mut attributes = vec![];
180
181        if is_receiver_chain_source(
182            packet.port_id_on_a.clone(),
183            packet.chan_id_on_a.clone(),
184            &transfer_pkt.token.denom,
185        ) {
186            let prefix = TracePrefix::new(packet.port_id_on_a.clone(), packet.chan_id_on_a.clone());
187            let coin = {
188                let mut c = Coin {
189                    denom: transfer_pkt.token.denom.clone(),
190                    amount: remainder_amount,
191                };
192                c.denom.remove_trace_prefix(&prefix);
193                c
194            };
195
196            self.next
197                .unescrow_coins_execute(
198                    orm_metadata.overflow_receiver(),
199                    &packet.port_id_on_b,
200                    &packet.chan_id_on_b,
201                    &coin,
202                )
203                .map_err(|err| {
204                    MiddlewareError::Message(format!(
205                        "Unescrow to {} failed: {err}",
206                        orm_metadata.overflow_receiver()
207                    ))
208                })?;
209
210            push_event_attr(&mut attributes, "operation", "unescrow");
211            push_event_attr(&mut attributes, "denom", coin.denom.to_string());
212        } else {
213            let prefix = TracePrefix::new(packet.port_id_on_b.clone(), packet.chan_id_on_b.clone());
214            let coin = {
215                let mut c = Coin {
216                    denom: transfer_pkt.token.denom.clone(),
217                    amount: remainder_amount,
218                };
219                c.denom.add_trace_prefix(prefix);
220                c
221            };
222
223            self.next
224                .mint_coins_execute(orm_metadata.overflow_receiver(), &coin)
225                .map_err(|err| {
226                    MiddlewareError::Message(format!(
227                        "Mint to {} failed: {err}",
228                        orm_metadata.overflow_receiver()
229                    ))
230                })?;
231
232            push_event_attr(&mut attributes, "operation", "mint");
233            push_event_attr(&mut attributes, "denom", coin.denom.to_string());
234        }
235
236        push_event_attr(&mut attributes, "amount", remainder_amount.to_string());
237        push_event_attr(
238            &mut attributes,
239            "overflow-receiver",
240            orm_metadata.overflow_receiver().to_string(),
241        );
242        push_event_attr(
243            &mut attributes,
244            "info",
245            "Successfully redirected funds with overflow receiver middleware",
246        );
247        emit_event_with_attrs(extras, attributes);
248
249        let override_packet = {
250            let ics20_packet_data = PacketData {
251                token: Coin {
252                    denom: transfer_pkt.token.denom.clone(),
253                    amount: override_amount,
254                },
255                memo: extract_next_memo_from_orm_packet::<M::PacketMetadata>(&transfer_pkt).into(),
256                ..transfer_pkt
257            };
258
259            let encoded_packet_data = serde_json::to_vec(&ics20_packet_data).map_err(|err| {
260                MiddlewareError::Message(format!("Failed to encode ICS-20 packet: {err}"))
261            })?;
262
263            Packet {
264                data: encoded_packet_data,
265                ..packet.clone()
266            }
267        };
268        let (next_extras, maybe_ack) = self.next.on_recv_packet_execute(&override_packet, relayer);
269        join_module_extras(extras, next_extras);
270
271        Ok(maybe_ack)
272    }
273}
274
275#[cfg_attr(coverage_nightly, coverage(off))]
276impl<M> MiddlewareModule for OverflowReceiveMiddleware<M>
277where
278    M: IbcCoreModule + OverflowRecvContext,
279{
280    type NextMiddleware = M;
281
282    #[inline]
283    fn next_middleware(&self) -> &M {
284        self.next()
285    }
286
287    #[inline]
288    fn next_middleware_mut(&mut self) -> &mut M {
289        self.next_mut()
290    }
291
292    fn middleware_on_recv_packet_execute(
293        &mut self,
294        packet: &Packet,
295        relayer: &Signer,
296    ) -> (ModuleExtras, Option<Acknowledgement>) {
297        let mut extras = ModuleExtras::empty();
298
299        match self.on_recv_packet_execute_inner(&mut extras, packet, relayer) {
300            Ok(maybe_ack) => (extras, maybe_ack),
301            Err(MiddlewareError::ForwardToNextMiddleware) => {
302                self.next.on_recv_packet_execute(packet, relayer)
303            }
304            Err(MiddlewareError::Message(err)) => (extras, Some(new_error_ack(err).into())),
305        }
306    }
307}
308
309#[cfg_attr(coverage_nightly, coverage(off))]
310#[inline]
311fn new_error_ack(message: impl fmt::Display) -> AcknowledgementStatus {
312    AcknowledgementStatus::error(
313        // NB: allow expect here, because this should only fail if
314        // we construct an `AckStatusValue` with an empty message
315        #[allow(clippy::expect_used)]
316        AckStatusValue::new(format!("{MODULE} error: {message}"))
317            .expect("Acknowledgement error must not be empty"),
318    )
319}
320
321#[inline]
322fn event_attr<K, V>(key: K, value: V) -> ModuleEventAttribute
323where
324    K: Into<String>,
325    V: Into<String>,
326{
327    ModuleEventAttribute {
328        key: key.into(),
329        value: value.into(),
330    }
331}
332
333#[inline]
334fn push_event_attr<K, V>(attributes: &mut Vec<ModuleEventAttribute>, key: K, value: V)
335where
336    K: Into<String>,
337    V: Into<String>,
338{
339    attributes.push(event_attr(key, value));
340}
341
342#[inline]
343fn emit_event_with_attrs(extras: &mut ModuleExtras, attributes: Vec<ModuleEventAttribute>) {
344    extras.events.push(ModuleEvent {
345        kind: MODULE.to_owned(),
346        attributes,
347    });
348}
349
350fn decode_ics20_msg(packet: &Packet) -> Result<PacketData, MiddlewareError> {
351    serde_json::from_slice(&packet.data).map_err(|_| {
352        // NB: if `packet.data` is not a valid fungible token transfer
353        // packet, we forward the call to the next middleware
354        MiddlewareError::ForwardToNextMiddleware
355    })
356}
357
358fn decode_overflow_receive_msg<Msg>(packet: &Packet) -> Result<(PacketData, Msg), MiddlewareError>
359where
360    Msg: msg::PacketMetadata + for<'de> Deserialize<'de>,
361{
362    let transfer_pkt = decode_ics20_msg(packet)?;
363
364    let json_obj_memo: serde_json::Map<String, serde_json::Value> =
365        serde_json::from_str(transfer_pkt.memo.as_ref()).map_err(|_| {
366            // NB: if the ICS-20 packet memo is not a valid JSON object, we forward
367            // this call to the next middleware
368            MiddlewareError::ForwardToNextMiddleware
369        })?;
370
371    if !Msg::is_overflow_receive_msg(&json_obj_memo) {
372        // NB: the memo was a valid json object, but it wasn't up to
373        // the ORM to consume it, so we forward the call to the next middleware
374        return Err(MiddlewareError::ForwardToNextMiddleware);
375    }
376
377    serde_json::from_value(json_obj_memo.into()).map_or_else(
378        |err| Err(MiddlewareError::Message(err.to_string())),
379        |msg| Ok((transfer_pkt, msg)),
380    )
381}
382
383fn join_module_extras(first: &mut ModuleExtras, mut second: ModuleExtras) {
384    first.events.append(&mut second.events);
385    first.log.append(&mut second.log);
386}
387
388// NB: Assume that `src_packet_data` has been validated as a PFM packet
389#[inline]
390fn extract_next_memo_from_orm_packet<Msg>(src_packet_data: &PacketData) -> String
391where
392    Msg: PacketMetadata,
393{
394    #[allow(clippy::unwrap_used, clippy::unreachable)]
395    let serde_json::Value::Object(memo_obj) =
396        serde_json::from_str(src_packet_data.memo.as_ref()).unwrap()
397    else {
398        unreachable!()
399    };
400
401    #[allow(clippy::unwrap_used)]
402    serde_json::to_string(&Msg::strip_middleware_msg(memo_obj)).unwrap()
403}