반응형
Recent Posts
Recent Comments
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

Bbaktaeho

[BlockChain/Ethereum] Contract의 Revert reason 분석하기 (Solidity, Ethers.js, Hardhat) 본문

개발 (Develop)/블록체인 (Blockchain)

[BlockChain/Ethereum] Contract의 Revert reason 분석하기 (Solidity, Ethers.js, Hardhat)

Bbaktaeho 2022. 9. 11. 15:55
반응형

들어가며

Ethereum 네트워크에 배포된 컨트랙트로 트랜잭션을 전송할 때 트랜잭션이 실패하는 경우가 있습니다.

일반적인 Gas, Nonce의 문제가 아닌 컨트랙트에서 작성된 코드(require, error, revert 등)에서 에러가 발생할 수 있는데요. 

이때 error에 대한 이유를 코드에 작성하게 되는데 이를 Revert reason이라고 합니다.

이번 글에서 컨트랙트로 전송한 실패한 트랜잭션이 어떤 이유로 실패했는지 알아보겠습니다.

테스트용 컨트랙트

여러 에러를 작성한 컨트랙트를 구현해서 테스트하겠습니다.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract RevertContract {
    error MyError(address sender, string reason);

    function revCustomError() external {
        revert MyError(msg.sender, "this is customError");
    } 

    function revCustomErrorView() external view {
        revert MyError(msg.sender, "this is customError");
    } 

    function revError() external {
        revert("this is default error");
    }

    function revErrorPure() external pure { 
        revert("this is default error");
    }
}

위 스마트 컨트랙트를 배포해서 

Remix에서 테스트

remix에서 배포 후 모습

먼저 Remix에서 테스트용 로컬 VM에 배포 후 결과를 확인하겠습니다.

revCustomError()

transact to RevertContract.revCustomError errored: VM error: revert.

revert
	The transaction has been reverted to the initial state.
Error provided by the contract:
MyError
Parameters:
{
 "sender": {
  "value": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4"
 },
 "reason": {
  "value": "this is customError"
 }
}
Debug the transaction to get more information.

일정량의 gas가 소비되고 revert 되었습니다.

remix log에서 custom error로 작성한 myError의 revert reason도 정확하게 출력되고 있습니다.

revError()

transact to RevertContract.revError pending ... 
transact to RevertContract.revError errored: VM error: revert.

revert
	The transaction has been reverted to the initial state.
Reason provided by the contract: "this is default error".
Debug the transaction to get more information.

이 함수도 조회성 함수가 아니기 때문에 일정량의 gas가 소비되었고 revert reason도 정확하게 출력되었습니다.

revCustomErrorView()

call to RevertContract.revCustomErrorView
call to RevertContract.revCustomErrorView errored: VM error: revert.

revert
	The transaction has been reverted to the initial state.
Error provided by the contract:
MyError
Parameters:
{
 "sender": {
  "value": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4"
 },
 "reason": {
  "value": "this is customError"
 }
}

조회성 함수라서 gas는 소비되지 않았습니다. 역시 remix에서 revert reason이 잘 출력되고 있네요.

revErrorPure()

call to RevertContract.revErrorPure
call to RevertContract.revErrorPure errored: VM error: revert.

revert
	The transaction has been reverted to the initial state.
Reason provided by the contract: "this is default error".
Debug the transaction to get more information.

view와 마찬가지로 gas 소비 없이 revert reason이 출력되었습니다.

 

Remix IDE에서 revert reason이 잘 출력된 이유는 ABI를 통해 파싱 하기 때문입니다.
다시 말해 ABI를 가지고 코드에서도 revert reason을 알 수 있습니다.

Hardhat에서 테스트

Hardhat 프레임워크를 통해서 Solidity를 로컬 테스트 네트워크에 쉽게 배포하고 테스트할 수 있습니다.

Hardhat의 테스트 기능을 이용하면 자동으로 가상 네트워크 환경을 만들어주고 Solidity를 테스트할 수 있습니다.

 

아래와 같이 테스트 코드를 작성하겠습니다.

import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import { Contract } from "ethers";
import { ethers } from "hardhat";
import { RevertContract } from "../typechain";

