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}