Skip to main content
Some issuers need every transfer to be initiated by a designated account, even when the sender and the recipient are both eligible holders. TRANSFER_EXECUTOR_POLICY gates msg.sender on every transfer path (transfer, transferFrom, and their memo variants), separately from the sender and receiver scopes. One example is a transfer-agent model: holders approve the agent, the agent moves units with transferFrom, and a holder’s own direct transfer is denied. The same control fits any setup where an operator contract, a settlement system, or a custodian must be the one pressing the button.
Real-world asset (RWA) tokenization is one of many use cases for the B20 Asset standard. The examples on this page use a stock token for illustration; the same flows apply to other asset types.Tokenized securities examples shown for illustration. Base is a general-purpose blockchain; issuance and compliance are the responsibility of the issuer under applicable law.
New to B20? See the B20 Token Standard for the concepts and a full launch walkthrough. These samples target base-std@1505323, viem@2.55.11, and Base Foundry v1.1.1. This surface exists on both B20 Asset and B20 Stablecoin.

Before You Start

You need all of the following:
  • A B20 token you administer, with DEFAULT_ADMIN_ROLE so you can call updatePolicy.
  • A policy admin able to create and manage a policy in the Policy Registry. Token admin and policy admin are separate roles; the same account can hold both.
  • The address of each intended initiator, for example the transfer agent contract.
  • TRANSFER not paused.

Which Account Is Checked

Transfer has three independent policy scopes, all enforced on the same shared path: The executor scope checks the initiator, not the holder. On transfer, the initiator is also from. On transferFrom, the initiator is the caller, which may be a different account. There is no carve-out for a holder acting on their own behalf: once you attach a restrictive executor policy, a holder must be on it to call transfer, or to call transferFrom with themselves as from.

Allowance Is a Separate Gate

transferFrom still requires an ERC-20 allowance from from. The allowance says “this caller may spend up to this amount”; the executor policy says “this caller may initiate a transfer at all.” A transfer agent needs both: an allowance from each holder it moves units for, and a place on the executor allowlist. Granting one does not grant the other.

Create and Bind the Initiator Allowlist

Create an allowlist seeded with the initiator, then attach it to TRANSFER_EXECUTOR_POLICY:
From the updatePolicy call forward, every transfer and transferFrom on this token checks msg.sender against the allowlist. Creating the policy alone changes nothing; only attaching it does. Batches are capped at 64 accounts, and only the policy admin can change membership with updateAllowlist. Each holder then approves the initiator for the amount it will move on their behalf:
Holder approves the transfer agent
The initiator moves units with transferFrom(holder, recipient, amount). The holder’s own transfer(recipient, amount) reverts PolicyForbids(TRANSFER_EXECUTOR_POLICY, executorId) before balance is checked.
Transfer(from, to, amount) appears on the initiator’s transferFrom. A holder’s direct transfer reverts with PolicyForbids(TRANSFER_EXECUTOR_POLICY, executorId); that revert confirms the restriction is active, not that something is misconfigured. isAuthorized(executorId, account) on the Policy Registry returns true for the initiator and false for the holder.
Seed every intended initiator before binding the allowlist. Once attached, any account not on it, including the issuer, can no longer initiate transfers until it is added.

Common Errors

These errors follow the order the shared transfer path checks them.

See Also

Restrict Eligible Holders

Gate who may send and receive with the sender and receiver scopes.

Policies

How policy scopes, allowlists, and blocklists work.