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    #[derive(Debug)]
22    struct Call3Value {
23        address target;
24        bool allowFailure;
25        uint256 value;
26        bytes callData;
27    }
28
29    function aggregate3Value(Call3Value[] calls) external payable returns (bytes[] memory);
30}
31
32/// Encode a `Vec<Call>` as a multicall3 `aggregate3` call (calldata only,
33/// caller is responsible for setting `to = MULTICALL3_ADDRESS` and `value`).
34///
35/// Per-call `Call::value` fields are NOT forwarded — multicall3's `aggregate3`
36/// has no per-call value parameter. ETH must be routed via the top-level
37/// transaction value.
38///
39/// An empty `calls` slice is valid and produces the calldata for an empty
40/// `aggregate3` invocation; callers that want to avoid sending an empty
41/// multicall should gate on `calls.is_empty()` themselves.
42///
43/// Pure. No I/O.
44pub fn encode_multicall3_aggregate3(calls: &[Call]) -> Bytes {
45    let call3s: Vec<Call3> = calls
46        .iter()
47        .map(|c| Call3 { target: c.target, allowFailure: false, callData: c.calldata.clone() })
48        .collect();
49
50    aggregate3Call { calls: call3s }.abi_encode().into()
51}
52
53/// Encode a `Vec<Call>` as a multicall3 `aggregate3Value` call (calldata only;
54/// caller sets `to = MULTICALL3_ADDRESS` and the top-level tx `value`).
55///
56/// Unlike [`encode_multicall3_aggregate3`], this FORWARDS each `Call::value` as
57/// the per-call `value`. Multicall3 requires the transaction's `msg.value` to
58/// equal the sum of per-call values; callers should use
59/// `wp_evm_compose::wrap_multicall3_value`, which sets that sum.
60///
61/// `allowFailure` is `false` for every call: any sub-call revert reverts the
62/// whole transaction (atomic execution).
63///
64/// Note: inside Multicall3 each sub-call's `msg.sender` is the Multicall3
65/// contract, not the original EOA. Do not encode `approve()` or other
66/// `msg.sender`-dependent calls here.
67///
68/// Pure. No I/O.
69pub fn encode_multicall3_aggregate3_value(calls: &[Call]) -> Bytes {
70    let call3s: Vec<Call3Value> = calls
71        .iter()
72        .map(|c| Call3Value {
73            target: c.target,
74            allowFailure: false,
75            value: c.value,
76            callData: c.calldata.clone(),
77        })
78        .collect();
79
80    aggregate3ValueCall { calls: call3s }.abi_encode().into()
81}
82
83/// Canonical multicall3 deployment, present on most EVM chains.
84pub const MULTICALL3_ADDRESS: alloy_primitives::Address =
85    alloy_primitives::address!("0xcA11bde05977b3631167028862bE2a173976CA11");
86
87/// Sign-extend alloy's `I24` (a wrapping signed 24-bit type) to `i32`.
88///
89/// The `alloy_primitives::aliases::I24` type is a 3-byte two's-complement
90/// integer stored in a wider native word. Direct `as i32` or `.try_into()`
91/// would either truncate or fail; this helper performs the canonical
92/// sign extension: if the high bit of the 3-byte representation is set,
93/// the result's upper bits fill with 1s; otherwise they fill with 0s.
94///
95/// Used by family hydrate modules when reading `tick` and `tickSpacing`
96/// from `sol! { #[sol(rpc)] }` bindings that return `I24`.
97pub fn sign_extend_i24(val: alloy_primitives::aliases::I24) -> i32 {
98    let bytes = val.to_le_bytes::<3>();
99    let sign_byte = if bytes[2] & 0x80 != 0 { 0xFF_u8 } else { 0x00_u8 };
100    i32::from_le_bytes([bytes[0], bytes[1], bytes[2], sign_byte])
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106    use crate::types::Call;
107    use alloy_primitives::{address, bytes, U256};
108
109    #[test]
110    fn encode_multicall3_aggregate3_two_calls() {
111        let calls = vec![
112            Call {
113                target: address!("0x0000000000000000000000000000000000000001"),
114                calldata: bytes!("aa"),
115                value: U256::ZERO,
116            },
117            Call {
118                target: address!("0x0000000000000000000000000000000000000002"),
119                calldata: bytes!("bb"),
120                value: U256::ZERO,
121            },
122        ];
123        let encoded = encode_multicall3_aggregate3(&calls);
124        assert_eq!(&encoded[..4], &[0x82, 0xad, 0x56, 0xcb]);
125    }
126
127    #[test]
128    fn encode_multicall3_aggregate3_round_trips_two_calls() {
129        let input = vec![
130            Call {
131                target: address!("0x0000000000000000000000000000000000000001"),
132                calldata: bytes!("aa"),
133                value: U256::ZERO,
134            },
135            Call {
136                target: address!("0x0000000000000000000000000000000000000002"),
137                calldata: bytes!("bb"),
138                value: U256::ZERO,
139            },
140        ];
141        let encoded = encode_multicall3_aggregate3(&input);
142        let decoded = aggregate3Call::abi_decode(&encoded).expect("abi_decode failed");
143        assert_eq!(decoded.calls.len(), 2);
144        assert_eq!(decoded.calls[0].target, input[0].target);
145        assert!(!decoded.calls[0].allowFailure);
146        assert_eq!(decoded.calls[0].callData, input[0].calldata);
147        assert_eq!(decoded.calls[1].target, input[1].target);
148        assert!(!decoded.calls[1].allowFailure);
149        assert_eq!(decoded.calls[1].callData, input[1].calldata);
150    }
151
152    #[test]
153    fn sign_extend_i24_positive_and_negative() {
154        use alloy_primitives::aliases::I24;
155
156        // Zero
157        assert_eq!(sign_extend_i24(I24::ZERO), 0i32);
158
159        // Small positive
160        assert_eq!(sign_extend_i24(I24::try_from(100i32).unwrap()), 100i32);
161
162        // Small negative
163        assert_eq!(sign_extend_i24(I24::try_from(-100i32).unwrap()), -100i32);
164
165        // Large positive (close to I24::MAX = 2^23 - 1 = 8_388_607)
166        assert_eq!(sign_extend_i24(I24::try_from(8_388_607i32).unwrap()), 8_388_607i32);
167
168        // Large negative (close to I24::MIN = -2^23 = -8_388_608)
169        assert_eq!(sign_extend_i24(I24::try_from(-8_388_608i32).unwrap()), -8_388_608i32);
170
171        // Uniswap V3 tick bounds: [-887272, 887272]
172        assert_eq!(sign_extend_i24(I24::try_from(887_272i32).unwrap()), 887_272i32);
173        assert_eq!(sign_extend_i24(I24::try_from(-887_272i32).unwrap()), -887_272i32);
174    }
175
176    #[test]
177    fn encode_multicall3_aggregate3_value_forwards_value_and_selector() {
178        let calls = vec![
179            Call {
180                target: address!("0x0000000000000000000000000000000000000001"),
181                calldata: bytes!("aa"),
182                value: U256::ZERO,
183            },
184            Call {
185                target: address!("0x0000000000000000000000000000000000000002"),
186                calldata: bytes!("bb"),
187                value: U256::from(1_000_000_000_000_000_000u64), // 1 ETH
188            },
189        ];
190        let encoded = encode_multicall3_aggregate3_value(&calls);
191
192        // aggregate3Value selector.
193        assert_eq!(&encoded[..4], &[0x17, 0x4d, 0xea, 0x71]);
194
195        // Round-trip: allowFailure=false and per-call value is forwarded.
196        let decoded = aggregate3ValueCall::abi_decode(&encoded).expect("abi_decode failed");
197        assert_eq!(decoded.calls.len(), 2);
198        assert!(!decoded.calls[0].allowFailure);
199        assert_eq!(decoded.calls[0].value, U256::ZERO);
200        assert_eq!(decoded.calls[1].value, U256::from(1_000_000_000_000_000_000u64));
201        assert_eq!(decoded.calls[1].callData, calls[1].calldata);
202    }
203}