Skip to main content
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 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. ReceivePolicy checks configured incoming-transfer rules independently of that choice.

Choose the account and rules

Verify the token and its decimals using Direct payment. 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. 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.
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. 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, 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 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.
Save the signed bytes and hash 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 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.