Skip to main content

evm_fork_cache/
multicall.rs

1//! Multicall3 batching support for EvmCache.
2//!
3//! This module provides utilities to batch multiple view calls into a single
4//! EVM execution using the Multicall3 contract. This significantly reduces
5//! the number of RPC round-trips needed when loading related contract state.
6//!
7//! Multicall3 is deployed at the same address on all EVM chains:
8//! `0xcA11bde05977b3631167028862bE2a173976CA11`
9
10use alloy_primitives::{Address, Bytes, address};
11use alloy_sol_types::{SolCall, sol};
12use anyhow::{Result, anyhow};
13use tracing::{debug, instrument};
14
15use crate::access_set::StorageAccessList;
16use crate::cache::EvmCache;
17
18/// Multicall3 contract address (same on all EVM chains).
19pub const MULTICALL3_ADDRESS: Address = address!("cA11bde05977b3631167028862bE2a173976CA11");
20
21/// Maximum number of calls to batch in a single `aggregate3` invocation.
22///
23/// Caps per-batch gas and calldata size. [`execute_batched`] splits larger call
24/// sets into chunks of at most this many calls.
25pub const MAX_BATCH_SIZE: usize = 200;
26
27sol! {
28    /// Multicall3 contract interface.
29    /// See: https://github.com/mds1/multicall
30    #[sol(rpc)]
31    contract IMulticall3 {
32        struct Call3 {
33            address target;
34            bool allowFailure;
35            bytes callData;
36        }
37
38        struct Result {
39            bool success;
40            bytes returnData;
41        }
42
43        /// Aggregate calls, returning the results.
44        /// Reverts if any call fails when allowFailure is false.
45        function aggregate3(Call3[] calldata calls) external payable returns (Result[] memory returnData);
46    }
47}
48
49/// A batch of calls to execute in a single `aggregate3` invocation via Multicall3.
50///
51/// Build a batch with [`add`](Self::add) / [`add_call`](Self::add_call), then run
52/// it with [`execute`](Self::execute) or [`execute_tracked`](Self::execute_tracked).
53/// The batch should hold at most [`MAX_BATCH_SIZE`] calls; use [`execute_batched`]
54/// to chunk larger sets automatically.
55pub struct MulticallBatch {
56    calls: Vec<IMulticall3::Call3>,
57}
58
59impl MulticallBatch {
60    /// Create a new empty batch.
61    ///
62    /// ```
63    /// use evm_fork_cache::multicall::MulticallBatch;
64    ///
65    /// let batch = MulticallBatch::new();
66    /// assert!(batch.is_empty());
67    /// assert_eq!(batch.len(), 0);
68    /// ```
69    pub fn new() -> Self {
70        Self { calls: Vec::new() }
71    }
72
73    /// Create a new empty batch with room for `capacity` calls before reallocating.
74    pub fn with_capacity(capacity: usize) -> Self {
75        Self {
76            calls: Vec::with_capacity(capacity),
77        }
78    }
79
80    /// Add a call to the batch.
81    ///
82    /// - `target`: The contract address to call
83    /// - `call`: The encoded call data
84    /// - `allow_failure`: If true, the call can fail without reverting the entire batch
85    pub fn add(&mut self, target: Address, call_data: Bytes, allow_failure: bool) -> &mut Self {
86        self.calls.push(IMulticall3::Call3 {
87            target,
88            allowFailure: allow_failure,
89            callData: call_data,
90        });
91        self
92    }
93
94    /// Add a typed [`SolCall`] to the batch, ABI-encoding its calldata.
95    ///
96    /// Convenience wrapper over [`add`](Self::add) for callers holding a generated
97    /// call type rather than raw bytes. As with `add`, `allow_failure` controls
98    /// whether a revert of this call fails the whole batch (`false`) or surfaces as
99    /// `success = false` in the result (`true`).
100    pub fn add_call<C: SolCall>(
101        &mut self,
102        target: Address,
103        call: C,
104        allow_failure: bool,
105    ) -> &mut Self {
106        self.add(target, call.abi_encode().into(), allow_failure)
107    }
108
109    /// Number of calls currently in the batch.
110    pub fn len(&self) -> usize {
111        self.calls.len()
112    }
113
114    /// Returns `true` if the batch contains no calls.
115    pub fn is_empty(&self) -> bool {
116        self.calls.is_empty()
117    }
118
119    /// Execute the batch against `cache`, returning one [`IMulticall3::Result`]
120    /// per input call, in order. An empty batch returns an empty vector without
121    /// touching the EVM.
122    ///
123    /// Per-call failure is reported in the result's `success` field rather than as
124    /// an `Err`: a call added with `allow_failure = true` that reverts surfaces as
125    /// `success = false` with whatever revert data it returned. The batch as a whole
126    /// is all-or-nothing — a call added with `allow_failure = false` that reverts
127    /// makes the entire `aggregate3` call revert, which is returned here as an `Err`.
128    ///
129    /// Requires Multicall3 to be deployed at [`MULTICALL3_ADDRESS`] on the forked
130    /// chain (it is on virtually all EVM chains).
131    ///
132    /// # Errors
133    ///
134    /// Returns an error if:
135    /// - the underlying `call_raw` execution does not return
136    ///   [`ExecutionResult::Success`](revm::context::result::ExecutionResult::Success)
137    ///   — e.g. the `aggregate3` call reverted because a call with
138    ///   `allow_failure = false` failed, or Multicall3 is not deployed; or
139    /// - the returned data cannot be ABI-decoded into the expected result list.
140    #[instrument(skip(self, cache), fields(batch_size = self.calls.len()))]
141    pub fn execute(&self, cache: &mut EvmCache) -> Result<Vec<IMulticall3::Result>> {
142        if self.calls.is_empty() {
143            return Ok(Vec::new());
144        }
145
146        let call = IMulticall3::aggregate3Call {
147            calls: self.calls.clone(),
148        };
149
150        let result = cache.call_raw(
151            Address::ZERO,
152            MULTICALL3_ADDRESS,
153            call.abi_encode().into(),
154            false,
155        )?;
156
157        match result {
158            revm::context::result::ExecutionResult::Success { output, .. } => {
159                let out = output.into_data();
160                let results: Vec<IMulticall3::Result> =
161                    IMulticall3::aggregate3Call::abi_decode_returns(&out)
162                        .map_err(|e| anyhow!("Failed to decode multicall result: {:?}", e))?;
163
164                debug!(results = results.len(), "multicall batch executed");
165
166                Ok(results)
167            }
168            other => Err(anyhow!("Multicall failed: {:?}", other)),
169        }
170    }
171
172    /// Execute the batch and return both the results and the
173    /// [`StorageAccessList`] of all accounts/storage slots touched during
174    /// execution.
175    ///
176    /// Same all-or-nothing batch semantics and Multicall3 deployment requirement
177    /// as [`execute`](Self::execute), but uses `call_raw_with_access_list` to
178    /// capture the EVM state touched by the multicall, enabling prefetch on the
179    /// next cycle. An empty batch returns an empty result list and a default
180    /// (empty) access list.
181    ///
182    /// # Errors
183    ///
184    /// Returns an error under the same conditions as [`execute`](Self::execute):
185    /// the `aggregate3` call did not succeed (revert, or Multicall3 not deployed),
186    /// or the returned data failed to ABI-decode.
187    #[instrument(skip(self, cache), fields(batch_size = self.calls.len()))]
188    pub fn execute_tracked(
189        &self,
190        cache: &mut EvmCache,
191    ) -> Result<(Vec<IMulticall3::Result>, StorageAccessList)> {
192        if self.calls.is_empty() {
193            return Ok((Vec::new(), StorageAccessList::default()));
194        }
195
196        let call = IMulticall3::aggregate3Call {
197            calls: self.calls.clone(),
198        };
199
200        let (result, access_list) = cache.call_raw_with_access_list(
201            Address::ZERO,
202            MULTICALL3_ADDRESS,
203            call.abi_encode().into(),
204        )?;
205
206        match result {
207            revm::context::result::ExecutionResult::Success { output, .. } => {
208                let out = output.into_data();
209                let results: Vec<IMulticall3::Result> =
210                    IMulticall3::aggregate3Call::abi_decode_returns(&out)
211                        .map_err(|e| anyhow!("Failed to decode multicall result: {:?}", e))?;
212
213                debug!(
214                    results = results.len(),
215                    "multicall batch executed (tracked)"
216                );
217
218                Ok((results, access_list))
219            }
220            other => Err(anyhow!("Multicall failed: {:?}", other)),
221        }
222    }
223}
224
225impl Default for MulticallBatch {
226    fn default() -> Self {
227        Self::new()
228    }
229}
230
231/// Execute multiple calls in batches using Multicall3.
232///
233/// Splits large call sets into consecutive batches of at most [`MAX_BATCH_SIZE`]
234/// calls, running each via [`MulticallBatch::execute`]. Results are concatenated
235/// in input order. Requires Multicall3 to be deployed at [`MULTICALL3_ADDRESS`]
236/// on the forked chain.
237///
238/// As with a single batch, the all-or-nothing semantics are per-batch: a call
239/// added with `allow_failure = true` that reverts surfaces as `success = false`
240/// in its result, whereas a call with `allow_failure = false` that reverts makes
241/// that batch's `aggregate3` revert (returned here as an `Err`).
242///
243/// # Arguments
244/// * `cache` - The EvmCache to execute calls on
245/// * `calls` - Iterator of (target, calldata, allow_failure) tuples
246///
247/// # Returns
248/// A vector of results in the same order as the input calls.
249///
250/// # Errors
251///
252/// Returns an error as soon as any chunk's [`MulticallBatch::execute`] fails —
253/// i.e. that chunk's `aggregate3` reverted (a `allow_failure = false` call failed,
254/// or Multicall3 is not deployed) or its return data failed to decode. Results
255/// from earlier successful chunks are discarded.
256#[instrument(skip(cache, calls))]
257pub fn execute_batched<I>(cache: &mut EvmCache, calls: I) -> Result<Vec<IMulticall3::Result>>
258where
259    I: IntoIterator<Item = (Address, Bytes, bool)>,
260{
261    let calls: Vec<_> = calls.into_iter().collect();
262    let total = calls.len();
263
264    if total == 0 {
265        return Ok(Vec::new());
266    }
267
268    let mut all_results = Vec::with_capacity(total);
269
270    for chunk in calls.chunks(MAX_BATCH_SIZE) {
271        let mut batch = MulticallBatch::with_capacity(chunk.len());
272        for (target, calldata, allow_failure) in chunk {
273            batch.add(*target, calldata.clone(), *allow_failure);
274        }
275
276        let results = batch.execute(cache)?;
277        all_results.extend(results);
278    }
279
280    debug!(
281        total_calls = total,
282        batches = total.div_ceil(MAX_BATCH_SIZE),
283        "executed batched multicalls"
284    );
285
286    Ok(all_results)
287}
288
289/// Decode a single multicall [`IMulticall3::Result`] into the call's typed return.
290///
291/// # Errors
292///
293/// Returns an error if `result.success` is `false` (the call reverted), or if
294/// `result.returnData` cannot be ABI-decoded into `C::Return`.
295pub fn decode_result<C: SolCall>(result: &IMulticall3::Result) -> Result<C::Return> {
296    if !result.success {
297        return Err(anyhow!("Call failed"));
298    }
299
300    C::abi_decode_returns(&result.returnData)
301        .map_err(|e| anyhow!("Failed to decode result: {:?}", e))
302}
303
304/// Like [`decode_result`], but returns `None` instead of an `Err` when the call
305/// did not succeed or its return data fails to decode.
306pub fn try_decode_result<C: SolCall>(result: &IMulticall3::Result) -> Option<C::Return> {
307    if !result.success {
308        return None;
309    }
310
311    C::abi_decode_returns(&result.returnData).ok()
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317
318    #[test]
319    fn test_multicall_batch_creation() {
320        let mut batch = MulticallBatch::new();
321        assert!(batch.is_empty());
322        assert_eq!(batch.len(), 0);
323
324        batch.add(Address::ZERO, Bytes::new(), false);
325        assert!(!batch.is_empty());
326        assert_eq!(batch.len(), 1);
327    }
328
329    #[test]
330    fn test_multicall3_address() {
331        // Verify the Multicall3 address is correct
332        let expected: Address = "0xcA11bde05977b3631167028862bE2a173976CA11"
333            .parse()
334            .unwrap();
335        assert_eq!(MULTICALL3_ADDRESS, expected);
336    }
337}