> ## 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.

# MIP410 API

> MIP410 shield, hold, resolution, reclaim, unshield, state, registry, and administration interfaces

MIP410 is the protocol-deployed shielded pool at
`0x0000000000000000000000000000000000000410`. This page describes its current
contract interface. The key holder constructs notes and wallet authorization
signatures. It may be the customer's wallet or the custodial system of the
customer's VASP.

For a shielded payment, the sender VASP prepares `initiatePayment`. The recipient
VASP prepares `acceptPayment` or `rejectPayment`. Each call carries a payment
proof and a policy-wrapper proof. Any public account may submit a prepared
transaction and pay gas. See [Who submits](https://docs.metalntwx.com/learn/mip410/roles-and-authorization.md#submitter).

The pool maintains its own VASP registry and sanctions registry. The authority
admits VASPs and assets, sets circuit verification keys, and can pause value
movement. A fresh pool starts with empty registries, no admitted assets, and no
configured circuit IDs. The authority configures them before the first shield.

Solidity identifiers retain `Fi` and `fi` in their names. In this reference,
those identifiers refer to VASPs. Use the matching ABI and client package for
calldata and return encoding.

## Core types

```solidity theme={null}
enum HoldState {
    Absent,
    Held,
    Accepted,
    Rejected,
    Expired,
    Reclaimed
}

struct ShieldRequest {
    address asset;
    address depositor;
    uint256 amount;
    bytes32 noteCommitment;
    uint64 shieldTimestamp;
    bytes32 sanctionsRoot;
}

struct InitiationRequest {
    bytes32 commitmentRoot;
    bytes32[] inputLockIds;
    bytes32 paymentEffectHash;
    bytes32 recipientFiBindingCommitment;
    uint256 nonce;
    uint64 deadline;
    bytes32 payloadCommitment;
    bytes32 registrationRoot;
    bytes32 sanctionsRoot;
}

struct Roots {
    bytes32 registrationRoot;
    bytes32 sanctionsRoot;
}

struct ResolutionEffect {
    bytes32[] nullifiers;
    bytes32 recipientNoteCommitment;
    bytes32 changeNoteCommitment;
}

struct UnshieldRequest {
    address asset;
    uint256 amount;
    address destination;
    bytes32 commitmentRoot;
    bytes32[] inputLockIds;
    bytes32[] nullifiers;
    bytes32 changeNoteCommitment;
    bytes32 sanctionsRoot;
}

struct HoldView {
    HoldState state;
    bytes32 paymentEffectHash;
    bytes32 recipientFiBindingCommitment;
    uint64 deadline;
    bytes32 payloadCommitment;
}

struct FiRecord {
    bytes32 fiId;
    bytes32 vkHash;
    bytes32 mpkX;
    bytes32 mpkY;
}

struct Point {
    bytes32 x;
    bytes32 y;
}

struct SanctionsLeaf {
    bytes32 key;
    bytes32 nextKey;
}

enum Circuit {
    Shield,
    Initiation,
    Resolution,
    Unshield,
    Wrapper
}
```

`Expired` is never stored. A hold whose deadline has passed remains `Held` in
storage, and `holdOf` synthesizes `Expired` when reporting it.

`acceptPayment` and `rejectPayment` select the outcome. The circuit uses
`Accept = 0` and `Reject = 1`, as listed under [Proof bindings](#proof-bindings).

Both `inputLockIds` and `nullifiers` must carry exactly `MAX_INPUTS = 2`
entries; a wrong length reverts `WrongInputArity`. A zero entry is a dummy
slot. A payment spending one note supplies its real value first and zero
second.

`inputLockIds` identify the inputs a hold reserves. They are distinct from
nullifiers, which the contract records only when a payment is accepted. An
unshield presents its input lock IDs so the pool can refuse any input that a
hold has reserved or that an accepted payment has spent. Acceptance keeps
the input locks permanently set. Only `rejectPayment` and `reclaimExpired`
release them.

`FiRecord` is one leaf's preimage in the VASP tree: the institution's
identifier, the hash of its policy verification key, and its Grumpkin
messaging key as two coordinates. A vacant slot reads as all zeros. `Point`
carries a Grumpkin point — a messaging key or a Schnorr nonce — as raw
coordinates. `SanctionsLeaf` is one entry of the sorted sanctioned set: its
key and the next greater key, zero for the greatest. `Circuit` indexes the
[circuit-ID table](#the-circuit-id-table); it travels as `uint8`.

## Root windows

Each request names the roots its proofs use. The applicable roots differ by
transaction type, as listed in [Proof bindings](#proof-bindings). The
commitment tree accepts every nonempty root it has had. Appending notes
does not invalidate a membership proof against an earlier root. Input locks
and nullifiers prevent the same note from being spent twice.

The VASP registry accepts its current root and, after an admission, the root
immediately before that admission. Revoking a VASP or updating its record
clears the previous root, so the change takes effect immediately. The sanctions
registry accepts only its current root. Zero is never accepted by either registry.
A proof against a retired registry root must be rebuilt with the accepted
registry state. If the required VASP admission or sanctions non-membership can
no longer be proven, a held payment can be reclaimed after its deadline.

Use `isKnownRegistrationRoot(root)` to check registration-root acceptance,
and compare the sanctions root with `sanctionsRoot()`. `previousRegistrationRoot()` reports the registration root retained
after an admission, or zero when there is no previous accepted root.

## Pool and hold state

| Getter | Return | Meaning |
| - | - | - |
| `commitmentRoot()` | `bytes32` | Current commitment tree root |
| `isKnownCommitmentRoot(root)` | `bool` | Whether the tree has had the supplied nonempty root |
| `commitmentCount()` | `uint256` | Number of note commitments appended to the commitment tree |
| `nullifierUsed(nullifier)` | `bool` | Whether the nullifier has been recorded |
| `inputLocked(inputLockId)` | `bool` | Whether the input is reserved by a live hold, or permanently locked by an accepted one |
| `holdOf(holdId)` | `HoldView` | Current state, fixed payment effect, recipient VASP binding commitment, deadline, and payload commitment |
| `backingOf(asset)` | `uint256` | Transparent MIP20 backing held for an asset |
| `maxHoldDuration()` | `uint64` | Ceiling, in seconds, on how far ahead a hold deadline may sit |
| `bridgeLiabilityOf(asset)` | `uint256` | Bridged value of an asset still in the pool, owed against the route's foreign reserves |
| `bridgeDepositMinted(depositId)` | `bool` | Whether the bridge deposit has already minted its note |

A proof may cite any historical nonempty commitment root. The empty tree
root is not accepted as a spend root.

Passing the deadline makes a hold reclaimable but releases nothing on its own;
only the `reclaimExpired` call does that.

## Registry state

The VASP tree is a positional Merkle tree of depth 10: 1,024 slots, one
admitted institution per occupied slot, vacant slots hashing to the empty
leaf. The sanctioned set is a sorted indexed tree of depth 20 whose entries
form a linked list in key order, position 0 being the start-of-chain leaf.
Both live in the pool's storage, so anyone rebuilds either tree — and any
inclusion or non-membership path — from the getters alone; the rebuilt root
must equal the pool's, otherwise the reader has stale or partial data.

| Getter | Return | Meaning |
| - | - | - |
| `registrationRoot()` | `bytes32` | Current root of the VASP tree |
| `previousRegistrationRoot()` | `bytes32` | The root before the latest admission, or zero after a revocation or update |
| `isKnownRegistrationRoot(root)` | `bool` | Whether `root` is the current root or the previous root retained after an admission |
| `fiUpdateNonce(index)` | `uint256` | Nonce for the next signed record update at a slot |
| `fiRecord(index)` | `FiRecord` | The record at a slot; all zeros while vacant |
| `fiRecords(start, count)` | `FiRecord[]` | The records at `start .. start + count`, one call; reverts `FiIndexOutOfRange` past slot 1,023 |
| `fiIndexOf(fiId)` | `(bool admitted, uint16 index)` | Whether an institution is admitted, and at which slot |
| `fiLeaf(index)` | `bytes32` | The leaf hash at a slot, or the empty leaf while vacant |
| `sanctionsRoot()` | `bytes32` | Current root of the sanctioned set |
| `sanctionsLeaf(index)` | `(bytes32 key, bytes32 nextKey)` | One entry by position; reverts `SanctionsIndexOutOfRange` at or past the count |
| `sanctionsLeaves(start, count)` | `SanctionsLeaf[]` | The entries at `start .. start + count`, in position order, one call |
| `sanctionsCount()` | `uint256` | Entries in the current set, the start-of-chain leaf included |
| `sanctionsGeneration()` | `uint256` | Incremented by every `resetSanctions`; positions restart at each generation |

Fetch `fiRecords(0, 1024)` and `sanctionsLeaves(0, sanctionsCount())` to
rebuild both trees in two calls. Registry events can trigger a refresh of
these records.

## Administration state

| Getter | Return | Meaning |
| - | - | - |
| `authority()` | `address` | The account controlling admission, sanctions, pause, verification keys, and the hold ceiling |
| `pendingAuthority()` | `address` | The account named by an unfinished authority transfer, or zero |
| `paused()` | `bool` | Whether the value paths are stopped |
| `assetAdmitted(asset)` | `bool` | Whether `shield` accepts the asset |
| `circuitId(circuit)` | `bytes32` | ID of the verification key used to verify that circuit's proofs |
| `bridgeGateway()` | `address` | The only account allowed to call `bridgeMint` and `bridgeWithdraw`; zero disables both |

## Shield

```solidity theme={null}
function shield(
    ShieldRequest calldata request,
    bytes calldata depositAuthorization,
    bytes calldata shieldProof
) external returns (bytes32 noteCommitment);
```

The customer VASP prepares `shield`. The shield proof verifies the key holder's
Schnorr authorization and binds the public asset and amount received by the
pool to `request.noteCommitment`. The diversified address, spending key, and
note secret stay private.

`request.asset` must be one the authority has admitted, otherwise the call
reverts `AssetNotAdmitted` before anything is pulled. `request.sanctionsRoot`
names the sanctions root the proof's non-membership opening used; see [Root
windows](#root-windows).

`depositAuthorization` authorizes the public MIP20 transfer from
`request.depositor` into one note, encoded as:

```solidity theme={null}
abi.encode(uint256 validAfter, uint256 validBefore, bytes signature)
```

The pool decodes that and calls the asset's ERC-3009
`receiveWithAuthorization` with itself as payee and `request.noteCommitment`
as the nonce. The depositor therefore signs a `ReceiveWithAuthorization` whose
`to` is the pool and whose `nonce` is the commitment of the note being funded.

The token's EIP-712 domain binds the chain and token. The payee and single-use
nonce bind the authorization to funding this note at this pool. The shield
proof also verifies the note owner's Schnorr authorization; this is separate
from the depositor's ERC-3009 signature.

The pool measures its MIP20 balance before and after the pull and requires an
increase of exactly `request.amount`, otherwise it reverts
`DepositDeltaMismatch`.

`request.shieldTimestamp` enters the note commitment. It must satisfy
`shieldTimestamp <= block.timestamp` and
`block.timestamp - shieldTimestamp <= 3600` seconds.

Successful execution updates `backingOf(request.asset)` and appends the note
commitment. Applications wait for the containing block to become final before
using the note. While the pool is paused, `shield` reverts `Paused`.

## Initiate a shielded payment

```solidity theme={null}
function initiatePayment(
    InitiationRequest calldata request,
    bytes calldata senderProof,
    bytes calldata policyProof
) external returns (bytes32 holdId);
```

The sender VASP prepares `initiatePayment`. `request.paymentEffectHash` commits
the payment effect without publishing the recipient or change note commitment
before the payment is accepted, and it *is* the returned `holdId` — holds are
keyed by their own effect hash, written once and never deleted, so the same
effect can never open a second hold.

`request.deadline` must satisfy `block.timestamp < deadline` and
`deadline <= block.timestamp + maxHoldDuration()`, reverting `DeadlinePast` or
`DeadlineExceedsCap` respectively. `request.nonce` is forwarded to the proof
alone; the pool neither stores it nor keys anything on it.
`request.payloadCommitment` commits to the payment's payload: its identifiers
and, when present, its travel-rule record. It must be a
canonical field element and nonzero, reverting `NonCanonicalField` or
`ZeroPayloadCommitment`, and the pool stores it on the hold.
`request.registrationRoot` and `request.sanctionsRoot` name the anchors the
two proofs opened; see [Root windows](#root-windows).

Two proofs are required. `senderProof` is the initiation statement, binding:

* the chain and the pool;
* the commitment root the inputs are proven against;
* both input lock IDs;
* the payment effect hash and the recipient VASP binding commitment;
* the nonce and deadline;
* the payload commitment, recomputed from the payload's plaintext; and
* the sanctions root — a sanctions non-membership proof.

The initiation circuit also verifies the spending-key signature, input-note
membership and ownership, amount bounds, and value conservation. The
nullifiers and output note commitments stay private inside the payment effect
hash until acceptance.

`policyProof` is the sender VASP's policy-wrapper proof. The wrapper verifies the
private policy proof under the verification-key hash in that VASP's registry
leaf. It exposes the same chain, pool, payment effect hash, payload commitment,
commitment root, and recipient VASP binding commitment at phase `0`. It proves
the sender VASP's membership against `request.registrationRoot` and verifies
that VASP's signature under its registered messaging key.

The payload itself travels off chain, sealed to the recipient VASP. The pool
stores only its commitment, and both resolution proofs carry the stored value.

A successful initiation creates a hold, fixes its payment effect and deadline,
and locks every real input. Nullifiers are not consumed, and neither the
recipient nor the change note commitment is appended. While the pool is
paused, `initiatePayment` reverts `Paused`.

## Resolve a shielded payment

```solidity theme={null}
function acceptPayment(
    bytes32 holdId,
    Roots calldata roots,
    ResolutionEffect calldata resolutionEffect,
    bytes calldata recipientProof,
    bytes calldata policyProof
) external;

function rejectPayment(
    bytes32 holdId,
    Roots calldata roots,
    bytes calldata recipientProof,
    bytes calldata policyProof
) external;
```

The recipient VASP resolves a finalized, live hold addressed to it by calling one
of the two entry points. It prepares `recipientProof`, binding the decision to
the hold's payment effect, and `policyProof`, its policy-wrapper proof. Any
account may then submit the prepared pair. `roots` names the registration and
sanctions roots the two proofs opened — a resolution has no request struct of
its own, so the pair rides beside the hold ID; see [Root
windows](#root-windows). Both proofs carry the payload commitment stored on the
hold; neither entry point takes it as a parameter. Resolution requires
`block.timestamp <= deadline`.

`acceptPayment` takes the nullifiers plus the recipient and change note
commitments that `paymentEffectHash` committed to. The pool recomputes the
effect hash from the four supplied words and requires it to equal `holdId`,
reverting `EffectMismatch` otherwise. Given a match, the pool records the
nullifiers, appends the recipient note commitment and any nonzero change note
commitment, and marks the hold `Accepted`. Its input locks stay set permanently,
preventing the spent inputs from entering another hold or unshield.

`rejectPayment` takes no resolution effect. The pool supplies four zero values
to the resolution proof, so no nullifiers or proposed note commitments are
published. It releases the input locks and marks the hold `Rejected`, recording
no nullifiers and appending no commitments.

Policy-wrapper phases differ between the two: `1` for accept, `2` for reject.
At both phases the wrapper opens the recipient VASP's leaf against
`roots.registrationRoot` and requires the leaf's institution to be the one the
hold's recipient VASP binding commitment names.

An invalid, stale, mismatched, or unauthorized resolution leaves the hold
unchanged. Applications update payment status only after successful execution
and block finality. While the pool is paused, both entry points revert
`Paused`.

## Reclaim an expired hold

```solidity theme={null}
function reclaimExpired(bytes32 holdId) external;
```

`reclaimExpired` is valid only after the hold deadline. Successful execution
moves the stored state from `Held` — which `holdOf` has been reporting as
`Expired` — to `Reclaimed`, and releases the input locks. Nullifiers are not
consumed and no note commitment is appended. The `Accepted`, `Rejected`, and
`Reclaimed` terminal states are mutually exclusive, and none of them can be
left once entered.

`reclaimExpired` carries no proof and reads only public hold state, so the pool
accepts it from any account once the deadline has passed. The sender VASP
normally reclaims the hold and pays the gas, but another account may call
`reclaimExpired` with the same result. Reclaim is never paused: a hold whose
resolution the pause prevented can still be released after its deadline.

## Unshield

```solidity theme={null}
function unshield(
    UnshieldRequest calldata request,
    bytes calldata unshieldProof
) external;
```

The customer VASP prepares `unshield`. The call requires one unshield proof and no
policy-wrapper proof. It binds the note spend, the chain and pool, the
asset, amount and destination, both nullifiers, both input lock IDs, the change
note commitment, and the sanctions root named by `request.sanctionsRoot`.

The signature under the spending key authorizes the destination. MIP410 has
no customer or destination-registration check; any destination restrictions
are the customer VASP's offchain controls and the public token's transfer policy.
Revoking an asset's admission stops new shields while allowing existing backing
to be unshielded.

Execution requires every entry of `inputLockIds` to be unlocked, so a note
reserved by a live hold — or permanently locked by an accepted payment — cannot be
unshielded.

The pool follows strict checks-effects-interactions: it records the nullifiers,
appends any nonzero change note commitment, and reduces
`backingOf(request.asset)` *before* transferring to `request.destination`.
After the transfer it asserts that its own MIP20 balance still covers the
asset's remaining backing. A failed transfer, or a broken solvency assertion,
rolls back the complete operation. While the pool is paused, `unshield`
reverts `Paused`.

## Bridge

```solidity theme={null}
function bridgeMint(
    BridgeMintRequest calldata request,
    bytes calldata mintProof
) external returns (bytes32 noteCommitment);

function bridgeWithdraw(
    BridgeWithdrawRequest calldata request,
    bytes calldata withdrawProof
) external returns (bytes32 withdrawalId);
```

Value proven onto Metal from a foreign chain enters the pool as a note, with
no MIP20 balance and no `shield` call. Only `bridgeGateway()` may call either
function; any other caller, or any caller while no gateway is set, reverts
`NotBridgeGateway`. The gateway attests the deposit: its id, asset and
amount. The note is never trusted: the `bridgeMint` proof shows that
`request.noteCommitment` opens to exactly that amount and asset for an
unsanctioned owner. Each `depositId` mints once; a second mint reverts
`BridgeDepositAlreadyMinted`.

Bridged value is accounted in `bridgeLiabilityOf(asset)`, never in
`backingOf(asset)`, because its reserves sit on the foreign chain. The two
paths never share an asset: `bridgeMint` reverts `BridgeAssetShieldable` for
an asset admitted for `shield` or still backing shielded notes, and
`admitAsset` reverts `AssetBridged` for an asset with bridge liability
outstanding. Bridged notes pay and resolve like any other note.

`bridgeWithdraw` is the exit toward the foreign chain. It spends notes
exactly as `unshield` does — the same nullifier set, input locks checked and
change appended — but moves no tokens. Instead of a destination it binds an
opaque `routeId` and a `destCommitment` that the foreign vault opens at
payout, so the pool never learns the payout address. It reduces
`bridgeLiabilityOf(request.asset)`, reverting `InsufficientBridgeLiability`
when the liability cannot cover the amount, and returns the `withdrawalId`
the gateway routes outward. While the pool is paused, both functions revert
`Paused`.

## Payment requests

A shielded payment starts with a request the recipient's VASP issues off chain
and the sender's wallet verifies before anything is proven. The request never
reaches the pool; it is listed here because the sender's check reads the VASP
tree through the getters above.

| Field | Size | Meaning |
| - | - | - |
| `version` | 1 byte, `2` | The request format |
| `address` | 96 bytes | The recipient's diversified address: the diversifier and the point's two coordinates |
| `fi_id` | 32 bytes | The recipient VASP's identifier, as admitted in the VASP tree |
| `index` | 8 bytes | The VASP tree slot the recipient VASP occupies |
| `expires_at` | 8 bytes | Unix time after which the sender refuses to initiate |
| `thresholds` | 4 × 48 bytes | Record thresholds as four `(asset, amount)` pairs; unused slots are zero |
| `signature` | 96 bytes | The recipient VASP's Schnorr signature over the fields above, under its messaging key |

The sender's wallet reads `fiRecord(index)`, checks that its `fiId` is the
request's, verifies the signature under the record's messaging key, and
refuses a request whose `expires_at` has passed at the chain's current block
timestamp. A request signed under a key the VASP has since rotated away fails
that check, so the sender fetches the record fresh rather than caching it.
Only then does it build the initiation proof.

The payment request signature and expiry are verified offchain. They are not
verified by the pool or initiation circuit. A wrong VASP binding can leave the
hold unresolved until reclaim.

## Proof system

| Layer | Choice | Role |
| - | - | - |
| Circuit language | Noir | Expresses circuit constraints |
| Proving system | UltraHonk | Produces proofs verified onchain |
| Native field | BN254 scalar field | Circuit arithmetic |
| Embedded curve | Grumpkin | Key arithmetic with native-field coordinates |
| Hash | Poseidon2 | Note commitments, nullifiers, and key derivation |
| Onchain verifier | `0xF2` precompile | Verifies UltraHonk proofs |

## Commitment and field rules

The note commitment is:

```text theme={null}
Poseidon2(amount, asset_id, d, pk_x, pk_y, secret, P, bitmap_0, bitmap_1, depth, shieldTimestamp)
```

`P`, `bitmap_0`, `bitmap_1`, and `depth` hash as zero.

| Rule | Value |
| - | - |
| BN254 field modulus | `0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001` |
| Amount range | `0 < amount < 2^128` |
| Commitment tree depth | `32` |
| Historical commitment roots accepted | Every historical nonempty root |
| Registry roots accepted | Registration: current root and the previous root after an admission. Sanctions: current root only |
| VASP tree | Depth `10`, positional: one institution per slot, 1,024 slots |
| Sanctioned set | Depth `20`, sorted indexed tree keyed by `affilID` |
| Payment inputs | Exactly two slots; one or two real inputs, with zero dummy padding |
| `Accept` resolution note commitments | One recipient note commitment and one change note commitment; the change note commitment is zero when no change remains |
| Tree behavior | Note commitments are append-only; spending records nullifiers |

`asset_id` encodes the 160-bit MIP20 token address directly as a field element.
Proof-bound field elements use canonical encodings below the BN254 field
modulus. The pool checks the calldata words it uses as native field elements
and reverts `NonCanonicalField` for out-of-range values.

## Proof bindings

The generated circuit package is the source for serialized public-input order.
At the contract boundary, each proof is bound to the following data:

| Proof | Publics | Required binding |
| - | - | - |
| Shield | 8 | Chain, pool, note commitment, amount, asset, depositor, `shieldTimestamp`, sanctions root |
| Initiation | 11 | Chain, pool, commitment root, both input lock IDs, payment effect hash, recipient VASP binding commitment, nonce, deadline, `payloadCommitment`, sanctions root |
| Resolution | 11 | Chain, pool, `holdId`, resolution flag (`Accept = 0`, `Reject = 1`), both nullifiers, recipient note commitment, change note commitment, recipient VASP binding commitment, `payloadCommitment` from the hold, sanctions root |
| Unshield | 12 | Chain, pool, asset, amount, destination, commitment root, both nullifiers, both input lock IDs, change note commitment, sanctions root |
| Bridge mint | 7 | Chain, pool, note commitment, amount, asset, `depositId`, sanctions root |
| Bridge withdraw | 13 | Chain, pool, asset, amount, `routeId`, `destCommitment`, commitment root, both nullifiers, both input lock IDs, change note commitment, sanctions root |
| Policy-wrapper proof | 8 | Chain, pool, payment effect hash, phase (`0` initiate, `1` accept, `2` reject), `payloadCommitment`, commitment root (present at phase `0`, zero afterwards), registration root, recipient VASP binding commitment (checked at phases `1` and `2`) |

The roots in every row are the values the request named, checked against the
[windows](#root-windows) before the proof is verified. Each VASP's policy
verification key hash sits in its leaf of the VASP tree beside its messaging
key, so the wrapper proves that the policy it verified is the one the leaf
carries and that the VASP signed for this payment.

Each proof is verified against the verification key the [circuit-ID
table](#the-circuit-id-table) names for its circuit. A proof under any other
key — including a key the table pointed at before a rotation — reverts
`InvalidProof` carrying the circuit ID it was checked against.

## Authority, registry, and VASP controls

Every write below is restricted to `authority()` unless noted, emits an event,
and stays available while the pool is paused. Registry writes update
their roots according to the [root windows](#root-windows).

### The VASP tree

| Operation | Caller | Result |
| - | - | - |
| `admitFi(uint16 index, bytes32 fiId, bytes32 vkHash, Point mpk)` | authority | Writes an institution's record into a vacant slot and recomputes the registration root; emits `FiAdmitted` |
| `revokeFi(uint16 index)` | authority | Returns an occupied slot to the empty leaf and recomputes the root; emits `FiRevoked` |
| `updateFi(uint16 index, bytes32 vkHash, Point mpk, Point r, bytes32 s)` | Any submitter with the VASP's signature | Replaces the slot's policy VK hash and messaging key and recomputes the root; emits `FiUpdated` |

`admitFi` reverts `FiSlotOccupied` at a taken slot, `FiIdInUse` for an
identifier already admitted elsewhere, `InvalidFiRecord` for a zero identifier
or VK hash, and `InvalidMessagingKey` for a key that is the identity, off the
curve, or non-canonical. `revokeFi` and `updateFi` revert `FiSlotVacant` at an
empty slot.

Messaging keys are bound to one VASP identifier. `admitFi` and `updateFi`
revert `MessagingKeyInUse` if another VASP holds or has held the supplied key.
A VASP can reuse its own earlier key.

`updateFi` verifies the Schnorr signature `(r, s)` under the slot's current
messaging key over
`Poseidon2(DOMAIN_FI_UPDATE_AUTH, chainId, pool, registrationRoot, index, nonce, fiId, vkHash, mpk.x, mpk.y)`,
where `registrationRoot` is the current anchor and `nonce` is
`fiUpdateNonce(index)`. Each update increments the nonce, and revocation does
not reset it. Binding the root means a
signature over a stale tree cannot be replayed after any other write; a
signature that does not verify reverts `InvalidFiSignature`. Rotating the
policy and rotating the key are the same write.

### The sanctioned set

| Operation | Result |
| - | - |
| `sanction(bytes32 affilId, uint32 lowIndex)` | Appends `affilId` to the set at the next position and relinks the entry at `lowIndex` to point at it; emits `Sanctioned` |
| `resetSanctions(bytes32[] sortedAffilIds)` | Rebuilds the set from a strictly ascending list under a fresh generation; emits `SanctionsReset` |

`lowIndex` names the entry that brackets the new key: `low.key < affilId <
low.nextKey`, or `low.nextKey == 0` when the new key is the greatest. A
wrong bracket — including an `affilId` already present — reverts
`SanctionsBracketMismatch`; a position at or past the count reverts
`SanctionsIndexOutOfRange`; a zero key reverts `InvalidAffilId`. Read
`sanctionsLeaves` to find the bracket before calling.

Lifting a sanction is a reset without the identifier. A reset that is not
strictly ascending reverts `SanctionsListUnsorted`. The rebuilt set starts a
new generation, so positions and paths from the old generation no longer
apply.

### The circuit-ID table

| Operation | Result |
| - | - |
| `setCircuitId(Circuit circuit, bytes32 newId)` | Points the circuit at a registered verification key; emits `CircuitIdUpdated` |

A verification key rotates without a redeploy: register the new key in the
circuit registry, then point the table at its ID. Proofs under the old key are
refused from that block on. There is no window, because a key rotation is a
deliberate upgrade the authority sequences with the institutions.

### Authority, pause, assets, and the hold ceiling

| Operation | Caller | Result |
| - | - | - |
| `transferAuthority(address next)` | authority | Names the account that may take over; naming zero cancels a pending transfer; emits `AuthorityTransferStarted` |
| `acceptAuthority()` | the pending account | Completes the transfer; emits `AuthorityTransferred` |
| `pause()` | authority | Stops `shield`, `initiatePayment`, `acceptPayment`, `rejectPayment`, `unshield`, `bridgeMint`, and `bridgeWithdraw`; emits `PoolPaused` |
| `unpause()` | authority | Reopens them; emits `PoolUnpaused` |
| `admitAsset(address asset)` | authority | Lets `shield` accept the asset; emits `AssetAdmitted` |
| `revokeAsset(address asset)` | authority | Stops new shields of the asset; emits `AssetRevoked` |
| `setMaxHoldDuration(uint64 newDuration)` | authority | Moves the ceiling on how far ahead a hold deadline may sit; emits `MaxHoldDurationUpdated` |
| `setBridgeGateway(address newGateway)` | authority | Names the account allowed to call the [bridge](#bridge) functions; zero disables them; emits `BridgeGatewayUpdated` |

The current authority retains its powers until the pending account calls
`acceptAuthority`. A zero pending address cancels the transfer.

While paused, `reclaimExpired` and registry and administrative writes remain
available. Asset revocation affects shielding only.

Policy commitments and verifier metadata do not publish private policy inputs.
Submitter accounts and their gas payments remain public, so a VASP that submits
its own transactions may be identifiable at the transaction layer.

## Gas

The hidden payment amount is conserved independently of transaction gas.
Whichever account submits a transaction pays ordinary Metal gas in a MIP20 gas
token. The submitter need not be the VASP that prepared the transaction.

## Events and errors

Use the [Event Catalog](https://docs.metalntwx.com/reference/network-fundamentals/events.md#mip410) to index note commitments,
nullifiers, hold transitions, and registry writes.

Use [Error Codes](https://docs.metalntwx.com/reference/error-codes.md#mip410) to distinguish proof failures,
input lock conflicts, terminal holds, registry failures, and administration
errors. Reverts from the MIP20 token or malformed ABI data can carry other
revert data.

> [card] [MIP410 shielding](https://docs.metalntwx.com/learn/mip410/shielded-payments.md)
>
> Understand how public MIP20 backing enters the shielded pool.

> [card] [Shielded payment guide](https://docs.metalntwx.com/build/payments/shielded-payment.md)
>
> Follow the shield, initiate, resolve, and unshield workflow.

> [card] [Constants and limits](https://docs.metalntwx.com/reference/protocol-constants.md#mip410)
>
> Check tree, amount, input, and hold bounds.

> [card] [Key derivation](https://docs.metalntwx.com/reference/privacy/key-derivation.md)
>
> Review the spending key, viewing keys, the diversified address, and the VASP messaging key.


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