describe("revert-contract test", () => {
    let RevertContract;
    let revertContract: Contract | RevertContract;

    let owner: SignerWithAddress;
    let addrs: SignerWithAddress[];

    beforeEach(async () => {
        [owner, ...addrs] = await ethers.getSigners();

        RevertContract = await ethers.getContractFactory("RevertContract", owner);
        revertContract = await RevertContract.deploy();
        revertContract = await revertContract.deployed();
    });

    describe("revert test", () => {
        it("revCustomError", async () => {
            const tx = await revertContract.revCustomError();
            await tx.wait();
        });

        it("revCustomErrorWrite", async () => {
            const data = ethers.BigNumber.from("5");
            const tx = await revertContract.revCustomErrorWrite(data);
            await tx.wait();
        });

        it("revError", async () => {
            const tx = await revertContract.revError();
            await tx.wait();
        });
    });
});
npx hardhat test

cli를 수행하면

1) revert-contract test
       revert test
         revCustomError:
     Error: VM Exception while processing transaction: reverted with custom error 'MyError("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "this is customError")'
      at RevertContract.revCustomError (contracts/RevertContract.sol:10)
      
 2) revert-contract test
       revert test
         revCustomErrorWrite:
     Error: VM Exception while processing transaction: reverted with custom error 'MyError("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "this is customError")'
      at RevertContract.revCustomErrorWrite (contracts/RevertContract.sol:14)
      
 3) revert-contract test
       revert test
         revError:
     Error: VM Exception while processing transaction: reverted with reason string 'this is default error'
      at RevertContract.revError (contracts/RevertContract.sol:23)

역시 Remix처럼 쉽게 확인할 수가 있습니다.

 

지금까지 Remix와 Hardhat Test로 Revert reason을 확인했는데요.

위와 같은 방식은 정말 테스트 환경에 초점을 둔 방식입니다.

저희는 실제 failed된 트랜잭션을 통해서 Revert reason을 확인할 수 있어야 합니다.

transactionHash 값으로 알아보기

실제 네트워크(테스트넷)에 배포 후, 트랜잭션을 전송해서 failed 트랜잭션을 만들어놓습니다.

ethers.js에서 estimate gas error가 발생하지 않도록 raw transaction을 만들어서 전송합니다.

0xa2045bc638c091763683fb7ae8327243891042d4e1fd4eb96cd3f9530d40d3aa

위 transactionHash는 실제 failed Tx 입니다.

ropsten etherscan

transactionHash값을 통해 트랜잭션을 조회하고 다시 이더리움 네트워크로 call하여 당시 결과를 가져옵니다.

해당 값을 ABI를 통해서 파싱합니다.

import { ethers } from "ethers";

async function main() {
  const ethProvider = new ethers.providers.JsonRpcProvider("<RPC_URL>");
  const abi = require("../../artifacts/contracts/RevertContract.sol/RevertContract.json").abi;
  const IRevertContract = new ethers.utils.Interface(abi);

  const txHash = "0xa2045bc638c091763683fb7ae8327243891042d4e1fd4eb96cd3f9530d40d3aa";
  const tx = await ethProvider.getTransaction(txHash);
  const code = await ethProvider.call({
    data: tx.data,
    to: tx.to,
  });

  const decode = IRevertContract.parseError(code);
  console.log(decode);
}

main();

파싱된 값을 로그로 출력하면

ErrorDescription {
  args: [
    '0x92B87857fB2411cFc7291358F6BCBBdd07Dc6A0C',
    'this is customError',
    sender: '0x92B87857fB2411cFc7291358F6BCBBdd07Dc6A0C',
    reason: 'this is customError'
  ],
  errorFragment: {
    type: 'error',
    name: 'MyError',
    inputs: [ [ParamType], [ParamType] ],
    _isFragment: true,
    constructor: [Function: ErrorFragment] {
      from: [Function (anonymous)],
      fromObject: [Function (anonymous)],
      fromString: [Function (anonymous)],
      isErrorFragment: [Function (anonymous)]
    },
    format: [Function (anonymous)]
  },
  name: 'MyError',
  signature: 'MyError(address,string)',
  sighash: '0x1551bbb1'
}

revert reason을 확인할 수 있습니다.

반응형