MulticallLibraryV2
The MulticallLibraryV2 provides efficient batch execution of multiple function calls in a single transaction, optimized for gas efficiency and error handling.
Overview
MulticallLibraryV2 enables developers to execute multiple contract calls atomically, reducing gas costs and improving user experience by bundling operations together.
Library Interface
library MulticallLibraryV2 {
struct Call {
address target;
bytes callData;
uint256 gasLimit;
}
struct CallWithValue {
address target;
bytes callData;
uint256 value;
uint256 gasLimit;
}
struct Result {
bool success;
bytes returnData;
uint256 gasUsed;
}
function multicall(Call[] memory calls) external returns (Result[] memory results);
function multicallWithValue(CallWithValue[] memory calls) external payable returns (Result[] memory results);
function tryMulticall(Call[] memory calls) external returns (Result[] memory results);
function aggregate(Call[] memory calls) external returns (uint256 blockNumber, bytes[] memory returnData);
}
Core Functions
multicall
Executes multiple calls and reverts if any call fails.
function multicall(Call[] memory calls) external returns (Result[] memory results)
Parameters:
calls
: Array of Call structs containing target addresses and call data
Returns:
results
: Array of Result structs with success status, return data, and gas used
Example:
MulticallLibraryV2.Call[] memory calls = new MulticallLibraryV2.Call[](2);
calls[0] = MulticallLibraryV2.Call({
target: tokenAddress,
callData: abi.encodeWithSignature("transfer(address,uint256)", recipient, amount),
gasLimit: 100000
});
calls[1] = MulticallLibraryV2.Call({
target: stakingAddress,
callData: abi.encodeWithSignature("stake(uint256)", amount),
gasLimit: 150000
});
MulticallLibraryV2.Result[] memory results = MulticallLibraryV2.multicall(calls);
multicallWithValue
Executes multiple calls with ETH value, reverting if any call fails.
function multicallWithValue(CallWithValue[] memory calls) external payable returns (Result[] memory results)
Parameters:
calls
: Array of CallWithValue structs including ETH value for each call
Returns:
results
: Array of Result structs with execution details
Example:
MulticallLibraryV2.CallWithValue[] memory calls = new MulticallLibraryV2.CallWithValue[](2);
calls[0] = MulticallLibraryV2.CallWithValue({
target: wethAddress,
callData: abi.encodeWithSignature("deposit()"),
value: 1 ether,
gasLimit: 50000
});
calls[1] = MulticallLibraryV2.CallWithValue({
target: dexAddress,
callData: abi.encodeWithSignature("swap(address,address,uint256)", tokenA, tokenB, amount),
value: 0,
gasLimit: 200000
});
MulticallLibraryV2.Result[] memory results = MulticallLibraryV2.multicallWithValue{value: 1 ether}(calls);
tryMulticall
Executes multiple calls without reverting on individual call failures.
function tryMulticall(Call[] memory calls) external returns (Result[] memory results)
Parameters:
calls
: Array of Call structs to execute
Returns:
results
: Array of Result structs, including failed calls
Example:
MulticallLibraryV2.Call[] memory calls = new MulticallLibraryV2.Call[](3);
// Some calls might fail, but execution continues
calls[0] = MulticallLibraryV2.Call({
target: tokenA,
callData: abi.encodeWithSignature("transfer(address,uint256)", recipient, amount1),
gasLimit: 100000
});
calls[1] = MulticallLibraryV2.Call({
target: tokenB,
callData: abi.encodeWithSignature("transfer(address,uint256)", recipient, amount2),
gasLimit: 100000
});
calls[2] = MulticallLibraryV2.Call({
target: invalidAddress, // This might fail
callData: abi.encodeWithSignature("someFunction()"),
gasLimit: 50000
});
MulticallLibraryV2.Result[] memory results = MulticallLibraryV2.tryMulticall(calls);
// Check individual results
for (uint i = 0; i < results.length; i++) {
if (results[i].success) {
// Handle successful call
} else {
// Handle failed call
string memory error = abi.decode(results[i].returnData, (string));
}
}
aggregate
Legacy function for basic multicall aggregation.
function aggregate(Call[] memory calls) external returns (uint256 blockNumber, bytes[] memory returnData)
Parameters:
calls
: Array of Call structs to execute
Returns:
blockNumber
: Current block numberreturnData
: Array of return data from each call
Advanced Usage Patterns
1. DeFi Batch Operations
contract DeFiBatchOperations {
using MulticallLibraryV2 for MulticallLibraryV2.Call[];
function swapAndStake(
address tokenIn,
address tokenOut,
uint256 amountIn,
address dexRouter,
address stakingContract
) external {
MulticallLibraryV2.Call[] memory calls = new MulticallLibraryV2.Call[](3);
// 1. Approve token for DEX
calls[0] = MulticallLibraryV2.Call({
target: tokenIn,
callData: abi.encodeWithSignature("approve(address,uint256)", dexRouter, amountIn),
gasLimit: 50000
});
// 2. Swap tokens
calls[1] = MulticallLibraryV2.Call({
target: dexRouter,
callData: abi.encodeWithSignature(
"swapExactTokensForTokens(uint256,uint256,address[],address,uint256)",
amountIn,
0, // Accept any amount of tokens out
getPath(tokenIn, tokenOut),
address(this),
block.timestamp + 300
),
gasLimit: 200000
});
// 3. Stake received tokens
calls[2] = MulticallLibraryV2.Call({
target: stakingContract,
callData: abi.encodeWithSignature("stakeAll()"),
gasLimit: 150000
});
MulticallLibraryV2.Result[] memory results = MulticallLibraryV2.multicall(calls);
// Verify all operations succeeded
for (uint i = 0; i < results.length; i++) {
require(results[i].success, "Batch operation failed");
}
}
}
2. NFT Batch Minting
contract NFTBatchMinter {
function batchMint(
address nftContract,
address[] memory recipients,
uint256[] memory tokenIds
) external {
require(recipients.length == tokenIds.length, "Array length mismatch");
MulticallLibraryV2.Call[] memory calls = new MulticallLibraryV2.Call[](recipients.length);
for (uint i = 0; i < recipients.length; i++) {
calls[i] = MulticallLibraryV2.Call({
target: nftContract,
callData: abi.encodeWithSignature(
"mint(address,uint256)",
recipients[i],
tokenIds[i]
),
gasLimit: 100000
});
}
MulticallLibraryV2.Result[] memory results = MulticallLibraryV2.multicall(calls);
uint256 successCount = 0;
for (uint i = 0; i < results.length; i++) {
if (results[i].success) {
successCount++;
}
}
emit BatchMintCompleted(successCount, recipients.length);
}
}
3. Cross-Chain Batch Operations
contract CrossChainBatchOps {
IIXFIGateway public gateway;
function batchCrossChainCalls(
string[] memory destinationChains,
address[] memory targetContracts,
bytes[] memory payloads
) external {
require(
destinationChains.length == targetContracts.length &&
targetContracts.length == payloads.length,
"Array length mismatch"
);
MulticallLibraryV2.Call[] memory calls = new MulticallLibraryV2.Call[](destinationChains.length);
for (uint i = 0; i < destinationChains.length; i++) {
calls[i] = MulticallLibraryV2.Call({
target: address(gateway),
callData: abi.encodeWithSignature(
"callContract(string,string,bytes)",
destinationChains[i],
Strings.toHexString(uint160(targetContracts[i]), 20),
payloads[i]
),
gasLimit: 200000
});
}
MulticallLibraryV2.Result[] memory results = MulticallLibraryV2.tryMulticall(calls);
emit BatchCrossChainCompleted(results);
}
}
Gas Optimization Features
Dynamic Gas Limit Adjustment
library GasOptimizedMulticall {
function optimizedMulticall(
MulticallLibraryV2.Call[] memory calls,
uint256 baseGasPerCall
) external returns (MulticallLibraryV2.Result[] memory results) {
results = new MulticallLibraryV2.Result[](calls.length);
for (uint256 i = 0; i < calls.length; i++) {
uint256 gasStart = gasleft();
// Adjust gas limit based on remaining gas
uint256 adjustedGasLimit = calls[i].gasLimit;
if (gasStart < calls[i].gasLimit + baseGasPerCall) {
adjustedGasLimit = gasStart - baseGasPerCall;
}
(bool success, bytes memory returnData) = calls[i].target.call{
gas: adjustedGasLimit
}(calls[i].callData);
uint256 gasUsed = gasStart - gasleft();
results[i] = MulticallLibraryV2.Result({
success: success,
returnData: returnData,
gasUsed: gasUsed
});
if (!success && calls[i].gasLimit == adjustedGasLimit) {
// If call failed and we used full gas limit, revert
revert("Multicall failed");
}
}
return results;
}
}
Gas Estimation
contract MulticallGasEstimator {
function estimateMulticallGas(
MulticallLibraryV2.Call[] memory calls
) external returns (uint256[] memory gasEstimates) {
gasEstimates = new uint256[](calls.length);
for (uint256 i = 0; i < calls.length; i++) {
try this.estimateSingleCall(calls[i]) returns (uint256 estimate) {
gasEstimates[i] = estimate;
} catch {
gasEstimates[i] = calls[i].gasLimit; // Fallback to provided limit
}
}
return gasEstimates;
}
function estimateSingleCall(
MulticallLibraryV2.Call memory call
) external returns (uint256) {
return ITargetContract(call.target).estimateGas(call.callData);
}
}
Error Handling
Custom Error Types
library MulticallErrors {
error CallFailed(uint256 callIndex, address target, bytes callData, bytes reason);
error InsufficientGas(uint256 callIndex, uint256 required, uint256 available);
error InvalidCallData(uint256 callIndex, bytes callData);
error TargetNotContract(uint256 callIndex, address target);
}
Enhanced Error Reporting
contract EnhancedMulticall {
using MulticallLibraryV2 for MulticallLibraryV2.Call[];
function safeMulticall(
MulticallLibraryV2.Call[] memory calls
) external returns (MulticallLibraryV2.Result[] memory results) {
results = new MulticallLibraryV2.Result[](calls.length);
for (uint256 i = 0; i < calls.length; i++) {
// Validate target is a contract
if (calls[i].target.code.length == 0) {
revert MulticallErrors.TargetNotContract(i, calls[i].target);
}
// Check gas availability
if (gasleft() < calls[i].gasLimit + 5000) {
revert MulticallErrors.InsufficientGas(i, calls[i].gasLimit, gasleft());
}
uint256 gasStart = gasleft();
(bool success, bytes memory returnData) = calls[i].target.call{
gas: calls[i].gasLimit
}(calls[i].callData);
uint256 gasUsed = gasStart - gasleft();
results[i] = MulticallLibraryV2.Result({
success: success,
returnData: returnData,
gasUsed: gasUsed
});
if (!success) {
revert MulticallErrors.CallFailed(i, calls[i].target, calls[i].callData, returnData);
}
}
return results;
}
}
Security Considerations
Reentrancy Protection
contract ReentrancyProtectedMulticall {
bool private locked;
modifier nonReentrant() {
require(!locked, "Reentrant call");
locked = true;
_;
locked = false;
}
function secureMulticall(
MulticallLibraryV2.Call[] memory calls
) external nonReentrant returns (MulticallLibraryV2.Result[] memory) {
return MulticallLibraryV2.multicall(calls);
}
}
Access Control
contract AccessControlledMulticall {
mapping(address => bool) public authorized;
mapping(address => bool) public allowedTargets;
modifier onlyAuthorized() {
require(authorized[msg.sender], "Not authorized");
_;
}
function authorizedMulticall(
MulticallLibraryV2.Call[] memory calls
) external onlyAuthorized returns (MulticallLibraryV2.Result[] memory) {
// Validate all targets are allowed
for (uint256 i = 0; i < calls.length; i++) {
require(allowedTargets[calls[i].target], "Target not allowed");
}
return MulticallLibraryV2.multicall(calls);
}
}
Integration Examples
Frontend JavaScript
class MulticallManager {
constructor(contractAddress, provider) {
this.contract = new ethers.Contract(contractAddress, multicallABI, provider);
}
async prepareCalls(operations) {
const calls = [];
for (const op of operations) {
const target = op.contract.address || op.contract.target;
const callData = op.contract.interface.encodeFunctionData(op.method, op.args);
calls.push({
target: target,
callData: callData,
gasLimit: op.gasLimit || 200000
});
}
return calls;
}
async executeMulticall(calls, options = {}) {
try {
const tx = await this.contract.multicall(calls, {
gasLimit: options.gasLimit || 1000000,
gasPrice: options.gasPrice
});
const receipt = await tx.wait();
return this.parseResults(receipt);
} catch (error) {
console.error('Multicall failed:', error);
throw error;
}
}
async estimateGas(calls) {
try {
return await this.contract.estimateGas.multicall(calls);
} catch (error) {
console.error('Gas estimation failed:', error);
return BigInt(1000000); // Fallback
}
}
parseResults(receipt) {
const results = [];
// Parse MulticallExecuted events
const events = receipt.logs.filter(log =>
log.topics[0] === ethers.id("MulticallExecuted(uint256,bool,bytes)")
);
for (const event of events) {
const decoded = ethers.AbiCoder.defaultAbiCoder().decode(
['uint256', 'bool', 'bytes'],
event.data
);
results.push({
callIndex: decoded[0],
success: decoded[1],
returnData: decoded[2]
});
}
return results;
}
}
React Hook
import { useState, useCallback } from 'react';
import { useContract, useSigner } from 'wagmi';
export function useMulticall(contractAddress) {
const { data: signer } = useSigner();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const multicallContract = useContract({
address: contractAddress,
abi: multicallABI,
signerOrProvider: signer
});
const executeBatch = useCallback(async (operations) => {
if (!multicallContract) return;
setIsLoading(true);
setError(null);
try {
const manager = new MulticallManager(contractAddress, signer.provider);
const calls = await manager.prepareCalls(operations);
const gasEstimate = await manager.estimateGas(calls);
const results = await manager.executeMulticall(calls, {
gasLimit: gasEstimate * 110n / 100n // 10% buffer
});
return results;
} catch (err) {
setError(err.message);
throw err;
} finally {
setIsLoading(false);
}
}, [multicallContract, contractAddress, signer]);
return {
executeBatch,
isLoading,
error
};
}
Performance Metrics
Token Transfers
60-80%
40-60% faster
50-100 calls
NFT Operations
50-70%
30-50% faster
20-50 calls
DeFi Interactions
40-60%
50-70% faster
10-30 calls
Cross-Chain Calls
30-50%
60-80% faster
5-20 calls
Best Practices
1. Gas Optimization
Set appropriate gas limits for each call
Use
tryMulticall
for non-critical operationsBatch similar operations together
Monitor gas usage patterns
2. Error Handling
Always check individual call results
Implement proper fallback mechanisms
Use descriptive error messages
Log failed operations for debugging
3. Security
Validate all target addresses
Implement access controls where needed
Use reentrancy protection
Audit multicall implementations
4. User Experience
Provide clear transaction previews
Show individual operation status
Implement retry mechanisms
Optimize for mobile gas limits
Resources
Last updated