Multicall을 사용하여 효율적인 contract 호출

개시하다


이번에는 Contract를 효율적으로 호출하는 Multicall과 그 사용 방법을 소개하겠습니다.
Multicall aggregates results from multiple contract constant function calls.
https://github.com/makerdao/multicall
간단하게 어떻게 말했는지 소개하자면 멀티플렉스 자체는 하나의 구조기로서 멀티플렉스에 콜하고 싶은 정보를 전달하고 contract에서 지정한address,function에 호출하여 결과를 종합하여 반환한다.
이것을 이용하면 프레임에서 전단과 Bot을 효과적으로 호출할 수 있을 뿐만 아니라나는 즉시 사용법부터 소개하고 싶다.

시용하다


이번에는 JavaScript 기반 환경을 활용하여 주로 다음과 같은 도구를 사용합니다.
  • ethers.등hereum Blockchain과 연합한 라이브러리
  • Hardhat->ethereum 개발 환경
  • ※ JS를 실시할 수 있는 환경이라면 무엇이든 가능하지만, 네트워크 연결 등이 수월하기 때문에 이번엔 하드하츠의task를 활용합니다.
  • (typescript)
  • 이미지


    멀티콜(그리고Contract 자체)과 협업하여 호출할 수 있는 코드라는 인상을 간단하게 주기 위해 먼저 코드의 이미지로 절차를 설명하고 싶습니다.
    // 1. Multicall Contract と連携するための Contract Instance を生成
    const multicall = new ethers.Contract(
      "address", // multicall contract の address
      ABI, // Application Binary Interface
      ethers.provider // 実際の ethereum network と通信するための Provider
    )
    // 2. Multicall#aggregate を呼び出すための引数を作成
    const inputs = [ ... ]
    // 3. Multicall#aggregate を呼び出し、実行結果を受け取る
    const result = await multicall.callStatic.aggregate(inputs);
    // 4. 3 で取得したデータから指定した function の実行結果を出力
    console.log(result[1][N])
    
    논리의 중심 부분은 상술한 이미지이다.
    여기Multicall#aggregate에서 교부된 매개 변수의 생성을 고려하면 더욱 복잡해질 것이다.
    이번에 Curve.fipool의 3pool을 이용해서 Multicall을 실제로 사용하고 싶어요.
    이 3pool에서 많은 사용자들이 Stable coin, USDC/USDT/DAIdeposit인데 실제로 3pool의Contract는 이런 token을 가지고 있다.
    이 3 pool을 user로 사용하고 multicall을 이용하여 각각의 Token 보유량을 확인해 보세요.
    !
    커브 파이낸스에 대한 자세한 설명은 없지만 일본어로 해설된 글도 있으니 궁금하신 분들은 한번 보세요.
    https://academy.binance.com/ja/articles/what-is-curve-finance-in-defi
    https://jp.cointelegraph.com/news/what-is-curve-finance

    Multicall을 사용하지 않는 경우


    Multicall을 사용하지 않고 호출하려면, 콜 각 영패 구조기 #balanceOf(address) 가 필요합니다.
    이미지는 다음과 같다. ethereum 네트워크를 3차례 호출한다.
    const ERC20_ABI = jsonfile.readFileSync("./abis/ERC20.json")
    
    const TOKENS = {
      USDC: {
        address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
        decimals: 6
      },
      USDT: {
        address: "0xdac17f958d2ee523a2206206994597c13d831ec7",
        decimals: 6
      },
      DAI: {
        address: "0x6b175474e89094c44da98b954eedeac495271d0f",
        decimals: 18
      }
    }
    const USER = "0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7" // Curve.fi: DAI/USDC/USDT Pool
    
    task("call:direct", "call:direct").setAction(async ({}, hre: HardhatRuntimeEnvironment) => {
      const { ethers } = hre
      const usdc = new ethers.Contract(
        TOKENS.USDC.address,
        new ethers.utils.Interface(ERC20_ABI),
        ethers.provider
      )
      const usdt = new ethers.Contract(
        TOKENS.USDT.address,
        new ethers.utils.Interface(ERC20_ABI),
        ethers.provider
      )
      const dai = new ethers.Contract(
        TOKENS.DAI.address,
        new ethers.utils.Interface(ERC20_ABI),
        ethers.provider
      )
      const results = await Promise.all([
        usdc.balanceOf(USER), // 1回
        usdt.balanceOf(USER), // 2回
        dai.balanceOf(USER), // 3回
      ])
      console.log(...)
    })
    
    같은 코드로 세 개의 Contract Instance를 생성하는 것이 좋습니다(자주 있기 때문에)Promise.all 병렬 호출을 사용하여 ethereum network에 RPC request를 3회 실행합니다.
    사용하는 쪽과 부르는 쪽은 모두 효율이 낮으니 멀티콜을 이용해 효율화를 시도해 보자.

    멀티컬로 한번 볼게요.


    갑작스럽지만 완성판 코드는 다음과 같습니다. 개별 댓글에 설명이 추가되었으니 참고하시기 바랍니다.
    import keccak256 from "keccak256"
    import { task } from "hardhat/config"
    import { HardhatRuntimeEnvironment } from "hardhat/types"
    import jsonfile from "jsonfile"
    import { BigNumber } from "ethers"
    import { ERC20__factory, Multicall__factory } from "../../libs/contracts/__generated__"
    
    const multicall_abi = jsonfile.readFileSync("./abis/Multicall.json") // Multicall の Contract のABI (etherscan などで取得可能です)
    const MULTICALL_ADDRESS = "0xeefba1e63905ef1d7acba5a8513c70307c1ce441" // Multicall Contract の address
    
    // 今回利用する token (USDC/USDT/DAI) の address, decimals
    const TOKENS = {
      USDC: {
        address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
        decimals: 6
      },
      USDT: {
        address: "0xdac17f958d2ee523a2206206994597c13d831ec7",
        decimals: 6
      },
      DAI: {
        address: "0x6b175474e89094c44da98b954eedeac495271d0f",
        decimals: 18
      }
    }
    
    // Curve.fi: DAI/USDC/USDT Pool の address
    const ADDRESS = "0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7"
    
    /**
     * use multicall by only ethersjs
     * - support only mainnet
     */
    task("multicall", "multicall").setAction(async ({}, hre: HardhatRuntimeEnvironment) => {
      const { ethers } = hre
    
      const multicall = new ethers.Contract(
        MULTICALL_ADDRESS,
        new ethers.utils.Interface(multicall_abi),
        ethers.provider
      )
    
      // Multicall#aggregate に渡す callData の作成
      const selector = keccak256("balanceOf(address)").toString('hex').substr(0, 8) // #balanceOf(address) の method id 作成
      const param = ADDRESS.substring(2).padStart(64, "0") // 呼び出す function の引数生成: input を zero padding し 32byte のデータに加工する
    
      const inputs = [
        {
          target: TOKENS.USDC.address, // USDC
          callData: `0x${selector}${param}`
        },
        {
          target: TOKENS.USDT.address, // USDT
          callData: `0x${selector}${param}`
        },
        {
          target: TOKENS.DAI.address, // DAI
          callData: `0x${selector}${param}`
        },
      ]
      const result = await multicall.callStatic.aggregate(inputs);
      for (const [index, key] of Object.keys(TOKENS).entries()) {
        const balanceOf = ethers.utils.formatUnits( // 表示向けに、token の decimals 分 shift させる
          BigNumber.from(result[1][index]), // Multicall#aggregate 実行結果から、arguments order を考慮して、該当するデータを取得する
          TOKENS[key as keyof typeof TOKENS].decimals
        )
        console.log(`${key} ... ${balanceOf}`)
      }
    })
    
    상기 코드를 실행하면 다음과 같은 실행 결과를 얻을 수 있으며 각token의 저장량을 확인할 수 있다.
    $ npx hardhat multicall --network mainnet
    USDC ... 1085091186.161086
    USDT ... 822667786.055421
    DAI ... 1252359115.58683885475104325
    

    Self Q&A


    Q. 당신은 무엇을 하고 있습니까?
  • Multicall은 실제로 #call
  • 을 사용합니다.
  • call의 매개 변수는 다음과 같은 조합0x${selector}${param}이 필요합니다.
  • selector ... 호출할 function 지정
  • function의signaturehash화된value의 시작 4byte
  • param ... function에 필요한 매개 변수 부분 호출
  • input를zero padding으로 통합하여 32byte의 데이터로 가공
  • 참고 자료
    나는 스마트 구조기를 사용하는 입금 시스템을 전력으로 이해했다
    Q. 멀티컬은 어디서나 사용할 수 있나요?

  • repository에서 보듯이testnet도deploy로 사용할 수 있다
  • 또한 Paraichain에 대해서도 지원자에 따라 deploy를 진행하여 이용할 수 있는 곳이 많습니다.
  • deploy에 대한 보고가 PR에 있었지만 합병되지 않아 README가 확인할 수 없습니다. PR을 보시면 주소를 알 수 있을 것 같습니다
  • 이렇게 할 수 있기 때문에 자신이 활동하고 싶은 체인에 Multicall이 없으면 deploy에서도 사용할 수 있다
  • Q. Multicall을 쉽게 처리할 수 있는 라이브러리 없음?
  • Multicall.js 그런 말이 있는 것 같아요.
  • Multicall의repository에 링크가 있는데 MakerDao가 공식적으로 제공한 것 같다
  • (나 혼자 안 써봐서 소개 못하겠어. 미안해...)
  • https://github.com/makerdao/multicall.js

    Multicall with Type chain 사용


    멀티컬을 사용하는 방법을 설명했지만 사은품으로 typechain을 사용하면 더 쉽게 불러낼 수 있다는 것을 소개해드리고 싶습니다.
    https://github.com/dethcrypto/TypeChain
    typechain이란 ABI에서Contract와 연합하여 TypeScript 코드를 자동으로 생성하는integration tool입니다.hardhat plugin,truffle plugin 등은 자신의 ethereum 개발 환경,test,deploy script 등을 넣었기 때문에 기술하기 쉬워 개인에게 필수적인 프로젝트이다.
    Typechain을 사용하여 코드를 자동으로 생성할 때는 다음과 같습니다.
    task("multicall-with-typechain", "multicall-with-typechain").setAction(async ({}, hre: HardhatRuntimeEnvironment) => {
      const { ethers } = hre
      const multicall = Multicall__factory.connect(
        MULTICALL_ADDRESS,
        ethers.provider
      )
      const _interface = ERC20__factory.createInterface()
      const callData = [
        {
          target: TOKENS.USDC.address, // USDC
          callData: _interface.encodeFunctionData("balanceOf", [ADDRESS])
        },
        {
          target: TOKENS.USDT.address, // USDT
          callData: _interface.encodeFunctionData("balanceOf", [ADDRESS])
        },
        {
          target: TOKENS.DAI.address, // DAI
          callData: _interface.encodeFunctionData("balanceOf", [ADDRESS])
        },
      ]
      const result = await multicall.callStatic.aggregate(callData)
      for (const [index, key] of Object.keys(TOKENS).entries()) {
        console.log(
          ethers.utils.formatUnits(
            _interface.decodeFunctionResult(
              "balanceOf",
              result.returnData[index]
            )[0],
          TOKENS[key as keyof typeof TOKENS].decimals)
        )
      }
    })
    
    Typechain을 사용하지 않은 모드와 비교
  • Controct Instance(이번으로 말하면Multicall Contract)의 생성
  • Contract function 호출
  • Contract function 호출에 필요한 매개 변수 생성
    이것은 매우 간단하고 다른 프로그램 라이브러리가 필요하지 않다는 것을 모두가 알고 싶다.
  • 특히 멀티컬의 경우 keccak256를 활용해 지정된 byte 수를 재단하기 때문에 그런 부분의 설치를 생략할 수 있어 스텝을 보다 쉽게 처리할 수 있다.

    끝말


    키포인트 토픽이지만 타입 체인을 비롯한 Dapps 개발에서 생산성이 매우 높기 때문에 이번에 소개합니다.
    최대한 필요한 정보만 가능한 한 소개한 만큼 관심 있는 사람이 실제로 접해보면 좋을 것 같다.
    (지난번 업데이트부터 상당한 시간이 흘렀지만) 기술지식 공유로 기사를 낼 수 있어서 다행이다.앞으로도 웹3에 관심 있는 사람들을 위해 뭔가를 할 수 있을 것 같습니다.
    끝까지 읽어주셔서 감사합니다!🙇

    참고 자료


    https://github.com/makerdao/multicall
    https://destiner.io/blog/post/multicall-how-to-make-multiple-ethereum-calls-in-a-single-request/

    좋은 웹페이지 즐겨찾기