> ## Documentation Index
> Fetch the complete documentation index at: https://docs.metalntwx.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Error Codes

> Transaction admission failures and contract revert reasons across Metal protocol surfaces

Use this page to identify a rejected transaction or decode a contract revert.
Admission errors are JSON-RPC messages returned before EVM execution. Contract
errors appear in revert data and failed transaction traces.

## Transaction admission

These messages come from `eth_sendRawTransaction` before the transaction reaches
the EVM.

| Error | Cause | Corrective action |
| - | - | - |
| `unsupported Ethereum envelope type` | Envelope is not replay-protected legacy (`0x00`), EIP-2930 (`0x01`), EIP-1559 (`0x02`), standard blob (`0x03`), or Metal (`0x7f`) | Use an accepted envelope. Carry EIP-7702 authorizations inside `0x7f` |
| `transaction has no chain id` | Legacy transaction lacks EIP-155 replay protection | Sign the legacy transaction with the Metal chain ID or use EIP-1559 |
| `value must be zero` | `value != 0` in the envelope | Set `value = 0` |
| `gas_token ... not in MIP20 allowlist` | The selected gas token is absent from `MIP20Factory.isMIP20` | Use a MIP20 token. Standard Ethereum envelopes use the default gas token |
| `insufficient gas-token balance` | The signing account's balance in the gas token cannot cover the maximum gas cost, any blob fee, and a recognized same-token transfer debit | Fund the signing account in the selected or default gas token (the [faucet](https://docs.metalntwx.com/build/prepare/fund-a-test-account.md) dispenses `*USDZ`) |
| `max_fee_per_gas ... exceeds cap` | Fee cap is greater than `1_000_000_000_000` base units | Lower `max_fee_per_gas` to the chain cap or below |
| `effective gas price ... below minimum of 1` | `min(max_fee_per_gas, max_priority_fee_per_gas)` is zero | Set both fee fields to at least `1` |
| `nonce too low` | A previous transaction with this nonce was already included | Reconcile the original transaction, then read `getTransactionCount(sender, "pending")` before preparing a new attempt |

### Blob admission

These failures apply to standard `0x03` transactions. Transaction pool
validation messages can include additional details about the sidecar or failed
KZG check.

| Error or condition | Cause | Corrective action |
| - | - | - |
| `blob transaction must be submitted in EIP-7594 network form with its sidecar` | A consensus-form transaction was submitted without its sidecar | Submit the complete pooled network encoding |
| `max_fee_per_blob_gas must be at least 1` | Blob-fee bid is zero | Set a positive `maxFeePerBlobGas` |
| `max_fee_per_blob_gas ... exceeds cap` | Blob-fee bid exceeds `1_000_000_000_000` base units | Lower the bid to the cap or below |
| `blob transactions are not accepted via observer ingress; submit to a validator` | An observer cannot forward the blob sidecar | Use a validator RPC endpoint |
| Missing, empty, or legacy sidecar | The sidecar is absent, carries no blobs, or uses the EIP-4844 shape | Supply the EIP-7594 blobs, commitments, and cell proofs |
| Too many blobs | Transaction exceeds the active blob limit | Include no more than six blobs in the transaction |
| Invalid KZG commitment or cell proof | Sidecar does not verify against the transaction's versioned hashes | Rebuild the sidecar and verify it before submission |

## MIP20

MIP20 uses custom errors from ERC-20, ERC-3009 and ReceivePolicy. Decode revert
data using the ABI for the token and the called policy contract.

| Error | Meaning | Corrective action |
| - | - | - |
| `ERC20InvalidSender(address)` | The source address is zero | Supply a nonzero source |
| `ERC20InvalidReceiver(address)` | The recipient is zero, is ReceivePolicy directly, or differs from the submitter of `receiveWithAuthorization` | Check the recipient address. For `receiveWithAuthorization`, the caller must be the recipient |
| `ERC20InvalidSpender(address)` | The approved spender is zero | Supply a nonzero spender |
| `ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed)` | Spendable balance is below the amount | Read `balanceOf(from)` and reduce or fund the transfer |
| `ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed)` | The caller lacks allowance | Approve the required finite amount |
| `InsufficientHold(address owner, address spender, uint256 held, uint256 needed)` | The caller's hold is below the requested capture or release | Read `heldBalanceOf(owner, caller)` |
| `ERC3009InvalidAuthorizationTime(uint256 validAfter, uint256 validBefore)` | The authorization is outside its strict validity bounds | Check the signed window against chain time |
| `ERC3009UsedAuthorization(address authorizer, bytes32 nonce)` | The nonce was used or canceled | Reconcile `AuthorizationUsed`, `AuthorizationCanceled` and the payment outcome before deciding on another authorization |
| `ERC3009InvalidSignature()` | The signature does not authorize the exact payment or cancellation | Check domain, fields and signer, and ERC-1271 for the bytes form |
| `CreditRefused(uint8 reason)` | ReceivePolicy refused an ordinary credit in REVERT mode | Read `reasonFor` and correct the input or recipient policy |
| `OwnableUnauthorizedAccount(address)` | Caller lacks token-owner authority | Use the current owner for mint, burn or ownership changes |
| `OwnableInvalidOwner(address)` | An ownership transfer names zero | Use a nonzero owner, or explicitly renounce ownership |

