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 tracing::{debug, instrument};
13
14use crate::access_set::StorageAccessList;
15use crate::cache::EvmCache;
16use crate::errors::{MulticallError, MulticallResult as Result};
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).map_err(|e| {
162                        MulticallError::Decode {
163                            details: format!("{e:?}"),
164                        }
165                    })?;
166
167                debug!(results = results.len(), "multicall batch executed");
168
169                Ok(results)
170            }
171            other => Err(MulticallError::AggregateFailed {
172                result: format!("{other:?}"),
173            }),
174        }
175    }
176
177    /// Execute the batch and return both the results and the
178    /// [`StorageAccessList`] of all accounts/storage slots touched during
179    /// execution.
180    ///
181    /// Same all-or-nothing batch semantics and Multicall3 deployment requirement
182    /// as [`execute`](Self::execute), but uses `call_raw_with_access_list` to
183    /// capture the EVM state touched by the multicall, enabling prefetch on the
184    /// next cycle. An empty batch returns an empty result list and a default
185    /// (empty) access list.
186    ///
187    /// # Errors
188    ///
189    /// Returns an error under the same conditions as [`execute`](Self::execute):
190    /// the `aggregate3` call did not succeed (revert, or Multicall3 not deployed),
191    /// or the returned data failed to ABI-decode.
192    #[instrument(skip(self, cache), fields(batch_size = self.calls.len()))]
193    pub fn execute_tracked(
194        &self,
195        cache: &mut EvmCache,
196    ) -> Result<(Vec<IMulticall3::Result>, StorageAccessList)> {
197        if self.calls.is_empty() {
198            return Ok((Vec::new(), StorageAccessList::default()));
199        }
200
201        let call = IMulticall3::aggregate3Call {
202            calls: self.calls.clone(),
203        };
204
205        let (result, access_list) = cache.call_raw_with_access_list(
206            Address::ZERO,
207            MULTICALL3_ADDRESS,
208            call.abi_encode().into(),
209        )?;
210
211        match result {
212            revm::context::result::ExecutionResult::Success { output, .. } => {
213                let out = output.into_data();
214                let results: Vec<IMulticall3::Result> =
215                    IMulticall3::aggregate3Call::abi_decode_returns(&out).map_err(|e| {
216                        MulticallError::Decode {
217                            details: format!("{e:?}"),
218                        }
219                    })?;
220
221                debug!(
222                    results = results.len(),
223                    "multicall batch executed (tracked)"
224                );
225
226                Ok((results, access_list))
227            }
228            other => Err(MulticallError::AggregateFailed {
229                result: format!("{other:?}"),
230            }),
231        }
232    }
233}
234
235impl Default for MulticallBatch {
236    fn default() -> Self {
237        Self::new()
238    }
239}
240
241/// Execute multiple calls in batches using Multicall3.
242///
243/// Splits large call sets into consecutive batches of at most [`MAX_BATCH_SIZE`]
244/// calls, running each via [`MulticallBatch::execute`]. Results are concatenated
245/// in input order. Requires Multicall3 to be deployed at [`MULTICALL3_ADDRESS`]
246/// on the forked chain.
247///
248/// As with a single batch, the all-or-nothing semantics are per-batch: a call
249/// added with `allow_failure = true` that reverts surfaces as `success = false`
250/// in its result, whereas a call with `allow_failure = false` that reverts makes
251/// that batch's `aggregate3` revert (returned here as an `Err`).
252///
253/// # Arguments
254/// * `cache` - The EvmCache to execute calls on
255/// * `calls` - Iterator of (target, calldata, allow_failure) tuples
256///
257/// # Returns
258/// A vector of results in the same order as the input calls.
259///
260/// # Errors
261///
262/// Returns an error as soon as any chunk's [`MulticallBatch::execute`] fails —
263/// i.e. that chunk's `aggregate3` reverted (a `allow_failure = false` call failed,
264/// or Multicall3 is not deployed) or its return data failed to decode. Results
265/// from earlier successful chunks are discarded.
266#[instrument(skip(cache, calls))]
267pub fn execute_batched<I>(cache: &mut EvmCache, calls: I) -> Result<Vec<IMulticall3::Result>>
268where
269    I: IntoIterator<Item = (Address, Bytes, bool)>,
270{
271    let calls: Vec<_> = calls.into_iter().collect();
272    let total = calls.len();
273
274    if total == 0 {
275        return Ok(Vec::new());
276    }
277
278    let mut all_results = Vec::with_capacity(total);
279
280    for chunk in calls.chunks(MAX_BATCH_SIZE) {
281        let mut batch = MulticallBatch::with_capacity(chunk.len());
282        for (target, calldata, allow_failure) in chunk {
283            batch.add(*target, calldata.clone(), *allow_failure);
284        }
285
286        let results = batch.execute(cache)?;
287        all_results.extend(results);
288    }
289
290    debug!(
291        total_calls = total,
292        batches = total.div_ceil(MAX_BATCH_SIZE),
293        "executed batched multicalls"
294    );
295
296    Ok(all_results)
297}
298
299/// Decode a single multicall [`IMulticall3::Result`] into the call's typed return.
300///
301/// # Errors
302///
303/// Returns an error if `result.success` is `false` (the call reverted), or if
304/// `result.returnData` cannot be ABI-decoded into `C::Return`.
305pub fn decode_result<C: SolCall>(result: &IMulticall3::Result) -> Result<C::Return> {
306    if !result.success {
307        return Err(MulticallError::CallFailed);
308    }
309
310    C::abi_decode_returns(&result.returnData).map_err(|e| MulticallError::Decode {
311        details: format!("{e:?}"),
312    })
313}
314
315/// Like [`decode_result`], but returns `None` instead of an `Err` when the call
316/// did not succeed or its return data fails to decode.
317pub fn try_decode_result<C: SolCall>(result: &IMulticall3::Result) -> Option<C::Return> {
318    if !result.success {
319        return None;
320    }
321
322    C::abi_decode_returns(&result.returnData).ok()
323}
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328
329    #[test]
330    fn test_multicall_batch_creation() {
331        let mut batch = MulticallBatch::new();
332        assert!(batch.is_empty());
333        assert_eq!(batch.len(), 0);
334
335        batch.add(Address::ZERO, Bytes::new(), false);
336        assert!(!batch.is_empty());
337        assert_eq!(batch.len(), 1);
338    }
339
340    #[test]
341    fn test_multicall3_address() {
342        // Verify the Multicall3 address is correct
343        let expected: Address = "0xcA11bde05977b3631167028862bE2a173976CA11"
344            .parse()
345            .unwrap();
346        assert_eq!(MULTICALL3_ADDRESS, expected);
347    }
348}