A modern approach to Solidity transaction/call batching.
There have been a lot of implementations for batching in the past. This repo combines both multicall
and multisend
with modern syntax niceties like abicoder v2
and custom errors.
There is also both "fire and forget" and "fail all if one fails" variations of all methods.
The hardest part is to encode the calldata for each call separately.
With Solidity this can be done with abi.encodeWithSignature
and its siblings abi.encodeCall
and abi.encodeWithSelector
.
Other tools will most likely have an equivalent, like ethers.js
utility Interface#encodeFunctionData
.
Let's say you want to transfer Dai to multiple parties at once:
address dai = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
MultiCaller.Call[] memory calls = new MultiCaller.Call[](3);
calls[0].target = dai;
calls[0].data = abi.encodeWithSignature(
'transferFrom(address from, address to, uint amount)',
0x1234...,
0x2345...,
100_000000000000000000 // 100 Dai
)
calls[1].target = dai;
calls[1].data = abi.encodeWithSignature(
'transferFrom(address from, address to, uint amount)',
0x1234...,
0x1337...,
90_000000000000000000 // 90 Dai
)
calls[2].target = dai;
calls[2].data = abi.encodeWithSignature(
'transferFrom(address from, address to, uint amount)',
0x1234...,
0x2338...,
70_000000000000000000 // 70 Dai
)
If you don't care about the result of the transaction, you can use the "fire and forget" multiSend
:
multiCaller.multiSend(calls);
If any of the Dai transfers fail, it will simply be ignored. Notice that the way ERC-20 approvals work, the from
contract must have approved the multiCaller
instance to spend Dai in its behalf.
ℹ️ The MultiCaller
contract has no storage, which means it is safe to be used as a target for delegatecall
.
If you must make sure that all transactions from the batch are executed, you can use atomicMultisend
:
multiCaller.atomicMultiSend(calls);
If any of the Dai transfers fail, it will revert all transfers.
If you need to send ETH to call a payable
method you need to use the PayableCall
struct to provide one extra param: the value
.
MultiCaller.PayableCall[] memory calls = new MultiCaller.PayableCall[](2);
calls[0].target = 0x9876...;
calls[0].data = abi.encodeWithSignature(
'somethingPayable(address)',
0x1234...
)
calls[0].value = 0.1 ether;
calls[1].target = 0x9876...;
calls[1].data = abi.encodeWithSignature(
'somethingPayable(address)',
0x1337...
)
calls[1].value = 0.3 ether;
Then you can use the overloaded multiSend
and atomicMultisend
methods. Don't forget to provide enough ETH in the call to cover for the total amount:
// 0.1 ether + 0.3 ether = 0.4 ether
multiCaller.atomicMultisend{value: 0.4 ether}(calls);
Suppose you need to read the Dai balance of multiple accounts at once, but you are really short on RPC calls to make. You can optimize it with MultiCaller
.
Prepare the call:
address dai = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
MultiCaller.Call[] memory calls = new MultiCaller.Call[](3);
calls[0].target = dai;
calls[0].data = abi.encodeWithSignature(
'balanceOf(address)',
0x1234...
)
calls[1].target = dai;
calls[1].data = abi.encodeWithSignature(
'balanceOf(address)',
0x1337...
)
calls[2].target = dai;
calls[2].data = abi.encodeWithSignature(
'balanceOf(address)',
0x2338...
)
Then use the multiCall
method:
multiCaller.multiCall(calls);
If your calls might fail, you probably want to use the atomicMultiCall
variant to fail the entire call if one of them fails.