Skip to main content

wp_evm_multicall/
lib.rs

1#![warn(missing_docs)]
2
3//! wp-evm-multicall — thin async wrapper over the canonical Multicall3
4//! contract for dynamic, heterogeneous batch reads.
5//!
6//! `alloy::providers::MulticallBuilder` does not fit the CLI use case:
7//! - The static (tuple-generic) builder requires call count + types
8//!   known at compile time — can't loop with `.add(...)` over a
9//!   runtime-sized `Vec<Address>`.
10//! - The dynamic builder restricts all calls to a single decoder type
11//!   (homogeneous), so it can't mix e.g. `slot0` + `liquidity` +
12//!   `symbol` across pool and ERC20 contracts.
13//!
14//! This crate exposes a content-agnostic `aggregate3(provider,
15//! multicall, calls)` that ships a `Vec<Call3>` and returns
16//! `Vec<Result>` raw. Callers encode each calldata via the relevant
17//! interfaces crate (e.g. `wp-evm-v3-interfaces`) and decode each
18//! result with the matching `*Call::abi_decode_returns(returnData)`.
19//!
20//! Source: <https://github.com/mds1/multicall>
21
22use alloy_primitives::{address, Address};
23use alloy_provider::{network::Ethereum, Provider};
24use alloy_sol_types::sol;
25use anyhow::Result;
26
27sol! {
28    #[sol(rpc)]
29    interface IMulticall3 {
30        /// Single call within an `aggregate3` batch.
31        #[derive(Debug, PartialEq, Eq)]
32        struct Call3 {
33            address target;
34            bool allowFailure;
35            bytes callData;
36        }
37
38        /// Per-call result. `success == false` indicates the inner
39        /// call reverted (only possible when `Call3.allowFailure == true`;
40        /// otherwise the whole multicall reverts).
41        #[derive(Debug, PartialEq, Eq)]
42        struct Result {
43            bool success;
44            bytes returnData;
45        }
46
47        function aggregate3(Call3[] calldata calls)
48            external payable returns (Result[] memory returnData);
49
50        function getBlockNumber() external view returns (uint256 blockNumber);
51    }
52}
53
54/// Canonical Multicall3 deployment — same address on all major EVM
55/// chains via deterministic CREATE2.
56///
57/// Source: <https://github.com/mds1/multicall>
58pub const MULTICALL3_ADDRESS: Address = address!("cA11bde05977b3631167028862bE2a173976CA11");
59
60/// Send a heterogeneous batch of pre-encoded calls in a single
61/// Multicall3 RPC. `output[i]` corresponds to `calls[i]`.
62///
63/// Per-call failures (when `Call3.allowFailure == true`) surface as
64/// `IMulticall3::Result { success: false, returnData: <revert data> }`;
65/// the outer `Result` only fails for transport/RPC errors.
66///
67/// Callers encode each calldata via the relevant `*Call::abi_encode()`
68/// from the matching interfaces crate and decode each result via
69/// `*Call::abi_decode_returns(&result.returnData)`.
70///
71/// `calls` is taken by value (not `&[Call3]`) so callers can transfer
72/// freshly-built calldata bytes into the wrapper without an extra
73/// clone. No internal chunking — `calls` ships as a single Multicall3
74/// RPC, so very large batches may exceed the provider's `request_size`
75/// budget; chunk caller-side when that bites.
76pub async fn aggregate3<P: Provider<Ethereum>>(
77    provider: &P,
78    multicall: Address,
79    calls: Vec<IMulticall3::Call3>,
80) -> Result<Vec<IMulticall3::Result>> {
81    let contract = IMulticall3::new(multicall, provider);
82    let returns = contract.aggregate3(calls).call().await?;
83    Ok(returns)
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use alloy_sol_types::SolCall;
90
91    /// Lock the canonical Multicall3 address against the well-known
92    /// CREATE2 deployment.
93    #[test]
94    fn canonical_multicall3_address() {
95        assert_eq!(MULTICALL3_ADDRESS, address!("cA11bde05977b3631167028862bE2a173976CA11"),);
96    }
97
98    /// Lock the `aggregate3((address,bool,bytes)[])` selector against
99    /// the known on-chain value. Any signature regression flips it.
100    #[test]
101    fn aggregate3_selector_matches_known_value() {
102        // Computed from keccak256("aggregate3((address,bool,bytes)[])")[:4]
103        assert_eq!(IMulticall3::aggregate3Call::SELECTOR, [0x82, 0xad, 0x56, 0xcb],);
104    }
105
106    /// Lock the `getBlockNumber()` selector against Multicall3's known on-chain value.
107    #[test]
108    fn get_block_number_selector_matches_known_value() {
109        // Computed from keccak256("getBlockNumber()")[:4]
110        assert_eq!(IMulticall3::getBlockNumberCall::SELECTOR, [0x42, 0xcb, 0xb1, 0x5c],);
111    }
112}