Sync IB20 + IPolicyRegistry interfaces with finalized PRD#14
Merged
Conversation
Major simplification of the policy registry interface per the finalized PRD decisions: Type system: - WHITELIST/BLACKLIST -> ALLOWLIST/BLOCKLIST (terminology) - COMPOUND removed entirely. Per-role asymmetric authorization moves to the token layer (multiple policy IDs per token, one per role slot); the registry stops at flat membership checks. Authorization: - isAuthorizedSender/Recipient/MintRecipient -> single isAuthorized. With no on-registry composition, role distinctions have no meaning here; tokens consult the registry per-role using different policy IDs. Membership updates (batch only): - modifyPolicyWhitelist/Blacklist -> updateAllowlist/Blocklist taking address[] of accounts plus a single bool. Single-account update is expressed as a batch of one. Emits a single event per batch. Admin model (2-step + renounce): - setPolicyAdmin (single-step) -> stageUpdateAdmin + finalizeUpdateAdmin. Two-step transfer defends against typos and key compromise: the staged candidate must actively claim the role. - New renounceAdmin (single-step) permanently freezes the policy's member set by clearing the admin slot. After renunciation the policy still answers isAuthorized but cannot be mutated. - Pending admin lives in its own mapping; pendingPolicyAdmin view exposes the in-flight state. Read surface: - policyData() (combined) -> policyType() + policyAdmin() (separate). - New pendingPolicyAdmin() view. - policyIdCounter -> nextPolicyId (clearer naming for 'the next ID that will be assigned'). Dropped from interface entirely (no longer relevant): - PolicyData struct, CompoundPolicyData struct - createCompoundPolicy, compoundPolicyData - PolicyNotSimple error, CompoundPolicyCreated event - All compound natspec Documented as future-deferred (not in v1): - Union/intersect policies (path: append PolicyType enum values + add createUnionPolicy/createIntersectPolicy creators in a future hardfork; backward-compatible since enum extension preserves existing values).
IB20 (default token):
- Add BURN_BLOCKED_ROLE + burnBlocked(from, amount) for sanctions
seizure. Function requires the target to be NOT authorized under the
active TRANSFER_SENDER policy (i.e. on the sender-side blocklist);
rejects with AccountNotBlocked(from) otherwise. Emits Transfer plus
a distinct BurnedBlocked(caller, from, amount) event so indexers can
distinguish compliance seizure from regular burn. Tokens following
'freeze, never seize' simply never grant the role.
- Replace explicit transferPolicyId / changeTransferPolicyId with a
generic policy system: policyId(bytes32) + updatePolicy(bytes32, uint64).
Five standard policy-type constants (keccak256-hashed names per OZ
AccessControl convention):
TRANSFER_SENDER — checked against from on every transfer
TRANSFER_RECEIVER — checked against to on every transfer
TRANSFER_EXECUTOR — checked against msg.sender on transferFrom
when distinct from from
MINT_RECEIVER — checked against to on every mint
REDEEMER_SENDER — checked against msg.sender on redeem (used by
variants that ship redeem, e.g. IB20Security)
Asymmetric per-role configuration is now token-side (multiple policy
IDs, one per slot) rather than registry-side (compound policies).
All slots default to ID 0 (always-reject); tokens cannot move balance
until admin configures their compliance regime.
- Remove redeem surface (redeem, redeemWithMemo, minimumRedeemable,
setMinimumRedeemable, Redeemed event, MinimumRedeemableUpdated
event, MinimumRedeemableNotMet error) — relocated to IB20Security
where it belongs semantically. The REDEEMER_SENDER policy-type
constant stays on IB20 so all B-20 tokens share a common policy-type
vocabulary.
- Replace TransferPolicyUpdated event with generic
PolicyUpdated(bytes32 policyType, uint64 oldPolicyId, uint64 newPolicyId).
- Update transfer / transferFrom / mint natspec to describe per-slot
policy checks (TRANSFER_SENDER on from, TRANSFER_RECEIVER on to,
TRANSFER_EXECUTOR on msg.sender when distinct from from,
MINT_RECEIVER on mint to).
- Refine PolicyForbids error to carry both the policy type (which slot)
and the policy ID (which registry entry).
IB20Security:
- Host the full redeem surface (redeem, redeemWithMemo, minimumRedeemable,
setMinimumRedeemable, Redeemed event, MinimumRedeemableUpdated event,
MinimumRedeemableNotMet error). Redeem now uses the inherited
REDEEMER_SENDER policy slot from IB20's generic policy system
(rather than the compound-policy redeemer slot we previously
documented). Tokens that do not offer redemption point that slot at
policy ID 0 (always-reject).
- Refresh natspec to reflect the inherited generic policy system: the
brokerage allowlist is just the policy at REDEEMER_SENDER, configured
by the admin via updatePolicy. No separate redeemPolicyId field on
the security token; it is a normal policy slot in the generic mapping.
- Document the role separation more explicitly: 'security issuers
typically do not grant MINT_ROLE; create / adminMint are the
canonical issuance paths' and analogous notes for BURN_ROLE.
REDEEMER_SENDER is only consulted by the redeem path, which lives on IB20Security. Exposing it on the base IB20 surface implied all B-20 tokens share that vocabulary, but in practice nothing on Default ever references the slot. Move the constant to where the function that uses it lives. The underlying policyId(bytes32) mapping on IB20 still accepts any bytes32 key, so this is a pure interface relocation: no change to the generic policy storage shape, no impact on how the registry is called, no breaking change for callers that read the slot via the inherited generic accessor.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Brings
IPolicyRegistry,IB20, andIB20Securityinto spec with thefinalized PRD decisions in
PRD: Native ERC20 ("B20").
Two atomic commits, build clean throughout. Interface changes only — no
implementation in this PR.
Supersedes the direction in #11 (compound policies are gone) and #7
(implementation work depends on the registry shape settling here first).
Some content overlaps with #13 but goes further: this PR adds the
2-step admin transfer + renounce path on the registry side, and on the
token side adopts the PRD's generic 5-slot policy mapping instead of
explicit
transferPolicyId/mintPolicyId/redeemPolicyIdfields.Commits
1. Overhaul
IPolicyRegistryto PRD spec (4f63440)Type system:
WHITELIST/BLACKLIST→ALLOWLIST/BLOCKLISTCOMPOUNDremoved entirely. Per-role asymmetric authorizationmoves to the token layer (see commit 2); the registry stops at
flat membership checks.
Authorization:
isAuthorizedSender/isAuthorizedRecipient/isAuthorizedMintRecipientcollapsed into a singleisAuthorized(policyId, account). With no on-registry composition,role distinctions have no meaning here.
Membership updates (batch only):
modifyPolicyWhitelist/modifyPolicyBlacklist→updateAllowlist(policyId, bool, address[])/updateBlocklist(policyId, bool, address[]). Single-account updateis a batch of one. One event per batch.
Admin model (2-step + renounce):
setPolicyAdmin(single-step) →stageUpdateAdmin+finalizeUpdateAdmin. Two-step defends against typos and keycompromise: the staged candidate must actively claim the role.
renounceAdmin(single-step) permanently freezes the policy'smember set by clearing the admin slot. After renunciation the
policy still answers
isAuthorizedbut cannot be mutated.pendingPolicyAdmin(policyId)view exposes the in-flight state.Read surface:
policyData()split intopolicyType()+policyAdmin()+new
pendingPolicyAdmin().policyIdCounter→nextPolicyId(clearer naming).Future-deferred (documented inline, not in this PR):
(
UNION_ALLOWLIST,INTERSECT_ALLOWLIST, blocklist counterparts)createUnionPolicy/createIntersectPolicycreators.Backward-compatible since enum extension preserves existing values.
2. Refactor
IB20+IB20Securityper PRD (4cda0a3)IB20 (default token):
New
BURN_BLOCKED_ROLE+burnBlocked(from, amount)forsanctions seizure. Requires the target to be NOT authorized under
the active
TRANSFER_SENDERpolicy (otherwise reverts withAccountNotBlocked(from)). Emits standardTransferplus adistinct
BurnedBlocked(caller, from, amount)event so indexerscan distinguish compliance seizure from regular burn. Tokens
following "freeze, never seize" simply never grant the role.
Replace explicit
transferPolicyId/changeTransferPolicyIdwiththe PRD's generic policy mapping:
policyId(bytes32)+updatePolicy(bytes32, uint64). Five standard policy-typeconstants (keccak256-hashed names per OZ AccessControl convention):
TRANSFER_SENDERfromon every transferTRANSFER_RECEIVERtoon every transferTRANSFER_EXECUTORmsg.senderontransferFrom(when distinct fromfrom)MINT_RECEIVERtoon every mintREDEEMER_SENDERmsg.senderonredeem(used by variants that ship redeem)Asymmetric per-role configuration is expressed by pointing
different slots at different policies (e.g. sanctions BLOCKLIST on
TRANSFER_SENDER, unrestricted always-allow onMINT_RECEIVER).All slots default to ID
0(always-reject): newly minted tokenscannot move balance until admin configures their compliance regime.
Remove redeem surface (relocated to
IB20Security). TheREDEEMER_SENDERpolicy-type constant stays onIB20so all B-20tokens share a common policy-type vocabulary.
TransferPolicyUpdatedevent replaced by genericPolicyUpdated(bytes32 indexed policyType, uint64 oldPolicyId, uint64 newPolicyId).PolicyForbidserror now carries bothpolicyType(which slotfailed) and
policyId(which registry entry it pointed at).IB20Security:
Hosts the full redeem surface (
redeem,redeemWithMemo,minimumRedeemable,setMinimumRedeemable,Redeemedevent,MinimumRedeemableUpdatedevent,MinimumRedeemableNotMeterror).redeemchecks the inheritedREDEEMER_SENDERpolicy slot — noseparate
redeemPolicyIdfield; it's just a normal slot in thegeneric mapping.
Brokerage-allowlist story documented as "point
REDEEMER_SENDERat an ALLOWLIST policy admin'd by Coinbase, which adds addresses
as users complete KYC + brokerage account connection."
Operational guidance refreshed: security issuers typically do not
grant
MINT_ROLE(usecreate/adminMintinstead) and do notgrant
BURN_ROLE(holders useredeem; admins useadminBurn).Design calls worth a second look
Two judgment calls beyond the literal PRD text:
BurnedBlocked(caller, from, amount)event added even thoughthe PRD doesn't explicitly require one. Parity with
Redeemed(both are burn variants that mean something specific to compliance
or settlement). Indexers need a way to distinguish compliance
seizure from regular burn without inspecting role membership
offchain. Open to dropping if you'd rather stay strictly
PRD-minimal.
AccountNotBlocked(address)error for the case whereburnBlockedis called against an authorized address. PRD says"Requires
!isAuthorized(transferSenderPolicy, sender)" — Iinterpreted this as "must revert if the target is authorized."
Could alternatively reuse
PolicyForbids(TRANSFER_SENDER, policyId)but that's semantically backwards (the policy isn'tforbidding anything; the operation requires the policy to forbid).
New dedicated error is clearer.
Out of scope
IB20Stablecoin(no PRD changes affect it).Capabilities.sol(may want a follow-up pass to align bits withthe new generic policy system and
BURN_BLOCKED_ROLE; not in thisPR).