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

# Build a restricted account

> Deploy a contract that holds funds and enforces incoming and outgoing transfer rules

> [!NOTE]
>
> **What you'll be able to do**
>
> * Limit incoming MIP20 transfers to configured senders and a token.
> * Deploy a RestrictedAccount to hold the funds governed by these rules.
> * Enforce outgoing destination rules, an amount limit and a spending pause.
> * Verify rejected transfers and recover credits placed in quarantine.

MIP20 calls the shared
[receive policy contract](https://docs.metalntwx.com/reference/compliance/receive-policy.md) to check the
recipient's configured rules before crediting funds. A send policy contract
checks outgoing rules before requesting a token transfer.

This guide deploys `RestrictedAccount`, a send policy contract for a MIP20 token.
At deployment, choose its controller, initial allowed sender and destination,
and maximum amount per payment. The controller submits payments, updates the
sender and destination lists, and pauses outgoing payments. Its address and
the maximum amount per payment are fixed at deployment.

Funds must be held at this contract's address for its outgoing rules to apply.
For delegated spending from Metal's smart account, use
[mandates](https://docs.metalntwx.com/agents/spending-limits.md).
ReceivePolicy checks configured incoming-transfer rules independently of that choice.

## Choose the account and rules

Verify the token and its decimals using
[Direct payment](https://docs.metalntwx.com/build/payments/direct-payment.md). Choose an account controller,
an initial allowed sender, an initial allowed destination and a maximum amount
in token base units. Use USDZ and test accounts you control for this procedure.
The account verifies each outgoing transfer by checking that its balance falls
by the requested amount and the recipient's balance rises by the same amount.

Verify that ReceivePolicy has code at
`0x0000000000000000000000000000000000000f4d` on the target network.

Each account configures its own receive policy by calling ReceivePolicy from
its address. The contract below makes those calls through its constructor and
controller-only methods. To change its rules, the controller calls the contract,
which then calls ReceivePolicy.

The constructor accepts incoming transfers only from allowed senders and in
the configured token. It selects `REVERT` refusal mode and `RECEIVER` recovery,
so ordinary refused transfers revert and the controller can recover forced
quarantine credits on the account's behalf. Outgoing payments check that the
destination receives the full amount, reverting the call if its receive policy
refuses or quarantines the credit.

## Implement the controlled account

Use the compiler and Foundry setup from
[Deploy an application contract](https://docs.metalntwx.com/build/contracts/deploy-an-application-contract.md).
Implement `RestrictedAccount` in `src/RestrictedAccount.sol`. The contract holds
funds at its deployed address and sends them through `send`, which enforces its
destination rules, amount limit and pause.

```solidity theme={null}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.31;

interface ControlledToken {
    function balanceOf(address account) external view returns (uint256);
    function transfer(address to, uint256 amount) external returns (bool);
}

interface AccountReceivePolicy {
    struct Receipt {
        uint256 nonce;
        address token;
        address originator;
        address receiver;
        address authority;
        uint256 amount;
    }
    function setPolicy(bool allowlistOnly, bool tokenFilter, uint8 refusal,
        uint8 recovery, address authority) external;
    function setAllowedSenders(address[] calldata senders, bool allowed) external;
    function setAcceptedTokens(address[] calldata tokens, bool accepted) external;
    function claim(Receipt calldata receipt, address to) external;
}

contract RestrictedAccount {
    AccountReceivePolicy public constant POLICY =
        AccountReceivePolicy(address(0xf4d));
    ControlledToken public immutable token;
    address public immutable controller;
    uint256 public immutable maximumAmount;
    mapping(address => bool) public allowedDestination;
    mapping(bytes32 => bool) public paid;
    bool public sendPaused;
    bool private entered;

    event DestinationSet(address indexed destination, bool allowed);
    event SendPaused(bool paused);
    event Sent(bytes32 indexed paymentId, address indexed destination, uint256 amount);
    error Unauthorized();
    error InvalidConfiguration();
    error PaymentRefused();
    error InexactTransfer();

    modifier onlyController() {
        if (msg.sender != controller) revert Unauthorized();
        _;
    }

    constructor(address token_, address controller_, address initialSender,
        address initialDestination, uint256 maximum_) {
        if (token_.code.length == 0 || address(POLICY).code.length == 0
            || controller_ == address(0) || initialSender == address(0)
            || initialDestination == address(0) || initialDestination == address(this)
            || maximum_ == 0) revert InvalidConfiguration();
        token = ControlledToken(token_);
        controller = controller_;
        maximumAmount = maximum_;
        allowedDestination[initialDestination] = true;
        address[] memory list = new address[](1);
        list[0] = token_;
        POLICY.setAcceptedTokens(list, true);
        list[0] = initialSender;
        POLICY.setAllowedSenders(list, true);
        POLICY.setPolicy(true, true, 0, 1, address(0));
        emit DestinationSet(initialDestination, true);
    }

    function allowSender(address sender, bool allowed) external onlyController {
        address[] memory list = new address[](1);
        list[0] = sender;
        POLICY.setAllowedSenders(list, allowed);
    }

    function allowDestination(address destination, bool allowed) external onlyController {
        if (destination == address(0) || destination == address(this)) {
            revert InvalidConfiguration();
        }
        allowedDestination[destination] = allowed;
        emit DestinationSet(destination, allowed);
    }

    function pauseSending(bool paused) external onlyController {
        sendPaused = paused;
        emit SendPaused(paused);
    }

    function send(bytes32 paymentId, address to, uint256 amount) external onlyController {
        if (entered || sendPaused || !allowedDestination[to] || paid[paymentId]
            || paymentId == bytes32(0) || amount == 0 || amount > maximumAmount) {
            revert PaymentRefused();
        }
        entered = true;
        paid[paymentId] = true;
        uint256 beforeAccount = token.balanceOf(address(this));
        uint256 beforeRecipient = token.balanceOf(to);
        if (!token.transfer(to, amount)) revert InexactTransfer();
        if (token.balanceOf(address(this)) + amount != beforeAccount
            || token.balanceOf(to) != beforeRecipient + amount) revert InexactTransfer();
        entered = false;
        emit Sent(paymentId, to, amount);
    }

    function acceptQuarantined(AccountReceivePolicy.Receipt calldata receipt)
        external onlyController {
        if (receipt.token != address(token) || receipt.receiver != address(this)
            || receipt.authority != address(this)) revert PaymentRefused();
        POLICY.claim(receipt, address(this));
    }
}
```

The policy enums use `uint8` in the ABI. `REVERT = 0` makes an ordinary refused
transfer fail without crediting the account. `RECEIVER = 1` makes the account
the recovery authority for forced quarantine receipts. The contract checks
that the genesis contract exists before configuring these rules.

Keep funds at the deployed contract address so outgoing transfers pass through
`send`. If you extend the contract, apply the same controls to any additional
spending or authorization methods. An ordinary signing account retains the
ability to spend its own balance directly, including after EIP-7702 delegation.

## Verify the rules locally

Test the account with an EVM fixture that implements the documented
ReceivePolicy behavior at `0x0000000000000000000000000000000000000f4d` and a
token that applies its screening results. Include account configuration,
sender and token filters, `REVERT` and `QUARANTINE` outcomes, forced quarantine and receipt
claims.

| Test | Required result |
| - | - |
| Allowed sender transfers the configured token | Account receives the amount |
| Sender is removed, or token is not accepted | Ordinary transfer reverts without a credit |
| Approved spender transfers from a disallowed holder | Refused based on the holder, regardless of the spender |
| Disallowed holder pays through a hold capture | Capture succeeds into quarantine, account balance stays unchanged |
| Unauthorized caller changes a list, pause or submits a payment | Reverts with rules and balances unchanged |
| Allowed destination, valid ID and amount | Exact token transfer from the account to the destination and one `Sent` event |
| Unlisted destination, paused sending, excess amount or reused ID | Reverts without moving funds or consuming a new ID |
| Allowed destination refuses by REVERT or QUARANTINE | Send reverts, preserving funds and ID and leaving no quarantine receipt |
| Account controller accepts an unclaimed quarantine receipt | Claim credits this account once, emits `Claimed` and closes the receipt |
| Wrong `token`, `receiver`, `authority` or already claimed receipt | Claim fails without moving funds |

Inbound rules apply when funds arrive, while `pauseSending` controls outgoing
calls. If you extend the administrative interface with a sender blocklist,
verify that it takes precedence over the allowlist. Refused mints and hold
captures always create quarantine receipts, regardless of the refusal mode.

## Deploy and configure

Deploy with the verified token, account controller, initial sender, initial destination
and maximum using the deployment guide. Check for code at the deployed address,
compare constructor getters with those inputs, and verify the deployment block
hash and finality. Read `policyOf(accountAddress)`,
`senderAllowed(accountAddress, initialSender)` and
`tokenAccepted(accountAddress, tokenAddress)` on ReceivePolicy. The expected
policy is `(true, true, 0, 1, address(0), false)`. The final `limited` field
is false because this example sets no receive amount or frequency limits.

To administer the account, use a wallet client connected to the controller's
signing account to call `allowSender(sender, allowed)`,
`allowDestination(destination, allowed)` or `pauseSending(paused)`. Simulate and
estimate each exact call using
[Estimate and record fees](https://docs.metalntwx.com/build/prepare/estimate-and-record-fees.md), save the signed
attempt, and verify the relevant setter event and resulting state after
finality. `allowSender` emits `SenderAllowedSet` from ReceivePolicy for the
deployed `RestrictedAccount`. Destination and pause events come from that contract.

Fund the account with a small [direct transfer](https://docs.metalntwx.com/build/payments/direct-payment.md)
from its allowed sender. The contract supplies the payment amount for outgoing
transfers. Fund the controller's signing account separately so it can pay the
transaction fees for administration and spending calls from its balance in the
gas token.

## Send and verify a controlled payment

Store a nonzero `paymentId`, destination and amount in the payment instruction.
Use the controller's signing account through `wallet`.
Load `accountAddress` from the verified deployment record. Estimate the exact
`send(paymentId, destination, amount)` call to obtain `gasLimit` and
`maxFeePerGas`.

```typescript theme={null}
import { parseAbi } from "viem"
import { signMip20Contract } from "@metalntwx/metal-ts"

const accountAbi = parseAbi([
  "function send(bytes32 paymentId, address to, uint256 amount)",
  "function paid(bytes32 paymentId) view returns (bool)",
  "event Sent(bytes32 indexed paymentId, address indexed destination, uint256 amount)",
])
const call = { address: accountAddress, abi: accountAbi,
  functionName: "send", args: [paymentId, destination, amount] } as const
await client.simulateContract({ ...call, account: wallet.account })
const signed = await signMip20Contract({
  publicClient: client, walletClient: wallet, ...call,
  gasToken: "0x000000000000000000000000000000000000b5d5",
  gasLimit, gasFallback: gasLimit, maxFeePerGas,
})
```

[Save the signed bytes and hash](https://docs.metalntwx.com/build/payments/verify-and-reconcile-payments.md#save-outgoing-attempts-before-submission)
before `await signed.send()`. Require the token's exact
`Transfer(accountAddress, destination, amount)` and the account's matching
`Sent(paymentId, destination, amount)`. Verify successful execution, the
receipt's block hash and finality before recording the payment. Use its
`paymentId` to prevent duplicate records.

## Recover refusals and forced credits

A refused ordinary credit reverts with `CreditRefused(reason)`. Record its
transaction fee and inspect `reasonFor(token, sender, accountAddress, amount)`
before changing the rule or retrying. When `send` reverts, its ID and funds
remain available, including when the destination chooses quarantine. Resolve
uncertain submissions by their saved transaction hash before retrying.

When a mint or hold capture succeeds with a refused credit, the funds go to
ReceivePolicy. Save the full quarantine receipt from `Quarantined`, verify its
`token`, `originator`, `receiver` and `amount`, and mark it pending review. To
accept it, the controller calls
`acceptQuarantined(receipt)` on the account. The account claims the funds to its
own address as the receipt's recovery authority, bypassing its inbound rules
for that reviewed credit. Verify `Claimed`, the token transfer from
ReceivePolicy to the account and finality before recording the recovery. Each
receipt can be claimed once.

Use the exact [receipt tuple and claim rules](https://docs.metalntwx.com/reference/compliance/receive-policy.md#claiming)
when encoding the call. This contract supports recovery only for its configured
token, so direct other tokens to accounts that can receive and recover them.
Preserve access to the controller's signing account for administration, spending and
receipt recovery. Complete an allowed payment and a refused transfer on the
target network to verify the deployed account's behavior.


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