여러개의 트랜잭션을 한번에 전송할 수 있게 도와주는 컨트랙트입니다.
웬만한 네트워크에는 배포되어있는듯 합니다.
신기하게도 모든 네트워크에 배포된 컨트랙트 주소가 동일합니다
https://www.multicall3.com/deployments
아래 링크에서 확인할 수 있습니다.
https://www.multicall3.com/abi#json

callData를 생성
// callData 형식
struct {
target : contractAddress,
callData : contract.interface.encodeFunctionData("methodName", [param1, param2, ...]
}
// contract instance 에서 functionData encoding
const mulltiCallData = [
{
target : contractAddress1,
callData : contract1.interface.encodeFunctionData("methodName1", [param1, param2, ...]
},
{
target : contractAddress2,
callData : contract2.interface.encodeFunctionData("methodName2", [param1, param2, ...]
},
]
// contractInterface instance 에서 functionData encoding
const contractInterface1 = new ethers.Interface(ABI1)
const contractInterface2 = new ethers.Interface(ABI2)
const mulltiCallData = [
{
target : contractAddress1,
callData : contractInterface1.encodeFunctionData("methodName1", [param1, param2, ...]
},
{
target : contractAddress2,
callData : contractInterface2.encodeFunctionData("methodName2", [param1, param2, ...]
},
]
aggregate 함수 호출
// ethers.js v6 기준!
const multicallResultRaw = await multicallContract.aggregate.staticCall(multiCallData)
호출결과를 decoding
/*
aggregate 반환값 형식 = [blockNumber , [ Result1, Result2, Result3, ... ]
*/
// contract instance 에서 functionDataResult decoding
const [ blockNum, results ] = multicallResultRaw
const [ result1, result2 ] = [
contract1.interface.decodeFunctionResult("methodName1",results[0]),
contract2.interface.decodeFunctionResult("methodName2",results[1]),
]
// contractInterface instance 에서 functionDataResult decoding
const [ result1, result2 ] = [
contractInterface1.decodeFunctionResult("methodName1",results[0]),
contractInterface2.decodeFunctionResult("methodName2",results[1]),
]
async function main() {
const rpcUrl = "https://api.avax-test.network/ext/bc/C/rpc";
const provider = new ethers.JsonRpcProvider(rpcUrl);
const multicallContractAddress = "0xcA11bde05977b3631167028862bE2a173976CA11";
const multicallContract = new ethers.Contract(
multicallContractAddress,
multicallAbi,
provider
);
const token0Address = "0x2B4931ab37d33AE909d605476168F90E10C1A9e4";
const token1Address = "0xb13BebA6d96244C38dcbcbd448303351BaF7723c";
const tokenInterface = new ethers.Interface(tokenAbi);
const multiCallData = [
{
target: token0Address,
callData: tokenInterface.encodeFunctionData("name"),
},
{
target: token0Address,
callData: tokenInterface.encodeFunctionData("symbol"),
},
{
target: token1Address,
callData: tokenInterface.encodeFunctionData("name"),
},
{
target: token1Address,
callData: tokenInterface.encodeFunctionData("symbol"),
},
];
// only call once
const [, [token0NameRaw, token0SymbolRaw, token1NameRaw, token1SymbolRaw]] =
await multicallContract.aggregate.staticCall(multiCallData);
const [token0Name] = tokenInterface.decodeFunctionResult(
"name",
token0NameRaw
);
const [token0Symbol] = tokenInterface.decodeFunctionResult(
"symbol",
token0SymbolRaw
);
const [token1Name] = tokenInterface.decodeFunctionResult(
"name",
token1NameRaw
);
const [token1Symbol] = tokenInterface.decodeFunctionResult(
"symbol",
token1SymbolRaw
);
console.log(token0Name);
console.log(token0Symbol);
console.log(token1Name);
console.log(token1Symbol);
}callData를 생성
// callData 형식
struct {
address target;
bool allowFailure; // false 일 경우 해당 callData 호출 실패시, 전체 트랜잭션이 revert됨
bytes callData;
}
// contract instance 에서 functionData encoding
const mulltiCallData = [
{
target : contractAddress1,
allowFailure : true,
callData : contract1.interface.encodeFunctionData("methodName1", [param1, param2, ...]
},
{
target : contractAddress2,
allowFailure : false,
callData : contract2.interface.encodeFunctionData("methodName2", [param1, param2, ...]
},
]
// contractInterface instance 에서 functionData encoding
const contractInterface1 = new ethers.Interface(ABI1)
const contractInterface2 = new ethers.Interface(ABI2)
const mulltiCallData = [
{
target : contractAddress1,
allowFailure : true,
callData : contractInterface1.encodeFunctionData("methodName1", [param1, param2, ...]
},
{
target : contractAddress2,
allowFailure : false,
callData : contractInterface2.encodeFunctionData("methodName2", [param1, param2, ...]
},
]
aggregate3 함수 호출
// ethers.js v6 기준!
const tx = await multicallContract.connect(wallet).aggregate3(multiCallData)
await tx.wait()
const multicallData2 = [
{
target: token0Address,
allowFailure: true,
callData: tokenInterface.encodeFunctionData("faucet", [tokenAmount]),
},
{
target: token0Address,
allowFailure: true,
callData: tokenInterface.encodeFunctionData("transfer", [
wallet.address,
tokenAmount,
]),
},
{
target: token1Address,
allowFailure: true,
callData: tokenInterface.encodeFunctionData("faucet", [tokenAmount]),
},
{
target: token1Address,
allowFailure: true,
callData: tokenInterface.encodeFunctionData("transfer", [
wallet.address,
tokenAmount,
]),
},
];
const tx = await multicallContract.connect(wallet).aggregate3(multicallData2);
await tx.wait();
const token0BalanceAfter = await token0Contract.balanceOf(wallet.address);
const token1BalanceAfter = await token1Contract.balanceOf(wallet.address);
console.log(
`Token0 Balance : ${ethers.formatEther(
token0BalanceBefore
)} -> ${ethers.formatEther(token0BalanceAfter)}`
);
console.log(
`Token1 Balance : ${ethers.formatEther(
token1BalanceBefore
)} -> ${ethers.formatEther(token1BalanceAfter)}`
);💡multicall 을 통해 함수를 실행할 때 주의해야할 점
- msg.sender 가 multicall contract Address 가 되기 때문에 transfer, approve같은 메서드를 호출할 때 메서드 실행주체에 대해 잘 생각해야함.
실행결과 tx 정보 확인 → https://testnet.snowtrace.io/tx/0x3395ec1e1888acc7e91f78f4b4969f6cde93e43d881fc37086f80bc8c2d8a94d/internal?chainid=43113
Multicall Contract 를 통해 tx 를 전송하게 될 경우 구조는 다음과 같습니다.
[ Wallet ] → [ Multicall Contract ] → [ Target Contract ]
따라서 Target Contract 입장에서 tx의 주체, 즉 msg.sender 는 Multicall Contract가 됩니다.
상황 예시
코드를 살펴보면, multicall Contract 가 target Contract로 callData 를 그대로 전송하고 있습니다.


위 사진과 같이 Multicall 을 상속받는 경우는 조금 다른 부분이 있습니다.
위 코드에서 상속받은 Multicall 코드를 살펴보면 다음과 같이 작성돼있습니다.

여기서는 delegatecall 을 사용하여 tx를 처리하고 있음을 확인할 수 있습니다.
delegateCall 은 특정 함수를 호출할 때 msg.sender 와 msg.value를 그대로 유지하여 트랜잭션을 전송하게 됩니다.
예시 상황으로 두 방법에 대해 정리하자면,
상황 : 사람A 가 사람B 에게 중간 매개자(사람C)를 통해 물건을 전송하는 상황.