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_ROLEso you can callupdatePolicy. - 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.
TRANSFERnot 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 toTRANSFER_EXECUTOR_POLICY:
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
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.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.