Shared Precompile Errors — IPrecompile
Every Maroo precompile inherits IPrecompile, which declares the shared typed errors used for input validation, chain-layer authorization, encoding, and transaction-level rejection.
IPrecompile is the base Solidity interface that every Maroo precompile (IOkrw, IPcl, IEas, IAgent, IPrivacy) extends. It declares three families of typed errors that any of those precompiles may revert with: input / encoding errors (InvalidAddress, InvalidAmount, InvalidNumberOfArgs, UnknownMethod, QueryFailed, MsgServerFailed, EventEmitFailed, ABISetupFailed, RequesterIsNotMsgSender, InvalidHeight, InvalidPubkey, InvalidPubkeySize, InvalidPageRequest), stock SDK error mappings (SDKUnauthorized, SDKInsufficientFunds, SDKInvalidAddress, SDKInvalidCoins, SDKInvalidRequest, SDKInvalidType, SDKNotFound, UnmappedCosmosError), and transaction-level rejection errors (InsufficientFee, NonceTooLow, NonceGap, IntrinsicGasTooLow, FloorDataGasTooLow, TipAboveFeeCap, FeeCapTooHigh, TipTooHigh, GasPriceTooLow, GasLimitExceeded, InvalidSender, ChainIdMismatch). Because they are declared once and inherited, an ABI fragment for these errors decodes correctly against any Maroo precompile.
Architecture
flowchart LR
DApp["dApp / Contract caller"]:::evm
Module["Module precompile<br/>(IOkrw / IPcl / IEas / IAgent / IPrivacy)"]:::precompile
Shared["IPrecompile (shared errors)"]:::precompile
Revert["Revert data<br/>typed error OR Error(string)"]:::evm
DApp -->|"call"| Module
Module -.->|"inherits"| Shared
Module -->|"module-specific error<br/>e.g. UnauthorizedMinter"| Revert
Shared -->|"shared error<br/>e.g. SDKInsufficientFunds"| Revert
Revert -->|"decodeErrorResult(combinedAbi, data)"| DApp
classDef evm fill:#0096AA,stroke:#0096AA,color:#fff;
classDef precompile fill:#FF8C50,stroke:#FF8C50,color:#fff; Every module precompile inherits IPrecompile, so a single call can revert with either a module-specific typed error or a shared IPrecompile error. Client decoders should combine both ABIs to name the failure.
The three error families
- Input / encoding — the ABI decode step or a precompile-owned precondition failed.
InvalidAddress(string bad)andInvalidAmount(string amount)carry the offending value as a string, not the rawaddressoruint256— a common ABI mismatch is decoding them with the old numeric shapes. - Stock SDK mappings — the chain-layer rejected the call.
SDKUnauthorized,SDKInsufficientFunds,SDKInvalidAddress,SDKInvalidCoins,SDKInvalidRequest,SDKInvalidType,SDKNotFoundcover the common cases; anything else falls through toUnmappedCosmosError(string codespace, uint32 code)with the raw stock SDK error code. - Transaction-level rejection — the surrounding EVM transaction failed pre-execution validation (fees, nonce, gas, chain id, sender). These are the twelve errors added most recently and are what the PCL-validating RPC mempool now returns from
eth_sendRawTransactionwhen it rejects a transaction before broadcast.
// From precompiles/common/interfaces/IPrecompile.sol
interface IPrecompile {
// --- input / encoding ---
error RequesterIsNotMsgSender(address msgSender, address requester);
error InvalidAddress(string bad);
error InvalidAmount(string amount);
error InvalidHeight(string height);
error InvalidPubkey(string pubkey);
error InvalidPubkeySize(uint256 got, uint256 expected);
error ABISetupFailed(string reason);
error InvalidNumberOfArgs(uint256 expected, uint256 got);
error InvalidPageRequest(string method, uint256 index, string value);
error UnknownMethod(string methodName);
error QueryFailed(string queryMethod, string reason);
error MsgServerFailed(string msgMethod, string reason);
error EventEmitFailed(string eventKind, string reason);
// --- stock SDK mappings ---
error SDKUnauthorized();
error SDKInsufficientFunds();
error SDKInvalidAddress();
error SDKInvalidCoins();
error SDKInvalidRequest();
error SDKInvalidType();
error SDKNotFound();
error UnmappedCosmosError(string codespace, uint32 code);
// --- transaction-level rejection ---
error InsufficientFee();
error NonceTooLow();
error NonceGap();
error IntrinsicGasTooLow();
error FloorDataGasTooLow();
error TipAboveFeeCap();
error FeeCapTooHigh();
error TipTooHigh();
error GasPriceTooLow();
error GasLimitExceeded();
error InvalidSender();
error ChainIdMismatch(uint256 expected, uint256 actual);
} Transaction-level rejection errors
| Error | Meaning |
|---|---|
InsufficientFee | Total fee (gasPrice * gas, or maxFeePerGas-derived) is below the base-fee floor. |
NonceTooLow | The transaction's nonce is lower than the account's next expected nonce. |
NonceGap | The nonce skips past the next expected value — mempool will not queue it. |
IntrinsicGasTooLow | Gas limit is below the intrinsic cost for the calldata payload. |
FloorDataGasTooLow | Calldata-floor gas (post-EIP-7623) not met. |
TipAboveFeeCap | maxPriorityFeePerGas > maxFeePerGas. |
FeeCapTooHigh | maxFeePerGas exceeds the configured ceiling. |
TipTooHigh | maxPriorityFeePerGas exceeds the configured ceiling. |
GasPriceTooLow | Legacy gasPrice is below the current base fee. |
GasLimitExceeded | Gas limit exceeds the block gas limit. |
InvalidSender | The recovered sender is not a valid externally-owned account. |
ChainIdMismatch(expected, actual) | The transaction's chain id does not match the connected network. Use it to prompt a wallet-switch: mainnet is 815, testnet is 450815. |
Because these are inherited from
IPrecompile, the same ABI fragment decodes them from a revert produced by any precompile or from a PCL rejection surfaced through eth_sendRawTransaction (see the enforcement wiring below).PCL rejections at RPC submission are typed too
eth_sendRawTransaction response — instead of surfacing it as an opaque string. The revert payload is the ABI-encoded PCL ReasonCode (from IPcl) or, when the failure is an authorization / encoding condition, one of the shared IPrecompile errors above. Decoding logic on the client can therefore be uniform: attach both the PCL error fragment and the IPrecompile error fragment to the ABI you decode error data against.import { decodeErrorResult } from "viem";
// Include IPcl reason codes AND IPrecompile shared errors in one ABI.
const sharedErrorAbi = [
{ type: "error", name: "InvalidAddress", inputs: [{ name: "bad", type: "string" }] },
{ type: "error", name: "InvalidAmount", inputs: [{ name: "amount", type: "string" }] },
{ type: "error", name: "ChainIdMismatch", inputs: [
{ name: "expected", type: "uint256" },
{ name: "actual", type: "uint256" },
] },
{ type: "error", name: "NonceTooLow", inputs: [] },
{ type: "error", name: "NonceGap", inputs: [] },
{ type: "error", name: "InsufficientFee", inputs: [] },
{ type: "error", name: "IntrinsicGasTooLow", inputs: [] },
{ type: "error", name: "FloorDataGasTooLow", inputs: [] },
{ type: "error", name: "TipAboveFeeCap", inputs: [] },
{ type: "error", name: "FeeCapTooHigh", inputs: [] },
{ type: "error", name: "TipTooHigh", inputs: [] },
{ type: "error", name: "GasPriceTooLow", inputs: [] },
{ type: "error", name: "GasLimitExceeded", inputs: [] },
{ type: "error", name: "InvalidSender", inputs: [] },
{ type: "error", name: "InDenylist", inputs: [{ name: "sender", type: "address" }] },
{ type: "error", name: "EasNoAttestationReceived", inputs: [{ name: "sender", type: "address" }] },
// ...append the rest of IPcl's ReasonCode errors
] as const;
try {
await wallet.sendRawTransaction({ serializedTransaction: signedTx });
} catch (err: any) {
if (err?.data) {
const decoded = decodeErrorResult({ abi: sharedErrorAbi, data: err.data });
console.error(`tx rejected: ${decoded.errorName}`, decoded.args);
} else {
throw err;
}
} Payload shapes to watch
InvalidAddress and InvalidAmount from precompile-owned (address) / (uint256) shapes to shared (string) shapes. Any ABI fragment written before that move will fail to match the 4-byte selector and the revert will surface as an unrecognised error. When upgrading a client:- Replace
InvalidAddress(address)withInvalidAddress(string bad). - Replace
InvalidAmount(uint256)withInvalidAmount(string amount). - Add the twelve transaction-level rejection errors (
InsufficientFee,NonceTooLow,NonceGap,IntrinsicGasTooLow,FloorDataGasTooLow,TipAboveFeeCap,FeeCapTooHigh,TipTooHigh,GasPriceTooLow,GasLimitExceeded,InvalidSender,ChainIdMismatch(uint256, uint256)) so pre-broadcast rejections decode cleanly.
The on-chain selector-based dispatch (Solidity
try/catch reading bytes4(errorSelector)) keeps working for both changes — only the ABI-tail decoders need updating.When you'll see a plain string instead
Error(string)) rather than a typed custom error. That happens when the Go implementation returns an error text with no typed mapping — most commonly for conditions unreachable from normal Solidity, such as "amount must not be nil" on IOkrw.mint when a low-level ABI caller passes a nil big.Int. For those, decodeErrorResult will not match; check for a plain-string revert and read it directly. Do not invent a CamelCase name for such a condition — it is not a typed error and clients cannot decode a name that does not exist.