OKRW 프리컴파일 오류 처리하기

integration intermediate

OKRW 프리컴파일의 모든 revert 경로를 디코드합니다. 모듈 소유 오류 UnauthorizedMinter, IPrecompile에서 상속된 공유 오류 InvalidAddress / InvalidAmount(현재 string 페이로드), nil 또는 음수 금액에 대한 일반 문자열 revert를 다룹니다.

공유 오류 리팩터링 이후 IOkrw is IPrecompile입니다. 이전에 IOkrw에 있던 두 오류 InvalidAddress(address)InvalidAmount(uint256)는 제거되었고, 이제 IPrecompile에서 InvalidAddress(string bad)InvalidAmount(string amount)로 상속됩니다. 인자 타입이 달라졌으므로 4바이트 selector도 달라졌습니다. 온체인 revert는 여전히 완전한 타입 오류이지만, 이전 형태를 담고 있는 ABI 조각은 클라이언트 측에서 매칭에 실패합니다. 이 가이드는 모든 경로에 대해 동작하는 디코더를 제공합니다.

사전 요구사항

  • Solidity try/catch에 대한 기본 지식.
  • JavaScript try/catch와 Ethers.js에 익숙할 것.

세 가지 revert 형태

mint 호출은 정확히 다음 세 가지 방식 중 하나로 실패합니다.

트리거Revert 형태소유자
msg.sender ≠ 지정된 발행자UnauthorizedMinter(address caller, address authorizedMinter)IOkrw (모듈)
recipient == 0x0 또는 인코딩 불가InvalidAddress(string bad)IPrecompile (공유)
amount == 0InvalidAmount(string amount)IPrecompile (공유)
amount가 nil 또는 음수 (ABI 저수준 조작으로만)일반 문자열 revert("amount must not be nil" / "amount must not be negative")타입 오류 아님

상속된 모든 IPrecompile 오류(UnknownMethod, InvalidNumberOfArgs, SDKUnauthorized, EventEmitFailed 등)도 내부/경계 장애 시 노출될 수 있으며 동일한 방식으로 디코드합니다.
// IOkrw의 전체 오류 표면 — 모듈 소유 + 상속.
import {IPrecompile} from "@maroo-chain/contracts/precompiles/common/interfaces/IPrecompile.sol";

interface IOkrw is IPrecompile {
    error UnauthorizedMinter(address caller, address authorizedMinter);
    // InvalidAddress(string), InvalidAmount(string) 및 그 외 공유 오류는
    // IPrecompile에서 옵니다. IOkrw에 다시 선언하지 마십시오.
}

필요한 클라이언트 ABI 조각

이 ABI를 dApp에 복사합니다. InvalidAddressInvalidAmountstring 인자에 주의합니다. 이전 address / uint256 타입을 그대로 두면 실제 revert가 발생했을 때 decodeErrorResult(ethers의 interface.parseError)가 조용히 undefined를 반환합니다.
// 리팩터링 이후 ABI입니다. OKRW.mint ABI 옆에 두십시오.
export const okrwErrorsAbi = [
  { type: "error", name: "UnauthorizedMinter",
    inputs: [
      { name: "caller",            type: "address" },
      { name: "authorizedMinter",  type: "address" },
    ] },
  { type: "error", name: "InvalidAddress", inputs: [{ name: "bad",    type: "string" }] },
  { type: "error", name: "InvalidAmount",  inputs: [{ name: "amount", type: "string" }] },
  // 내부 장애 시 노출될 수 있는 상속 오류입니다.
  { type: "error", name: "UnknownMethod",       inputs: [{ name: "methodName", type: "string" }] },
  { type: "error", name: "InvalidNumberOfArgs", inputs: [
      { name: "expected", type: "uint256" },
      { name: "got",      type: "uint256" },
    ] },
  { type: "error", name: "SDKUnauthorized",     inputs: [] },
  { type: "error", name: "EventEmitFailed", inputs: [
      { name: "eventKind", type: "string" },
      { name: "reason",    type: "string" },
    ] },
] as const;

viem으로 디코드

writeContracttry/catch로 감싸고, 위 ABI로 revert 데이터를 decodeErrorResult에 넘깁니다. errorName으로 분기하는 부분에 주목합니다. UnauthorizedMinter는 실패한 호출자와 현재 지정된 발행자를 함께 제공하므로, 사용자에게 정확한 메시지를 보여줄 수 있습니다.
import { createWalletClient, http, parseEther, decodeErrorResult } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { okrwErrorsAbi } from "./okrwErrors";

const OKRW = "0x1000000000000000000000000000000000000001" as const;
const mintAbi = [{
  type: "function", name: "mint", stateMutability: "nonpayable",
  inputs: [{ name: "recipient", type: "address" }, { name: "amount", type: "uint256" }],
  outputs: [{ type: "bool" }],
}] as const;

const wallet = createWalletClient({
  account: privateKeyToAccount(process.env.MINTER_KEY as `0x${string}`),
  transport: http("https://rpc-testnet.maroo.io"),
});

