use alloy_primitives::{Address, Bytes, address};
use alloy_sol_types::{SolCall, sol};
use tracing::{debug, instrument};
use crate::access_set::StorageAccessList;
use crate::cache::EvmCache;
use crate::errors::{MulticallError, MulticallResult as Result};
pub const MULTICALL3_ADDRESS: Address = address!("cA11bde05977b3631167028862bE2a173976CA11");
pub const MAX_BATCH_SIZE: usize = 200;
sol! {
#[sol(rpc)]
contract IMulticall3 {
struct Call3 {
address target;
bool allowFailure;
bytes callData;
}
struct Result {
bool success;
bytes returnData;
}
function aggregate3(Call3[] calldata calls) external payable returns (Result[] memory returnData);
}
}
pub struct MulticallBatch {
calls: Vec<IMulticall3::Call3>,
}
impl MulticallBatch {
pub fn new() -> Self {
Self { calls: Vec::new() }
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
calls: Vec::with_capacity(capacity),
}
}
pub fn add(&mut self, target: Address, call_data: Bytes, allow_failure: bool) -> &mut Self {
self.calls.push(IMulticall3::Call3 {
target,
allowFailure: allow_failure,
callData: call_data,
});
self
}
pub fn add_call<C: SolCall>(
&mut self,
target: Address,
call: C,
allow_failure: bool,
) -> &mut Self {
self.add(target, call.abi_encode().into(), allow_failure)
}
pub fn len(&self) -> usize {
self.calls.len()
}
pub fn is_empty(&self) -> bool {
self.calls.is_empty()
}
#[instrument(skip(self, cache), fields(batch_size = self.calls.len()))]
pub fn execute(&self, cache: &mut EvmCache) -> Result<Vec<IMulticall3::Result>> {
if self.calls.is_empty() {
return Ok(Vec::new());
}
let call = IMulticall3::aggregate3Call {
calls: self.calls.clone(),
};
let result = cache.call_raw(
Address::ZERO,
MULTICALL3_ADDRESS,
call.abi_encode().into(),
false,
)?;
match result {
revm::context::result::ExecutionResult::Success { output, .. } => {
let out = output.into_data();
let results: Vec<IMulticall3::Result> =
IMulticall3::aggregate3Call::abi_decode_returns(&out).map_err(|e| {
MulticallError::Decode {
details: format!("{e:?}"),
}
})?;
debug!(results = results.len(), "multicall batch executed");
Ok(results)
}
other => Err(MulticallError::AggregateFailed {
result: format!("{other:?}"),
}),
}
}
#[instrument(skip(self, cache), fields(batch_size = self.calls.len()))]
pub fn execute_tracked(
&self,
cache: &mut EvmCache,
) -> Result<(Vec<IMulticall3::Result>, StorageAccessList)> {
if self.calls.is_empty() {
return Ok((Vec::new(), StorageAccessList::default()));
}
let call = IMulticall3::aggregate3Call {
calls: self.calls.clone(),
};
let (result, access_list) = cache.call_raw_with_access_list(
Address::ZERO,
MULTICALL3_ADDRESS,
call.abi_encode().into(),
)?;
match result {
revm::context::result::ExecutionResult::Success { output, .. } => {
let out = output.into_data();
let results: Vec<IMulticall3::Result> =
IMulticall3::aggregate3Call::abi_decode_returns(&out).map_err(|e| {
MulticallError::Decode {
details: format!("{e:?}"),
}
})?;
debug!(
results = results.len(),
"multicall batch executed (tracked)"
);
Ok((results, access_list))
}
other => Err(MulticallError::AggregateFailed {
result: format!("{other:?}"),
}),
}
}
}
impl Default for MulticallBatch {
fn default() -> Self {
Self::new()
}
}
#[instrument(skip(cache, calls))]
pub fn execute_batched<I>(cache: &mut EvmCache, calls: I) -> Result<Vec<IMulticall3::Result>>
where
I: IntoIterator<Item = (Address, Bytes, bool)>,
{
let calls: Vec<_> = calls.into_iter().collect();
let total = calls.len();
if total == 0 {
return Ok(Vec::new());
}
let mut all_results = Vec::with_capacity(total);
for chunk in calls.chunks(MAX_BATCH_SIZE) {
let mut batch = MulticallBatch::with_capacity(chunk.len());
for (target, calldata, allow_failure) in chunk {
batch.add(*target, calldata.clone(), *allow_failure);
}
let results = batch.execute(cache)?;
all_results.extend(results);
}
debug!(
total_calls = total,
batches = total.div_ceil(MAX_BATCH_SIZE),
"executed batched multicalls"
);
Ok(all_results)
}
pub fn decode_result<C: SolCall>(result: &IMulticall3::Result) -> Result<C::Return> {
if !result.success {
return Err(MulticallError::CallFailed);
}
C::abi_decode_returns(&result.returnData).map_err(|e| MulticallError::Decode {
details: format!("{e:?}"),
})
}
pub fn try_decode_result<C: SolCall>(result: &IMulticall3::Result) -> Option<C::Return> {
if !result.success {
return None;
}
C::abi_decode_returns(&result.returnData).ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_multicall_batch_creation() {
let mut batch = MulticallBatch::new();
assert!(batch.is_empty());
assert_eq!(batch.len(), 0);
batch.add(Address::ZERO, Bytes::new(), false);
assert!(!batch.is_empty());
assert_eq!(batch.len(), 1);
}
#[test]
fn test_multicall3_address() {
let expected: Address = "0xcA11bde05977b3631167028862bE2a173976CA11"
.parse()
.unwrap();
assert_eq!(MULTICALL3_ADDRESS, expected);
}
}