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}