try {
  await wallet.writeContract({
    address: OKRW,
    abi: mintAbi,
    functionName: "mint",
    // TODO: 프로덕션 배포 전 실제 수신자 주소로 교체하십시오.
    args: ["0x8F3ac2B1d9E74c05A6B18FE27Dc4913e5A0F7b62", parseEther("10000000")],
  });
} catch (err: any) {
  if (!err?.data) {
    // 일반 문자열 revert(예: nil / 음수 금액) — err.shortMessage를 확인합니다.
    throw err;
  }
  const decoded = decodeErrorResult({ abi: [...okrwErrorsAbi, ...mintAbi], data: err.data });
  switch (decoded.errorName) {
    case "UnauthorizedMinter": {
      const [caller, authorizedMinter] = decoded.args as [string, string];
      console.error(`발행자 아님. caller=${caller}, expected=${authorizedMinter}`);
      break;
    }
    case "InvalidAddress": {
      const [bad] = decoded.args as [string];
      console.error(`OKRW.mint가 수신자 ${bad}를 거절했습니다`);
      break;
    }
    case "InvalidAmount": {
      const [amount] = decoded.args as [string];
      console.error(`OKRW.mint가 금액 ${amount}을 거절했습니다`);
      break;
    }
    default:
      console.error(`OKRW.mint revert: ${decoded.errorName}`, decoded.args);
  }
}

Solidity에서 디코드

온체인 오류 라우팅에서는 저수준 bytes를 잡아 4바이트 selector로 분기합니다. InvalidAddress / InvalidAmount가 이제 string 인자를 받으므로 selector가 달라졌습니다. selector는 항상 IPrecompile.InvalidAddress.selector(상속된 정의에 접근)를 통해 얻고, 이전 버전에서 복사한 16진수 값을 하드코딩하지 않습니다.
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.28;

import "@maroo-chain/contracts/precompiles/okrw/IOkrw.sol";
import {IPrecompile} from "@maroo-chain/contracts/precompiles/common/interfaces/IPrecompile.sol";

contract SafeMinter {
    IOkrw constant okrw = IOkrw(0x1000000000000000000000000000000000000001);

    event MintRejected(bytes4 selector);

    function safeMint(address to, uint256 amount) external {
        try okrw.mint(to, amount) returns (bool ok) {
            require(ok, "mint returned false");
        } catch (bytes memory raw) {
            bytes4 sel;
            assembly { sel := mload(add(raw, 32)) }

            if (sel == IOkrw.UnauthorizedMinter.selector) {
                emit MintRejected(sel);
            } else if (sel == IPrecompile.InvalidAddress.selector) {
                // InvalidAddress(string) — 리팩터링 이후 selector가 바뀌었습니다.
                emit MintRejected(sel);
            } else if (sel == IPrecompile.InvalidAmount.selector) {
                // InvalidAmount(string) — 리팩터링 이후 selector가 바뀌었습니다.
                emit MintRejected(sel);
            } else {
                // 알 수 없는 revert — 상위로 전파합니다(일반 문자열 revert는 여기로 옵니다).
                assembly { revert(add(raw, 32), mload(raw)) }
            }
        }
    }
}
주의: 리팩터링 이전의 InvalidAddress / InvalidAmount selector 값을 하드코딩하지 마십시오. 시그니처가 addressstring, uint256string으로 바뀌면서 4바이트 selector도 함께 바뀌었습니다. 항상 현재 인터페이스 import를 통해 IPrecompile.InvalidAddress.selector / IPrecompile.InvalidAmount.selector로 참조합니다.

cast로 확인하기

배포된 프리컴파일의 현재 타입 오류 selector를 cast로 확인합니다. 디코딩 문제가 발생하면 대부분 ABI 조각이 리팩터링 이전 형태에 머물러 있는 경우입니다.
# selector 확인 — 코드에 16진수 상수를 두었다면 다시 생성합니다.
cast sig-error 'UnauthorizedMinter(address,address)'
cast sig-error 'InvalidAddress(string)'
cast sig-error 'InvalidAmount(string)'

# 테스트넷에 0 금액 mint를 시뮬레이션하고 revert 데이터를 확인합니다.
MAROO_RPC=https://rpc-testnet.maroo.io
cast call 0x1000000000000000000000000000000000000001 \
  'mint(address,uint256)' \
  0x8F3ac2B1d9E74c05A6B18FE27Dc4913e5A0F7b62 0 \
  --rpc-url $MAROO_RPC

마무리

공유 오류 리팩터링은 잘못된 입력과 경계 오류를 IPrecompile에 모으고, IOkrwInvalidAddress / InvalidAmount를 더 이상 재선언하지 않습니다. 호출자 입장에서 감수해야 할 비용은 ABI 조각을 한 번 갱신하는 것입니다. 두 오류 모두 이제 string 페이로드를 가지며, 그에 맞춰 4바이트 selector도 달라졌습니다. 이 가이드의 ABI 조각을 OKRW import 옆에 두면 is IPrecompile인 다른 마루 프리컴파일에도 동일한 디코딩 패턴을 그대로 사용할 수 있습니다.
ESC
검색어를 입력하세요