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

# Receive Policy

> Choose which MIP20 transfers an account accepts and who can recover refused funds

Receive policy lets an account choose which MIP20 tokens it accepts, from whom,
and in what amounts. The receive policy contract, `ReceivePolicy`, stores
each account's rules at `0x0000000000000000000000000000000000000f4d`. MIP20 calls
it to check those rules before crediting the account.

For an ordinary transfer, a refusal either reverts the transfer or sends the
funds to **quarantine**, where `ReceivePolicy` holds them until an authorized
account releases them. The recipient chooses the refusal mode and who can
recover quarantined funds. Refused mints, MIP20 hold captures, and payouts from
a token's own balance always use quarantine.

> [!WARNING]
>
> Maximum amounts and sender limits are not on Metal Testnet yet. Its
> `ReceivePolicy` is an earlier version: `setMaxAmount`, `setSenderLimit`,
> `setSenderFrequency`, `maxAmount`, `senderLimitOf`, `senderFrequencyOf`,
> `receivedFrom`, and `arrivalsFrom` revert, the `ABOVE_MAXIMUM`,
> `SENDER_OVER_LIMIT`, and `SENDER_TOO_FREQUENT` reasons never occur, and
> `policyOf` returns five fields without `limited`.

## Rules

Configure receive policy by calling the setters from the account whose rules
you want to change. Each setter applies to `msg.sender`. An externally owned
account (EOA) calls directly. For a smart contract, call a function that invokes
the setter from that contract's address.

