Skip to main content

wp_evm_base/
evm.rs

1//! EVM helpers: multicall encoding, provider extensions.
2//!
3//! Multicall encoding is pure (no async, no provider). Provider helpers
4//! that perform actual calls live in family `hydrate` modules instead —
5//! this module never reads the chain.
6
7use crate::types::Call;
8use alloy_primitives::Bytes;
9use alloy_sol_types::{sol, SolCall};
10
11sol! {
12    #[derive(Debug)]
13    struct Call3 {
14        address target;
15        bool allowFailure;
16        bytes callData;
17    }
18
19    function aggregate3(Call3[] calls) external payable returns (bytes[] memory);
20}
21
22/// Encode a `Vec<Call>` as a multicall3 `aggregate3` call (calldata only,
23/// caller is responsible for setting `to = MULTICALL3_ADDRESS` and `value`).
24///
25/// Per-call `Call::value` fields are NOT forwarded — multicall3's `aggregate3`
26/// has no per-call value parameter. ETH must be routed via the top-level
27/// transaction value.
28///
29/// An empty `calls` slice is valid and produces the calldata for an empty
30/// `aggregate3` invocation; callers that want to avoid sending an empty
31/// multicall should gate on `calls.is_empty()` themselves.
32///
33/// Pure. No I/O.
34pub fn encode_multicall3_aggregate3(calls: &[Call]) -> Bytes {
35    let call3s: Vec<Call3> = calls
36        .iter()
37        .map(|c| Call3 { target: c.target, allowFailure: false, callData: c.calldata.clone() })
38        .collect();
39
40    aggregate3Call { calls: call3s }.abi_encode().into()
41}
42
43/// Canonical multicall3 deployment, present on most EVM chains.
44pub const MULTICALL3_ADDRESS: alloy_primitives::Address =
45    alloy_primitives::address!("0xcA11bde05977b3631167028862bE2a173976CA11");
46
47/// Sign-extend alloy's `I24` (a wrapping signed 24-bit type) to `i32`.
48///
49/// The `alloy_primitives::aliases::I24` type is a 3-byte two's-complement
50/// integer stored in a wider native word. Direct `as i32` or `.try_into()`
51/// would either truncate or fail; this helper performs the canonical
52/// sign extension: if the high bit of the 3-byte representation is set,
53/// the result's upper bits fill with 1s; otherwise they fill with 0s.
54///
55/// Used by family hydrate modules when reading `tick` and `tickSpacing`
56/// from `sol! { #[sol(rpc)] }` bindings that return `I24`.
57pub fn sign_extend_i24(val: alloy_primitives::aliases::I24) -> i32 {
58    let bytes = val.to_le_bytes::<3>();
59    let sign_byte = if bytes[2] & 0x80 != 0 { 0xFF_u8 } else { 0x00_u8 };
60    i32::from_le_bytes([bytes[0], bytes[1], bytes[2], sign_byte])
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66    use crate::types::Call;
67    use alloy_primitives::{address, bytes, U256};
68
69    #[test]
70    fn encode_multicall3_aggregate3_two_calls() {
71        let calls = vec![
72            Call {
73                target: address!("0x0000000000000000000000000000000000000001"),
74                calldata: bytes!("aa"),
75                value: U256::ZERO,
76            },
77            Call {
78                target: address!("0x0000000000000000000000000000000000000002"),
79                calldata: bytes!("bb"),
80                value: U256::ZERO,
81            },
82        ];
83        let encoded = encode_multicall3_aggregate3(&calls);
84        assert_eq!(&encoded[..4], &[0x82, 0xad, 0x56, 0xcb]);
85    }
86
87    #[test]
88    fn encode_multicall3_aggregate3_round_trips_two_calls() {
89        let input = vec![
90            Call {
91                target: address!("0x0000000000000000000000000000000000000001"),
92                calldata: bytes!("aa"),
93                value: U256::ZERO,
94            },
95            Call {
96                target: address!("0x0000000000000000000000000000000000000002"),
97                calldata: bytes!("bb"),
98                value: U256::ZERO,
99            },
100        ];
101        let encoded = encode_multicall3_aggregate3(&input);
102        let decoded = aggregate3Call::abi_decode(&encoded).expect("abi_decode failed");
103        assert_eq!(decoded.calls.len(), 2);
104        assert_eq!(decoded.calls[0].target, input[0].target);
105        assert!(!decoded.calls[0].allowFailure);
106        assert_eq!(decoded.calls[0].callData, input[0].calldata);
107        assert_eq!(decoded.calls[1].target, input[1].target);
108        assert!(!decoded.calls[1].allowFailure);
109        assert_eq!(decoded.calls[1].callData, input[1].calldata);
110    }
111
112    #[test]
113    fn sign_extend_i24_positive_and_negative() {
114        use alloy_primitives::aliases::I24;
115
116        // Zero
117        assert_eq!(sign_extend_i24(I24::ZERO), 0i32);
118
119        // Small positive
120        assert_eq!(sign_extend_i24(I24::try_from(100i32).unwrap()), 100i32);
121
122        // Small negative
123        assert_eq!(sign_extend_i24(I24::try_from(-100i32).unwrap()), -100i32);
124
125        // Large positive (close to I24::MAX = 2^23 - 1 = 8_388_607)
126        assert_eq!(sign_extend_i24(I24::try_from(8_388_607i32).unwrap()), 8_388_607i32);
127
128        // Large negative (close to I24::MIN = -2^23 = -8_388_608)
129        assert_eq!(sign_extend_i24(I24::try_from(-8_388_608i32).unwrap()), -8_388_608i32);
130
131        // Uniswap V3 tick bounds: [-887272, 887272]
132        assert_eq!(sign_extend_i24(I24::try_from(887_272i32).unwrap()), 887_272i32);
133        assert_eq!(sign_extend_i24(I24::try_from(-887_272i32).unwrap()), -887_272i32);
134    }
135}