Multicall Contract

Natreeum's Blog·2024년 11월 17일

Blockchain

목록 보기
1/6

MultiCall 이란?

여러개의 트랜잭션을 한번에 전송할 수 있게 도와주는 컨트랙트입니다.

장점

  • 여러개의 트랜잭션을 한번에 묶어서 보내기 때문에 각각의 트랜잭션을 전송하는것 보다 가스비가 저렴할 수 있습니다.
  • 트랜잭션에 대해 서명을 한 번만 하면 되기 때문에, UX향상을 기대할 수 있습니다.

Multicall 컨트랙트 주소

웬만한 네트워크에는 배포되어있는듯 합니다.
신기하게도 모든 네트워크에 배포된 컨트랙트 주소가 동일합니다

https://www.multicall3.com/deployments

Multicall Abi

아래 링크에서 확인할 수 있습니다.

https://www.multicall3.com/abi#json

사용법

  • 여러개의 contract 에서 read 함수를 호출하여 결과를 종합하는 경우
  • 여러개의 상태가 변경되는 함수를 호출하는 경우

🍏여러개의 contract 에서 read 함수를 호출하여 결과를 종합하는 경우

  1. 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, ...]
    	},
    ]
  2. aggregate 함수 호출

    // ethers.js v6 기준!
    
    const multicallResultRaw = await multicallContract.aggregate.staticCall(multiCallData)
  3. 호출결과를 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);
    }

🍏여러개의 contract 에서 state-changing 함수를 호출하여 결과를 종합하는 경우

  1. 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, ...]
    	},
    ]
  2. 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 와 import 된 Multicall 차이점

Multicall Contract

Multicall Contract 를 통해 tx 를 전송하게 될 경우 구조는 다음과 같습니다.

[ Wallet ] → [ Multicall Contract ] → [ Target Contract ]

따라서 Target Contract 입장에서 tx의 주체, 즉 msg.sender 는 Multicall Contract가 됩니다.

상황 예시

  • 내 지갑(W0)에 90 TokenA 가 있다.
  • (W1, W2, W3) 에 30 TokenA 씩 전송하려고 한다.
  • ( token.transfer ) 트랜잭션 3개를 multicall로 전송한다.
  • multicall은 3개의 트랜잭션을 TokenA 컨트랙트로 전송한다.
  • 이때, transfer의 호출 주체(msg.sender) 는 multicall contract 이고, multicall contract 는 tokenA 잔액이 0 이기 때문에 모든 트랜잭션이 revert 된다.

코드를 살펴보면, multicall Contract 가 target Contract로 callData 를 그대로 전송하고 있습니다.

import 된 Multicall

위 사진과 같이 Multicall 을 상속받는 경우는 조금 다른 부분이 있습니다.

위 코드에서 상속받은 Multicall 코드를 살펴보면 다음과 같이 작성돼있습니다.

여기서는 delegatecall 을 사용하여 tx를 처리하고 있음을 확인할 수 있습니다.

delegateCall 은 특정 함수를 호출할 때 msg.sender 와 msg.value를 그대로 유지하여 트랜잭션을 전송하게 됩니다.

상황 비유

예시 상황으로 두 방법에 대해 정리하자면,

상황 : 사람A 가 사람B 에게 중간 매개자(사람C)를 통해 물건을 전송하는 상황.

  • Multicall Contract를 사용한다면
    1. 사람A 가 사람C에게 물건을 전달한다. 물건에는 [보낸 사람 : 사람A] 라고 적혀있다.
    2. 사람C 가 사람B에게 물건을 전달한다. 물건에는 [보낸 사람 : 사람C] 라고 적혀있다.
  • import된 Multicall 을 사용한다면
    1. 사람A 가 사람C에게 물건을 전달한다. 물건에는 [보낸 사람 : 사람A] 라고 적혀있다.
    2. 사람C 가 사람B에게 물건을 전달한다. 물건에는 [보낸 사람 : 사람A] 라고 적혀있다.

정리

  • Multicall Contract
    • "한번에" 여러개의 target Contract로, 여러개의 트랜잭션을 보낼 수 있는것
    • 트랜잭션의 주체가 multicall Contract
  • import된 Multicall
    • 한번에 여러개의 트랜잭션을 보낼 수 있지만, target Contract는 해당 컨트랙트로만 한정
    • 트랜잭션의 주체가 wallet(caller)
profile
BlockChain DEV

0개의 댓글