| Rule | Setter | Effect |
| - | - | - |
| Sender blocklist | `setBlockedSenders(address[] senders, bool blocked)` | Refuses listed senders, including in allowlist-only mode |
| Allowlist-only mode | `setPolicy(allowlistOnly, …)` with `setAllowedSenders(address[] senders, bool allowed)` | Refuses any sender not on the allowlist |
| Token filter | `setPolicy(…, tokenFilter, …)` with `setAcceptedTokens(address[] tokens, bool accepted)` | Refuses any token not on the accepted list |
| Minimum amount | `setMinAmount(address token, uint256 amount)` | Refuses a credit below the minimum, measured in the token's smallest units |
| Maximum amount | `setMaxAmount(address token, uint256 amount)` | Refuses a single credit above the per-token maximum. Zero sets no maximum |
| Sender total | `setSenderLimit(address token, uint192 max, uint64 period)` | Refuses a credit that would take one sender's total of the token above `max` in the current window |
| Sender frequency | `setSenderFrequency(address token, uint192 max, uint64 period)` | Refuses a sender's credit of the token beyond `max` arrivals in the current window |
| Refusal mode | `setPolicy(…, refusal, …)` | `REVERT` (0, the default) or `QUARANTINE` (1). See [Refusal modes](#refusal-modes) |

```solidity theme={null}
function setPolicy(
    bool allowlistOnly,
    bool tokenFilter,
    Refusal refusal,
    Recovery recovery,
    address authority
) external;
function setBlockedSenders(address[] calldata senders, bool blocked) external;
function setAllowedSenders(address[] calldata senders, bool allowed) external;
function setAcceptedTokens(address[] calldata tokens, bool accepted) external;
function setMinAmount(address token, uint256 amount) external;
function setMaxAmount(address token, uint256 amount) external;
function setSenderLimit(address token, uint192 max, uint64 period) external;
function setSenderFrequency(address token, uint192 max, uint64 period) external;
```

The sender is the account whose balance is debited. For `transferFrom` and
ERC-3009 authorizations, this is the account that owns the tokens. A mint's
sender is the zero address, so an account in allowlist-only mode must allow
`address(0)` to accept mints.

An unconfigured account accepts all credits and uses `REVERT` mode.
Self-transfers are accepted.

### Sender limits

Sender limits count each sender separately for each token. A zero `period`
counts over the account's lifetime. Otherwise, counts use fixed windows of
`period` seconds from the Unix epoch; an `86400`-second window starts at UTC
midnight. A zero `max` sets no limit.

Only accepted, nonzero credits from another account count. A screened
quarantine claim counts against its destination's limits as a credit from the
originator. A claim accepted for the original receiver by a receiver or
`THIRD_PARTY` authority is unscreened and does not count.

Changing `max` preserves existing counts. Changing `period` preserves a count
only if the new window starts at the same time as the counted window;
otherwise, the count starts at zero.

### Screening order

`ReceivePolicy` checks the rules in this order and refuses on the first failure:

| `Reason` | Value | Refused because |
| - | - | - |
| `NONE` | 0 | Accepted |
| `SENDER_BLOCKED` | 1 | The sender is on the recipient's blocklist |
| `SENDER_NOT_ALLOWED` | 2 | Allowlist-only mode is on and the sender is not allowlisted |
| `TOKEN_NOT_ACCEPTED` | 3 | The token filter is on and the token is not accepted |
| `BELOW_MINIMUM` | 4 | The amount is below the recipient's minimum for the token |
| `ABOVE_MAXIMUM` | 5 | The amount is above the recipient's maximum for the token |
| `SENDER_OVER_LIMIT` | 6 | The credit would take the sender's total of the token above the recipient's limit in this window |
| `SENDER_TOO_FREQUENT` | 7 | The sender has reached the recipient's number of arrivals of the token in this window |

### Check before sending

Applications can check whether the stored rules would refuse a transfer before
submitting it:

```solidity theme={null}
function reasonFor(address token, address from, address to, uint256 amount)
    external view returns (Reason);
```

A nonzero result identifies the refusal under the current rules. The transfer
path and refusal mode determine whether it reverts or credits quarantine. Read
the configuration through `policyOf(account)`, `senderBlocked(account, sender)`,
`senderAllowed(account, sender)`, `tokenAccepted(account, token)`,
`minAmount(account, token)`, `maxAmount(account, token)`,
`senderLimitOf(account, token)`, and `senderFrequencyOf(account, token)`.
`receivedFrom(account, sender, token)` and `arrivalsFrom(account, sender, token)`
return the window start and the amount or arrival count used in that window.
The final `limited` field in `policyOf` indicates whether the account has
enabled limit tracking.

## Refusal modes

For ordinary transfers, the recipient selects one of these modes:

| `Refusal` | A refused credit | Use |
| - | - | - |
| `REVERT` (0, default) | Reverts with `CreditRefused(Reason reason)` and leaves the funds with the sender | Reject the transfer immediately |
| `QUARANTINE` (1) | Completes the token transfer with funds held by `ReceivePolicy` under a quarantine receipt | Hold refused funds for review and recovery |

Three paths always send refused credits to quarantine, whatever the recipient's
mode:

* **MIP20 mints**.
* **MIP20 hold captures** (`captureHold`), such as a scheduled payment's hold.
* **Payouts a token makes from its own balance**, such as a yield token's
  redirected yield, which settles inside other parties' transfers.

Contracts paying multiple recipients can check `reasonFor` before each transfer
and handle refusals explicitly. `Clearing` defers a payout that reverts in
`REVERT` mode and lets the destination withdraw it later. A payout refused in
`QUARANTINE` mode goes to `ReceivePolicy` escrow.

## Recovery modes

`setPolicy` selects the **recovery authority**, the account allowed to release
quarantined funds. Each receipt records the authority chosen when it was
created. Changes to recovery settings apply to new receipts.

| `Recovery` | Authority | Use |
| - | - | - |
| `ORIGINATOR` (0, default) | The sender, subject to the fallback below | The sender reclaims or redirects the funds |
| `RECEIVER` (1) | The recipient | The recipient accepts the payment after review |
| `THIRD_PARTY` (2) | The configured `authority` | A compliance officer or operator decides |

Set `authority` to a nonzero address for `THIRD_PARTY` and to the zero address
for the other modes. Any other combination reverts with `InvalidAuthority()`.

In `ORIGINATOR` mode, a nonzero sender with no code or an EIP-7702 delegation
becomes the recovery authority. For a mint or a sender with other contract code,
the receipt names the recipient instead.

## Quarantine and receipts

When a refused credit goes to quarantine, the token emits
`Transfer(from, 0x…0f4d, amount)` and `ReceivePolicy` emits:

```solidity theme={null}
struct Receipt {
    uint256 nonce;
    address token;
    address originator;
    address receiver;
    address authority;
    uint256 amount;
}

event Quarantined(
    bytes32 indexed receiptId,
    address indexed receiver,
    address indexed originator,
    Receipt receipt,
    Reason reason
);
```

`receiptId` is `keccak256(abi.encode(receipt))`. Save the full receipt from the
event to pass to `claim`. `open(receiptId)` returns `true` while that receipt's
funds remain unclaimed.

Transfers that name the escrow as the recipient revert with
`ERC20InvalidReceiver`. Only a quarantine can credit it.

## Claiming

The recovery authority recorded in the quarantine receipt releases the funds. An
EOA submits directly. A contract authority must expose an entry point that calls
`claim` from that contract:

```solidity theme={null}
function claim(Receipt calldata receipt, address to) external;
```

| Destination | Screening |
| - | - |
| The original recipient, caller is not the originator | None. The caller accepts the payment |
| The original recipient, caller is the originator | Screened as a transfer from the originator |
| Any other address | Screened as a transfer from the originator |

The recipient can also call `claim` when the receipt names its originator as
authority and that originator now has contract code other than an EIP-7702
delegation. This covers a contract that sent funds during construction, before
its code was deployed. Recovery uses the original receipt and hash.

A successful claim emits `Claimed(receiptId, to)` and the token emits
`Transfer(0x…0f4d, to, amount)`. Each receipt can be claimed once.

| Error | Cause |
| - | - |
| `CreditRefused(Reason reason)` | The recipient refused an ordinary transfer in `REVERT` mode |
| `UnknownReceipt()` | The receipt does not match an open quarantine, or was already claimed |
| `NotRecoveryAuthority()` | The caller is neither the recorded authority nor the recipient eligible for the fallback |
| `StillBlocked(Reason reason)` | The destination's rules refuse the release |
| `ClaimTransferFailed()` | The token transfer out of the escrow returned `false` |

## Read and recover a quarantined credit

Use the public client, wallet client and transaction procedure from
[Send a direct payment](https://docs.metalntwx.com/build/payments/direct-payment.md). Verify the network
and code at the configured ReceivePolicy address.

Read the saved transaction's successful receipt as `transactionReceipt` and
verify its canonical block and finality. Decode `ReceivePolicy`'s logs with this
ABI, which represents enum values as `uint8`:

```typescript theme={null}
import { parseAbi, parseEventLogs, encodeAbiParameters, keccak256 } from "viem"

const policy = "0x0000000000000000000000000000000000000f4d" as const
const policyAbi = parseAbi([
  "function reasonFor(address token, address from, address to, uint256 amount) view returns (uint8)",
  "function policyOf(address account) view returns (bool allowlistOnly, bool tokenFilter, uint8 refusal, uint8 recovery, address authority, bool limited)",
  "function open(bytes32 receiptId) view returns (bool)",
  "function claim((uint256 nonce,address token,address originator,address receiver,address authority,uint256 amount) receipt,address to)",
  "event Quarantined(bytes32 indexed receiptId,address indexed receiver,address indexed originator,(uint256 nonce,address token,address originator,address receiver,address authority,uint256 amount) receipt,uint8 reason)",
  "event Claimed(bytes32 indexed receiptId,address indexed to)",
])
const quarantines = parseEventLogs({
  abi: policyAbi, eventName: "Quarantined", strict: true,
  logs: transactionReceipt.logs.filter(log => log.address.toLowerCase() === policy),
})
```

For the expected payment, match all of `receipt.token`, `receipt.originator`,
`receipt.receiver` and `receipt.amount` against the instruction. Require the
event's indexed receiver and originator to agree with its receipt, and a
matching `Transfer(originator, policy, amount)` from the expected token in the
same transaction. Together, these checks tie the policy event to the payment's
token and transfer.

Select the event that matches that evidence as `quarantine`. Its `args.receipt`
contains the full quarantine receipt. Recompute the ID before retaining it:

```typescript theme={null}
const receipt = quarantine.args.receipt
const receiptId = keccak256(encodeAbiParameters([
  { type: "tuple", components: [
    { name: "nonce", type: "uint256" },
    { name: "token", type: "address" },
    { name: "originator", type: "address" },
    { name: "receiver", type: "address" },
    { name: "authority", type: "address" },
    { name: "amount", type: "uint256" },
  ] },
], [receipt]))
```

Require `receiptId` to equal `quarantine.args.receiptId`. Save the full receipt,
encoding integers as decimal strings for JSON, along with the chain ID,
transaction hash and log index. Record the operation as executed with funds in
quarantine, retaining any consumed hold, authorization or spent-note state. Keep
the payment pending until recovery establishes its outcome.

Read `open(receiptId)` before recovery. Submit `claim(receipt, destination)`
from `receipt.authority`, or the recipient under the fallback above. For an EOA,
configure the wallet client with its signing account. For a contract, simulate
and submit its supported recovery entry point so the contract calls `ReceivePolicy`
itself. Choose `destination` from the authorized recovery instruction and apply
the screening rules above. Estimate the call and save a separate signed attempt
before submission. Recover an uncertain response by that saved hash.

After finality, match `Claimed(receiptId, destination)` from `ReceivePolicy` and
`Transfer(policy, destination, receipt.amount)` from the receipt's token. Your
application marks the original payment as delivered if the destination is its
intended recipient. Otherwise, it records the refund or redirection. If
`open(receiptId)` is false before your attempt, find the existing claim and
verify its destination.

## Send-side rules

To enforce spending limits, required approvals, mandates, or destination
allowlists, hold the funds in a send policy contract. Apply the rules to every function
that can spend or authorize spending:

* Check the rule and update counters in the same call that transfers.
* Apply the rules to ERC-20 approvals and ERC-1271 authorizations as well as
  direct transfers.
* Test that a refused call leaves balances and counters unchanged.

An EOA's private key can sign a direct `transfer` or ERC-3009 authorization,
including after an EIP-7702 delegation. Hold the funds in a separately deployed
send policy contract when spending rules must also constrain the key holder.


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