OKRW 프리컴파일 오류 처리하기
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 == 0 | InvalidAmount(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에 복사합니다.
InvalidAddress와 InvalidAmount의 string 인자에 주의합니다. 이전 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으로 디코드
writeContract를 try/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 값을 하드코딩하지 마십시오. 시그니처가
address → string, uint256 → string으로 바뀌면서 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에 모으고, IOkrw는 InvalidAddress / InvalidAmount를 더 이상 재선언하지 않습니다. 호출자 입장에서 감수해야 할 비용은 ABI 조각을 한 번 갱신하는 것입니다. 두 오류 모두 이제 string 페이로드를 가지며, 그에 맞춰 4바이트 selector도 달라졌습니다. 이 가이드의 ABI 조각을 OKRW import 옆에 두면 is IPrecompile인 다른 마루 프리컴파일에도 동일한 디코딩 패턴을 그대로 사용할 수 있습니다.