ERC-7943 (uRWA) standardised what a permissioned token exposes: canSend, canReceive, canTransfer, frozen balances and forcedTransfer. It deliberately left open how the token reaches those decisions. This proposal standardises that other half: the interface between an ERC-7943 token and an external policy contract.
Why
- Compliance logic changes faster than the token. Identity, jurisdiction, lock-ups, holder caps and settlement hours change more often than token accounting. Different teams audit them, and one rule set often serves many instruments. So tokens move these rules into a separate contract, and every implementation invents its own token↔policy interface.
- ERC-3643’s boundary is thin. Its compliance check is
canTransfer(from, to, amount) returns (bool), plus callbacks. It never learns who initiated a movement or under what authority, and it can’t say why it rejects. - Integrators can’t trust the pre-flight. They can’t know whether a token’s pre-flight is computed the same way as the transfer it predicts.
Design in brief
- Policy interface.
checkSendandcheckReceivebackcanSendandcanReceive.checkTransfer(TransferContext)decides each movement. All three are views returningbytes32.wantsAfterTransfer()andafterTransfer()serve stateful policies, such as holder counts. On the token side:policy()andPolicyChanged. - A non-zero success value.
TRANSFER_OK = keccak256("TRANSFER_OK"). Zero and every other value reject, so a forgotten or unassigned return fails closed. - A closed set of kinds, chosen by authority. Every movement carries one of
TRANSFER,EXECUTED,FORCED,MINTorBURN. An ordered procedure picks the kind from on-chain facts, meaning who authorised the movement, not function or business-process names. This comes from a real bug in the implementation the draft was extracted from: returning escrowed shares labelled as a forced transfer skipped the holder cap and the pause. - The pre-flight matches the movement. A token’s
canTransfermust return the decision that the transfer, transferFrom, mint or burn it predicts would receive, whoever submits it. To make that hold,TRANSFER,MINTandBURNdecisions may not depend on the operator, and policy answers may not depend onmsg.senderor other transaction context. - ERC-7943’s own limits apply. The policy answers
canSendandcanReceiveunder ERC-7943’s restrictions (account-level only), andcheckTransfermust stay consistent with them.
Open questions
- For the ERC-7943 authors. The draft reads
canTransferas the pre-flight for ordinary transfers, mints and burns only. Executor-settled and forced movements arrive through restricted functions and are not predicted. It also reads a zerofromortoas a mint or burn pre-flight. Does that match your intent? wantsAfterTransfer()versus ERC-165 detection of the callback. Is it worth oneSTATICCALLper movement?- The mint pre-flight sets
operatorto the caller. Decisions can’t depend on it either way. Should it usefrom(zero) instead, for a context that is the same for every caller? EXECUTED(holder-signed instructions carried out by an authorised executor): should it be in the base set, or an extension?
Links
- Draft: urwa-policy/erc/eip-draft_urwa_policy.md at main · fepvenancio/urwa-policy · GitHub
- Reference implementation: GitHub - fepvenancio/urwa-policy: urwa-policy · GitHub . It contains an ERC-20 + ERC-7943 example token and an allowlist / holder-cap policy, with 82 tests including fuzzed pre-flight parity.
Feedback is very welcome, especially from teams running ERC-7943 or ERC-3643 tokens.