The ECDSA `(v,r,s)` overloads can also return `ECDSAInvalidSignature()` or
`ECDSAInvalidSignatureS(bytes32)`. The bytes-signature form reports an
unsuccessful owner-key and ERC-1271 check as `ERC3009InvalidSignature()`.

Quarantine records a successful transfer into escrow under a quarantine receipt.
The [Receive Policy reference](https://docs.metalntwx.com/reference/compliance/receive-policy.md) defines refusal
reasons and the `UnknownReceipt`, `NotRecoveryAuthority`, `StillBlocked` and
`ClaimTransferFailed` recovery errors.

## MIP410

The pool at `0x0000000000000000000000000000000000000410` defines the custom
errors below. Calls can also return revert data from the MIP20 token,
precompiles, or ABI decoding. A valid rejection submitted with its resolution
proof and policy-wrapper proof succeeds and records rejection as the payment
outcome.

### Proofs and field rules

| Error | Meaning | Corrective action |
| - | - | - |
| `NonCanonicalField()` | A calldata word checked by the pool is not a canonical BN254 field encoding | Re-encode with the circuit package. Do not reduce arbitrary values modulo the field |
| `InvalidProof(circuitId)` | The named circuit's proof did not verify — an invalid proof, or one built under a key the circuit-ID table no longer names | Rebuild the proof for this chain and pool under the currently registered key. On a two-proof call the argument says which proof failed |

### Caller failures

| Error | Meaning | Corrective action |
| - | - | - |
| `ReentrantCall()` | The pool was re-entered during a value transfer | Avoid reentering a pool value operation. The outer call is rolled back |
| `NotAuthority()` | The caller is not `authority()` | Submit from the authority account |
| `NotPendingAuthority()` | `acceptAuthority` was called by an account other than the one named by `transferAuthority` | Have the named account accept, or name it first |
| `Paused()` | A value path was called while the pool is paused | Wait for `PoolUnpaused`. Reclaims and registry writes still succeed |

### Shield boundaries

| Error | Meaning | Corrective action |
| - | - | - |
| `AssetNotAdmitted(asset)` | The authority has not admitted the asset, or has revoked it | Shield an admitted asset. Read `assetAdmitted(asset)` first |
| `ZeroAmount()` | The shield or unshield amount is zero | Supply a positive amount |
| `AmountOutOfRange()` | The shield or unshield amount is not below `2^128` | Reduce the amount or split it across valid operations |
| `FutureShieldTimestamp()` | `shieldTimestamp` is after the block timestamp | Use the current chain time when minting the note |
| `StaleShieldTimestamp()` | `shieldTimestamp` is more than 3600 seconds before the block timestamp | Rebuild the note with a fresh timestamp and resubmit promptly |
| `DepositDeltaMismatch(expected, received)` | The pool received a different MIP20 amount than the request states | Use an asset whose transfer delivers the exact amount. The pull is rolled back |

### Commitment tree invariants

| Error | Meaning | Corrective action |
| - | - | - |
| `ZeroLeaf()` | A zero note commitment was presented for appending | Generate a real note |
| `EmptyLeafSentinel()` | The tree's empty-leaf sentinel was presented as a commitment | Generate a real note |
| `DuplicateCommitment()` | The note commitment is already in the tree | Generate a fresh note secret and rebuild the proof |
| `TreeFull()` | The commitment tree holds `2^32` leaves | No further notes can be appended to this pool |

### Hold and lock rules

| Error | Meaning | Corrective action |
| - | - | - |
| `WrongInputArity()` | `inputLockIds` or `nullifiers` does not carry exactly `MAX_INPUTS = 2` entries | Pad with zero dummy slots to the fixed arity |
| `AllDummyInputs()` | Every input slot is a dummy | Spend at least one real note |
| `DuplicateInput()` | Both input lock IDs of an initiation name the same note | Spend two distinct notes, or one note and a dummy slot |
| `UnknownCommitmentRoot()` | The commitment tree never had the named root | Rebuild the tree witness from the pool's appends and prove against `commitmentRoot()` |
| `UnknownRegistrationRoot()` | The named registration root is neither the current one nor, after an admission, the one before it | Re-read `registrationRoot()` and rebuild the wrapper's leaf opening. See [Root windows](https://docs.metalntwx.com/reference/assets-and-payments/mip410.md#root-windows) |
| `UnknownSanctionsRoot()` | The named sanctions root is not the current one. A sanctions write retires the previous root at once | Re-read `sanctionsRoot()` and rebuild the non-membership opening. See [Root windows](https://docs.metalntwx.com/reference/assets-and-payments/mip410.md#root-windows) |
| `InputAlreadyLocked()` | An input is reserved by a live hold, or tombstoned by an accepted one | After `Rejected` or `Reclaimed`, rebuild from the released inputs. After `Accepted`, refresh the wallet's unspent notes |
| `HoldNotAbsent()` | A hold with this payment effect hash already exists | Reuse the existing hold. Holds are never deleted |
| `DeadlinePast()` | The initiation deadline is not in the future | Choose a later deadline and rebuild the proof |
| `ZeroPayloadCommitment()` | The initiation names no payload commitment | Commit to the payment's payload and rebuild the proof |
| `DeadlineExceedsCap()` | The deadline is more than `maxHoldDuration()` ahead | Choose a deadline within the ceiling and rebuild the proof |
| `HoldNotHeld()` | The hold is absent or already terminal | Read `holdOf(holdId)` and stop retrying a terminal transition |
| `DeadlinePassed()` | A resolution arrived after the deadline | Reclaim the hold instead of resolving it |
| `EffectMismatch(recomputed)` | The supplied resolution effect does not hash to the hold ID | Supply the four words the initiation sealed. The argument is what the supplied words hash to |
| `NullifierAlreadyUsed()` | A nullifier is already recorded | Refresh the wallet's unspent notes and rebuild the proof |
| `HoldNotExpired()` | Reclaim was submitted at or before the deadline | Wait until the finalized chain time is beyond the deadline |

### Exit rules

| Error | Meaning | Corrective action |
| - | - | - |
| `InsufficientBacking(backing, amount)` | The asset's recorded backing cannot cover the proven amount | Treat as a pool-accounting failure and stop submission |
| `TransferFailed()` | The outbound MIP20 transfer returned false | Inspect the token's transfer behavior. The operation is rolled back |
| `SolvencyBroken(balance, backing)` | After the transfer, the pool's balance fell below the asset's remaining backing | Treat as a pool-accounting failure and stop submission |

### Bridge rules

| Error | Meaning | Corrective action |
| - | - | - |
| `NotBridgeGateway()` | The caller is not `bridgeGateway()`, or no gateway is set | Submit through the configured gateway |
| `BridgeDepositAlreadyMinted()` | The deposit id has already minted its note | Stop retrying. Read `bridgeDepositMinted(depositId)` |
| `InsufficientBridgeLiability(liability, amount)` | The asset's bridge liability cannot cover the proven exit amount | Treat as a pool-accounting failure and stop submission |
| `BridgeAssetShieldable(asset)` | `bridgeMint` named an asset admitted for `shield` or still backing shielded notes | Bridge under an asset id the shield path does not use |
| `AssetBridged(asset)` | `admitAsset` named an asset that bridged notes still denominate | Admit a different asset id for `shield` |

### The VASP tree

The tree records virtual asset service providers (VASPs).

| Error | Meaning | Corrective action |
| - | - | - |
| `FiIndexOutOfRange(index)` | The slot is beyond the tree's 1,024 leaves | Use a slot below 1,024 |
| `FiSlotOccupied(index)` | `admitFi` named a slot that already holds an institution | Admit at a vacant slot |
| `FiSlotVacant(index)` | `revokeFi` or `updateFi` named an empty slot | Read `fiIndexOf(fiId)` for the institution's slot |
| `FiIdInUse(fiId)` | The identifier is already admitted at another slot | Revoke the existing admission first, or use the existing slot |
| `InvalidFiRecord()` | The identifier or the policy verification key hash is zero | Supply the derived identifier and policy verification key hash |
| `InvalidMessagingKey()` | The messaging key is the identity, off the curve, or non-canonical | Supply the key's affine coordinates below the field modulus |
| `MessagingKeyInUse(ownerFiId)` | `admitFi` or `updateFi` named a messaging key another institution holds or has held | Generate a fresh messaging key. A key never moves between institutions |
| `InvalidFiSignature()` | The `updateFi` signature does not verify under the slot's current key over the current registration root and the slot's update nonce | Sign with the key being replaced, over the root and `fiUpdateNonce(index)` read in the same block. See [The VASP tree](https://docs.metalntwx.com/reference/assets-and-payments/mip410.md#the-vasp-tree) |

### The sanctioned set

| Error | Meaning | Corrective action |
| - | - | - |
| `InvalidAffilId()` | A zero identifier. Zero is the start-of-chain key | Sanction a real `affilID` |
| `SanctionsIndexOutOfRange(index)` | The position is at or past `sanctionsCount()` | Read the set and name a live position |
| `SanctionsBracketMismatch()` | The entry at `lowIndex` does not bracket the identifier, so it is already present or `lowIndex` is the wrong leaf | Read `sanctionsLeaves` and name the entry whose key is the greatest below the identifier |
| `SanctionsListUnsorted()` | A reset list is not strictly ascending | Sort and deduplicate before calling `resetSanctions` |

## Circuit Registry

| Error | Meaning | Corrective action |
| - | - | - |
| `UnknownBackend(uint8 backend)` | Registration uses a backend other than UltraHonk `0x02` | Use the supported backend tag |
| `InvalidVerificationKeyLength(uint256 length)` | Verification-key length or stored blob shape is invalid | Use a key between 256 and 24,574 bytes |
| `UnknownCircuit(bytes32 circuitId)` | Key lookup names an unregistered circuit | Register the key or use its correct content-addressed ID |

`CircuitRegistry.verify(bytes)` returns `false` for a malformed request, unknown
circuit, or failed proof. See the
[Circuit Registry](https://docs.metalntwx.com/reference/privacy/circuit-registry.md#verification-result) for
its wire and failure rules.

## Interoperability

| Error | Contracts | Meaning |
| - | - | - |
| `TreeFull()` | Metal outbox | The message tree has no remaining leaf capacity |
| `AlreadyConsumed()` | `MetalInbox`, `ForeignInbox` | This inbox has already executed the nonce successfully |
| `NotInTree()` | `MetalInbox` | Message index, leaf count, or Merkle path does not verify against the checkpoint message root |
| `MessageMismatch()` | `ForeignInbox` | The proved outbox slot does not contain the submitted message commitment |
| `AccountProofInvalid()` | `ForeignInbox` | The account proof does not authenticate the source outbox under the checkpoint state root |
| `CodeHashMismatch()` | `ForeignInbox` | The proved source outbox account does not run the outbox code hash the inbox was configured with |
| `InvalidOutboxCodeHash()` | `ForeignInbox` | Constructor outbox code hash is zero or the hash of empty code |
| `TrieProofTraversalError(uint8)` | `ForeignInbox` | An account or storage Merkle Patricia proof does not verify. The argument is OpenZeppelin's `TrieProof.ProofError` code |
| `RLPInvalidEncoding()` | `ForeignInbox` | A proved account or storage value is not valid RLP |
| `TargetReverted()` | `MetalInbox`, `ForeignInbox` | The destination contract call reverted. The nonce remains unconsumed |
| `UnknownCheckpoint()` | Metal, Ethereum, Tempo, and Arc light clients | No checkpoint is stored for the requested block hash |
| `InvalidCheckpointHeight()` | Metal, Tempo, and Arc light clients | Header height is zero, reserved for the unknown-checkpoint sentinel |
| `Malformed()` | Metal, Tempo, and Arc light clients | Header RLP cannot be parsed in the required source format |
| `NotMonotonic()` | Metal, Tempo, and Arc light clients | The checkpoint does not advance the required height and consensus-round ordering |
| `BadValidatorSet()` | Arc light client | Constructor validator keys or voting powers are invalid |
| `BadKeySet()` | Metal light client | Constructor key lists are empty or differ in length, a key's compressed and uncompressed forms are the wrong size or disagree, or the keys are not in strictly ascending order |
| `BadProofOfPossession()` | Metal light client | A constructor validator key's proof of possession fails verification |
| `BadIdentity()` | Tempo light client | Constructor group key or its encoding is invalid |
| `UnsupportedEpoch()` | Metal and Tempo light clients | Metal requires epoch `0`. Tempo requires an epoch at or after its configured key's starting epoch |
| `BadBitmap()` | Metal light client | Signer bitmap has an invalid length or trailing bits |
| `BadVoteOrder()` | Arc light client | Vote indices are out of range, duplicated, or unordered |
| `QuorumNotMet()` | Metal and Arc light clients | Valid signers do not meet the required quorum |
| `BadSignature()` | Metal, Tempo, and Arc light clients | The supplied consensus signature fails verification |

Use the [message](https://docs.metalntwx.com/reference/interop/messages.md),
[inbound message](https://docs.metalntwx.com/reference/interop/inbound-messages.md), and
[light-client references](https://docs.metalntwx.com/reference/interop/light-clients.md) to rebuild the
matching evidence.

## Decode a custom error

```ts theme={null}
import { decodeErrorResult } from "viem";

const decoded = decodeErrorResult({
  abi: contractAbi,
  data: revertData,
});

console.log(decoded.errorName, decoded.args);
```

For an included transaction, retrieve its receipt and execution trace from the
RPC provider. For an admission failure, inspect the error returned by
`eth_sendRawTransaction`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.