diff --git a/DESIGN_NOTES.md b/DESIGN_NOTES.md index e81a4a9..5691fcb 100644 --- a/DESIGN_NOTES.md +++ b/DESIGN_NOTES.md @@ -62,10 +62,10 @@ All B-20 tokens delegate transfer authorization to the policy engine via `transferPolicyId`. There is no internal blocklist on the token itself. Sanctions lists, KYC allowlists, jurisdiction restrictions, and similar compliance rules all live in the policy registry as -whitelist/blacklist/compound policies. +allowlist/blocklist policies. **Why.** Composability across tokens (one Coinbase-managed sanctions -blacklist policy serves every stablecoin AND every security AND every +blocklist policy serves every stablecoin AND every security AND every default token that opts in), single auditable source for compliance state, and no duplication of mechanism. CCS uses an internal blocklist; we deliberately diverge to centralize this. @@ -280,11 +280,11 @@ commitment from the issuer. Gated on a separate `redeemPolicyId` (see below). -#### Policy engine scope: TIP-403 + TIP-1015 parity, no callback or richer guards in v1 +#### Policy engine scope: TIP-403 only, no compound or richer guards in v1 We considered four levels of policy sophistication for v1: -1. **Pure set membership** (TIP-403): WHITELIST, BLACKLIST. +1. **Pure set membership** (TIP-403): ALLOWLIST, BLOCKLIST. 2. **+ Compound policies** (TIP-1015): asymmetric sender / recipient / mint-recipient slots referencing simple policies. 3. **+ Callback policies**: a fourth policy type that defers the @@ -294,20 +294,20 @@ We considered four levels of policy sophistication for v1: 4. **+ Modular guards / hooks**: the Modular ERC20 vision. Per-operation guard arrays, custom storage per guard, etc. -We ship **Levels 1 + 2 only** in v1. +We ship **Level 1 only** in v1. + +Compound policies (Level 2) were removed: the asymmetric sender / +recipient / mint-recipient slots added complexity for a use case that +can be approximated by pointing the token at a blocklist that covers +all roles uniformly, or by wrapping the precompile in a periphery +contract. The forward-compat story is the same as callback — enum +extensions are backward-compatible, so compound can be added in a +future hardfork without breaking existing consumers. The case for adding callback (Level 3) was real (richer rules without -chain bloat, small interface delta). But the forward-compat argument is -weak: even if we reserved the `CALLBACK` enum value now, the actual -implementation requires a hardfork — same as just adding it later. -Enum extensions are backward-compatible (existing values keep their -meanings), so consumers don't break when callback is added in a future -hardfork. Conclusion: defer to a future hardfork if real demand -emerges. - -The user-stories doc explicitly lists three types (allowlist, blocklist, -compound). Conner has consistently steered toward "fork Tempo cleanly." -Our v1 matches that exactly. +chain bloat, small interface delta). But it similarly requires a +hardfork and has no confirmed demand. Defer to a future hardfork if +real demand emerges. **Rules that v1 DOES NOT support and would need future work:** - Per-tx amount limits (callback signature lacks the amount) @@ -321,18 +321,21 @@ pattern; no chain change needed. #### Brokerage allowlist via separate `redeemPolicyId` Each security token holds two policy IDs: -- `transferPolicyId`: gates transfers and mints. Typically a compound - policy (e.g. KYC'd recipients, sanctions-blacklisted senders). -- `redeemPolicyId`: gates `redeem` callers. Typically a simple - whitelist of brokerage-verified accounts. Coinbase manages this list - by being the policy admin in the registry. +- `transferPolicyId` (inherited from `IB20`): gates transfers and mints. + Typically a blocklist of sanctioned addresses or an allowlist of KYC'd + accounts, depending on the issuer's compliance regime. +- `redeemPolicyId` (security-specific): gates `redeem` / `redeemWithMemo` + callers. Typically an allowlist of brokerage-verified accounts. Set to + built-in ID `0` to disable redemption entirely. + +Coinbase manages the relevant policies as the policy admin in the registry. **Why separate IDs?** Transfer-eligibility and redeem-eligibility are different sets in practice. Retail can hold and trade a tokenized security without being able to redeem to brokerage; redemption requires -KYC + brokerage account connection that not all holders have. Putting -both behind the same policy would force every holder to be brokerage- -verified. +KYC + brokerage account connection that not all holders have. A single +shared policy would force every holder to be brokerage-verified before +they could receive a transfer. #### Announcement coupling for metadata changes @@ -534,7 +537,7 @@ just promises determinism + variant-recoverability. For Default and Stablecoin, `initialSupply` is minted to `initialSupplyRecipient` atomically at creation. This bypasses BOTH the policy check (the recipient does not need to satisfy -`isAuthorizedMintRecipient` on `transferPolicyId`) AND the `MINTABLE` +`isAuthorized` on `transferPolicyId`) AND the `MINTABLE` capability check (the bootstrap mint works even on a token where `MINTABLE = false`). diff --git a/src/interfaces/IB20.sol b/src/interfaces/IB20.sol index 70b4b23..8f4d435 100644 --- a/src/interfaces/IB20.sol +++ b/src/interfaces/IB20.sol @@ -33,19 +33,15 @@ pragma solidity >=0.8.20 <0.9.0; /// Functions whose capability bit is unset revert with /// `FeatureDisabled`, regardless of role state. See `Capabilities`. /// -/// **Policy model.** Every transfer, mint, and redeem passes -/// through the token's currently-set policy ID, resolved against -/// the singleton policy registry. Transfer checks consult the -/// policy for `from`, `to`, AND `msg.sender` (the spender, when -/// distinct from `from`). Mint checks consult the policy for the -/// recipient via the mint-recipient slot of a compound policy. -/// Redeem checks consult the policy for `msg.sender` via the -/// redeemer slot of a compound policy: tokens without redemption -/// configure that slot as always-reject, making `redeem` revert -/// for every caller. Burn checks consult only the role of the -/// caller; `BURN_ROLE` plus the caller's own balance are -/// sufficient. `approve` is NOT gated by the policy (only the -/// act of MOVING balance is gated). +/// **Policy model.** Every transfer and mint passes through the +/// token's currently-set policy ID, resolved against the singleton +/// policy registry via `isAuthorized(transferPolicyId, address)`. +/// Transfer checks run for `from`, `to`, AND `msg.sender` (the +/// spender, when distinct from `from`). Mint checks run for `to` +/// (the recipient). Burn checks consult only the role of the caller; +/// `BURN_ROLE` plus the caller's own balance are sufficient. +/// `approve` is NOT gated by the policy (only the act of MOVING +/// balance is gated). /// /// **Permit.** EIP-2612 permit, EOA signatures only. ERC-1271 /// contract signatures are NOT supported on the default surface @@ -157,10 +153,6 @@ interface IB20 { /// permanent. error FeatureDisabled(uint256 capability); - /// @notice The redemption amount is below the configured - /// `minimumRedeemable` threshold. - error MinimumRedeemableNotMet(uint256 amount, uint256 minimum); - /// @notice `renounceRole(DEFAULT_ADMIN_ROLE, ...)` was called when the /// caller is the last admin. Tokens MUST always have at least /// one admin; rotate to a new admin first via `grantRole`. @@ -228,10 +220,14 @@ interface IB20 { /// @notice Emitted by `unpause`. All paused vectors are cleared. event Unpaused(address indexed updater); - /// @notice Emitted by `changeTransferPolicyId`. Includes the prior ID + /// @notice Emitted by `updateTransferPolicy`. Includes the prior ID /// for indexer convenience. event TransferPolicyUpdated(address indexed updater, uint64 oldPolicyId, uint64 newPolicyId); + /// @notice Emitted by `updateMintPolicy`. Includes the prior ID + /// for indexer convenience. + event MintPolicyUpdated(address indexed updater, uint64 oldPolicyId, uint64 newPolicyId); + /// @notice Emitted by `setSupplyCap`. Includes the prior cap for /// indexer convenience. event SupplyCapUpdated(address indexed updater, uint256 oldSupplyCap, uint256 newSupplyCap); @@ -249,18 +245,6 @@ interface IB20 { /// indexer consumption. event SymbolUpdated(address indexed updater, string newSymbol); - /// @notice Emitted by `redeem` and `redeemWithMemo` (in addition to - /// the standard `Transfer(holder, address(0), amount)`). - /// Distinguishes user-initiated redemption (which implies an - /// off-chain settlement obligation) from plain `burn`, which - /// emits the same `Transfer` event but carries no - /// off-chain meaning. - event Redeemed(address indexed holder, uint256 amount); - - /// @notice Emitted by `setMinimumRedeemable`. Includes the prior - /// minimum for indexer convenience. - event MinimumRedeemableUpdated(address indexed updater, uint256 oldMinimum, uint256 newMinimum); - /*////////////////////////////////////////////////////////////// ROLE IDENTIFIERS //////////////////////////////////////////////////////////////*/ @@ -462,54 +446,6 @@ interface IB20 { /// after the standard `Transfer` event. function burnWithMemo(uint256 amount, bytes32 memo) external; - /*////////////////////////////////////////////////////////////// - REDEEM - //////////////////////////////////////////////////////////////*/ - - /// @notice Destroys `amount` of the caller's balance, signaling an - /// off-chain redemption claim against the issuer. Subject to: - /// 1. `amount >= minimumRedeemable()` (else - /// `MinimumRedeemableNotMet(amount, minimum)`). - /// 2. `amount <= balanceOf(msg.sender)` (else - /// `InsufficientBalance(msg.sender, balance, amount)`). - /// 3. The `REDEEM` pause vector is unset (else - /// `ContractPaused(REDEEM)`). - /// 4. The active transfer policy authorizes `msg.sender` as - /// a redeemer (else `PolicyForbids(transferPolicyId)`). - /// @dev No role is required: redemption is a user-initiated - /// operation on the caller's own balance, gated entirely by - /// the policy's redeemer slot. - /// - /// Tokens that do not offer redemption configure their - /// transfer policy with the redeemer slot pointed at policy - /// ID `0` (always-reject); calls to `redeem` then revert - /// with `PolicyForbids` for every caller. The function is - /// present on every Default token but its availability is - /// policy-driven. - /// - /// Distinct from `burn` (which requires `BURN_ROLE` and - /// carries no off-chain settlement implication). Both emit - /// `Transfer(holder, address(0), amount)`; `redeem` - /// additionally emits `Redeemed(holder, amount)` so indexers - /// can distinguish. - function redeem(uint256 amount) external; - - /// @notice Same as `redeem`, with a memo. Emits `Memo(memo)` - /// immediately after the standard `Transfer` event (and - /// after `Redeemed`). - function redeemWithMemo(uint256 amount, bytes32 memo) external; - - /// @notice The minimum amount that may be redeemed in a single call - /// to `redeem` / `redeemWithMemo`. Defaults to 0 (no - /// minimum) at creation. - function minimumRedeemable() external view returns (uint256); - - /// @notice Sets a new minimum redeemable amount. Requires - /// `DEFAULT_ADMIN_ROLE`. May be set to 0 to disable the - /// minimum entirely. Takes effect immediately for the next - /// redemption. - function setMinimumRedeemable(uint256 newMinimum) external; - /*////////////////////////////////////////////////////////////// ROLES //////////////////////////////////////////////////////////////*/ @@ -596,14 +532,27 @@ interface IB20 { /// mints. Resolved against the singleton policy registry /// precompile. ID `0` always rejects (functional soft-pause /// via policy); ID `1` always allows. - function transferPolicyId() external view returns (uint64); + function transferPolicyId() external view returns (uint64 policyId); + + /// @notice Sets a new transfer policy. Requires `DEFAULT_ADMIN_ROLE`. + /// The policy MUST exist in the registry (or be one of the + /// built-in IDs `0` or `1`); otherwise reverts with + /// `PolicyNotFound`. Takes effect immediately for the next + /// transfer or mint. + function updateTransferPolicy(uint64 newPolicyId) external; + + /// @notice The policy ID currently gating this token's transfers and + /// mints. Resolved against the singleton policy registry + /// precompile. ID `0` always rejects (functional soft-pause + /// via policy); ID `1` always allows. + function mintPolicyId() external view returns (uint64 policyId); /// @notice Sets a new transfer policy. Requires `DEFAULT_ADMIN_ROLE`. /// The policy MUST exist in the registry (or be one of the /// built-in IDs `0` or `1`); otherwise reverts with /// `PolicyNotFound`. Takes effect immediately for the next /// transfer or mint. - function changeTransferPolicyId(uint64 newPolicyId) external; + function updateMintPolicy(uint64 newPolicyId) external; /*////////////////////////////////////////////////////////////// SUPPLY CAP diff --git a/src/interfaces/IB20Security.sol b/src/interfaces/IB20Security.sol index ffb9853..065f814 100644 --- a/src/interfaces/IB20Security.sol +++ b/src/interfaces/IB20Security.sol @@ -14,12 +14,18 @@ import {IB20} from "./IB20.sol"; /// @dev **Inherited surface.** `IB20` already provides the /// pieces that are shared with stablecoins and other variants: /// ERC-20 surface, mint / burn (gated by `MINT_ROLE` / `BURN_ROLE`), -/// redeem / redeemWithMemo / minimumRedeemable / setMinimumRedeemable -/// (gated by the redeemer slot of the compound transfer policy), /// pause vectors (including REDEEM at bit 3), permit, contract URI, /// supply cap, and OZ-style role management. Security tokens use /// all of these as-is and do not redeclare them here. /// +/// **Redeem surface.** Unlike IB20Stablecoin, security tokens carry +/// a full redemption surface: `redeem` / `redeemWithMemo` / +/// `minimumRedeemable` / `setMinimumRedeemable`. Redemption is +/// gated by a separate `redeemPolicyId` (distinct from +/// `transferPolicyId`) so brokerage-verified holders can redeem +/// without the transfer policy needing to authorize them for general +/// transfers. +/// /// **Security-specific additions.** This interface adds: /// 1. `announcement(...)` plus an `ANNOUNCE_ROLE` for posting /// holder-impacting disclosures (corporate actions, name @@ -85,10 +91,28 @@ interface IB20Security is IB20 { /// `identifierType` string. error InvalidIdentifierType(); + /// @notice The redemption amount is below the configured + /// `minimumRedeemable` threshold. + error MinimumRedeemableNotMet(uint256 amount, uint256 minimum); + /*////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////*/ + /// @notice Emitted by `updateRedeemPolicy`. Includes the prior ID + /// for indexer convenience. + event RedeemPolicyUpdated(address indexed updater, uint64 oldPolicyId, uint64 newPolicyId); + + /// @notice Emitted by `redeem` and `redeemWithMemo` (in addition to + /// the standard `Transfer(holder, address(0), amount)`). + /// Distinguishes user-initiated redemption (which implies an + /// off-chain settlement obligation) from plain `burn`. + event Redeemed(address indexed holder, uint256 amount); + + /// @notice Emitted by `setMinimumRedeemable`. Includes the prior + /// minimum for indexer convenience. + event MinimumRedeemableUpdated(address indexed updater, uint256 oldMinimum, uint256 newMinimum); + /// @notice A holder-impacting announcement. Posted before any /// metadata-changing operation that references the same /// `id`. @@ -109,9 +133,7 @@ interface IB20Security is IB20 { /// @notice A security identifier (ISIN, CUSIP, FIGI, etc.) was set, /// changed, or removed. `value` is the empty string on /// removal. - event SecurityIdentifierUpdated( - address indexed caller, string announcementId, string identifierType, string value - ); + event SecurityIdentifierUpdated(address indexed caller, string announcementId, string identifierType, string value); /// @notice Supply created via the compliant issuance path. event Created(address indexed to, uint256 amount); @@ -125,14 +147,11 @@ interface IB20Security is IB20 { /// @notice Per-caller create rate-limit configuration changed. event CreateRateLimitConfigured(address indexed caller, uint256 maxAmount, uint256 interval); - // NOTE on `NameUpdated` / `SymbolUpdated` / `Redeemed` / - // `MinimumRedeemableUpdated`: all four are inherited from - // `IB20` and are not redeclared here. Security - // implementations of `updateName` / `updateSymbol` emit the - // inherited `NameUpdated` / `SymbolUpdated` event after the matching - // `Announcement(id, ...)` has been emitted earlier in the - // transaction; indexers correlate the two via the shared - // transaction hash. + // NOTE on `NameUpdated` / `SymbolUpdated`: both are inherited from + // `IB20` and are not redeclared here. Security implementations of + // `updateName` / `updateSymbol` emit the inherited event after the + // matching `Announcement(id, ...)` has been emitted earlier in the + // transaction; indexers correlate the two via the shared transaction hash. /*////////////////////////////////////////////////////////////// ROLE IDENTIFIERS @@ -152,6 +171,67 @@ interface IB20Security is IB20 { /// tokens). function ISSUER_ROLE() external view returns (bytes32); + /*////////////////////////////////////////////////////////////// + POLICY + //////////////////////////////////////////////////////////////*/ + + /// @notice The policy ID currently gating this token's redemptions. + /// Checked via `isAuthorized(redeemPolicyId, msg.sender)` on + /// every call to `redeem` / `redeemWithMemo`. ID `0` always + /// rejects (disables redemption entirely); ID `1` always allows. + /// Distinct from the inherited `transferPolicyId` so brokerage- + /// verified holders can redeem without the transfer policy + /// needing to authorize them for general transfers. + function redeemPolicyId() external view returns (uint64 policyId); + + /// @notice Sets a new redeem policy. Requires `DEFAULT_ADMIN_ROLE`. + /// The policy MUST exist in the registry (or be one of the + /// built-in IDs `0` or `1`); otherwise reverts with + /// `PolicyNotFound`. Takes effect immediately for the next + /// redemption. Emits `RedeemPolicyUpdated`. + function updateRedeemPolicy(uint64 newPolicyId) external; + + /*////////////////////////////////////////////////////////////// + REDEEM + //////////////////////////////////////////////////////////////*/ + + /// @notice Destroys `amount` of the caller's balance, signaling an + /// off-chain redemption claim against the issuer. Subject to: + /// 1. `amount >= minimumRedeemable()` (else + /// `MinimumRedeemableNotMet(amount, minimum)`). + /// 2. `amount <= balanceOf(msg.sender)` (else + /// `InsufficientBalance(msg.sender, balance, amount)`). + /// 3. The `REDEEM` pause vector is unset (else + /// `ContractPaused(REDEEM)`). + /// 4. `isAuthorized(redeemPolicyId, msg.sender)` returns true + /// (else `PolicyForbids(redeemPolicyId)`). + /// @dev No role is required: redemption is a user-initiated + /// operation on the caller's own balance. Set `redeemPolicyId` + /// to the built-in ID `0` to disable redemption entirely. + /// + /// Distinct from `burn` (which requires `BURN_ROLE` and + /// carries no off-chain settlement implication). Both emit + /// `Transfer(holder, address(0), amount)`; `redeem` + /// additionally emits `Redeemed(holder, amount)` so indexers + /// can distinguish. + function redeem(uint256 amount) external; + + /// @notice Same as `redeem`, with a memo. Emits `Memo(memo)` + /// immediately after the standard `Transfer` event (and + /// after `Redeemed`). + function redeemWithMemo(uint256 amount, bytes32 memo) external; + + /// @notice The minimum amount that may be redeemed in a single call + /// to `redeem` / `redeemWithMemo`. Defaults to 0 (no + /// minimum) at creation. + function minimumRedeemable() external view returns (uint256); + + /// @notice Sets a new minimum redeemable amount. Requires + /// `DEFAULT_ADMIN_ROLE`. May be set to 0 to disable the + /// minimum entirely. Takes effect immediately for the next + /// redemption. + function setMinimumRedeemable(uint256 newMinimum) external; + /*////////////////////////////////////////////////////////////// ANNOUNCEMENTS //////////////////////////////////////////////////////////////*/ @@ -233,18 +313,15 @@ interface IB20Security is IB20 { /// @notice Cold-path batch mint. Used for unusual or emergency /// issuance (e.g. distribution of a stock dividend to many /// holders). All recipients must satisfy - /// `isAuthorizedMintRecipient` on the active transfer + /// `isAuthorized` on the active transfer /// policy. /// @dev Requires `ISSUER_ROLE` and an `Announcement(id, ...)` /// emitted earlier in the same transaction with the same /// `announcementId`. Subject to the inherited `supplyCap`. /// Reverts atomically if any single recipient fails; /// partial mints are not possible. - function adminMint( - string calldata announcementId, - address[] calldata recipients, - uint256[] calldata amounts - ) external; + function adminMint(string calldata announcementId, address[] calldata recipients, uint256[] calldata amounts) + external; /// @notice Cold-path batch burn. Used for cold-path corporate /// actions (reverse-tender settlement, mass-corrections @@ -256,11 +333,7 @@ interface IB20Security is IB20 { /// `announcementId`. Reverts atomically if any single /// account lacks sufficient balance; partial burns are not /// possible. - function adminBurn( - string calldata announcementId, - address[] calldata accounts, - uint256[] calldata amounts - ) external; + function adminBurn(string calldata announcementId, address[] calldata accounts, uint256[] calldata amounts) external; /*////////////////////////////////////////////////////////////// SECURITY IDENTIFIERS diff --git a/src/interfaces/IPolicyRegistry.sol b/src/interfaces/IPolicyRegistry.sol index b3f0733..6eeb328 100644 --- a/src/interfaces/IPolicyRegistry.sol +++ b/src/interfaces/IPolicyRegistry.sol @@ -8,12 +8,9 @@ pragma solidity >=0.8.20 <0.9.0; /// token consults the registry to determine whether the involved /// addresses are authorized. /// -/// Three policy types are supported in v1: -/// - WHITELIST: only listed addresses are authorized. -/// - BLACKLIST: all addresses except listed ones are authorized. -/// - COMPOUND: references three simple policies, one for senders, -/// one for recipients, one for mint recipients. Lets a single -/// policy ID carry asymmetric rules. +/// Two policy types are supported in v1: +/// - ALLOWLIST: only listed addresses are authorized. +/// - BLOCKLIST: all addresses except listed ones are authorized. /// /// @dev Adapted from Tempo TIP-403 + TIP-1015 with three deliberate /// omissions: no virtual-address rejection logic (no TIP-1022 on @@ -31,45 +28,21 @@ pragma solidity >=0.8.20 <0.9.0; /// that should not transfer until compliance is configured, /// and as a "kill switch" independent of pause state. /// - `1` — always-allow. All authorization queries return true. -/// Useful for tokens that opt out of compliance gating, -/// and as the identity element in compound policies. +/// Useful for tokens that opt out of compliance gating. /// /// Custom policy IDs start at 2 and are assigned monotonically by -/// `policyIdCounter`. +/// `nextPolicyId`. interface IPolicyRegistry { /*////////////////////////////////////////////////////////////// TYPES //////////////////////////////////////////////////////////////*/ /// @notice Policy type discriminator. - /// @param WHITELIST An address is authorized only if it is in the policy's set. - /// @param BLACKLIST An address is authorized unless it is in the policy's set. - /// @param COMPOUND The policy carries no member set of its own. It - /// references three simple policies and delegates the - /// per-role check. + /// @param ALLOWLIST An address is authorized only if it is in the policy's set. + /// @param BLOCKLIST An address is authorized unless it is in the policy's set. enum PolicyType { - WHITELIST, - BLACKLIST, - COMPOUND - } - - /// @notice Top-level data for any policy (simple or compound). - /// @param policyType The type of the policy. - /// @param admin The address that may modify this policy. Zero for - /// COMPOUND policies (they are structurally immutable). - struct PolicyData { - PolicyType policyType; - address admin; - } - - /// @notice Constituent policy IDs for a compound policy. - /// @param senderPolicyId Policy checked for transfer senders. - /// @param recipientPolicyId Policy checked for transfer recipients. - /// @param mintRecipientPolicyId Policy checked for mint recipients. - struct CompoundPolicyData { - uint64 senderPolicyId; - uint64 recipientPolicyId; - uint64 mintRecipientPolicyId; + ALLOWLIST, + BLOCKLIST } /*////////////////////////////////////////////////////////////// @@ -82,19 +55,11 @@ interface IPolicyRegistry { /// @notice The referenced policy ID does not exist (and is not built-in). error PolicyNotFound(); - /// @notice A compound policy attempted to reference another compound - /// policy as a constituent. Only simple policies (WHITELIST, - /// BLACKLIST) and the built-in IDs (0, 1) are valid constituents. - error PolicyNotSimple(); - /// @notice The operation is incompatible with the policy's type. For - /// example, calling `modifyPolicyWhitelist` on a BLACKLIST - /// policy, or `compoundPolicyData` on a non-COMPOUND policy. + /// example, calling `updatePolicyAllowlist` on a BLOCKLIST policy. error IncompatiblePolicyType(); - /// @notice The provided policy type value is not in the `PolicyType` - /// enum, or is not legal for the requested operation (e.g. - /// calling `createPolicy` with `COMPOUND`). + /// @notice The provided policy type value is not in the `PolicyType` enum. error InvalidPolicyType(); /// @notice A required address argument was the zero address. @@ -104,41 +69,29 @@ interface IPolicyRegistry { EVENTS //////////////////////////////////////////////////////////////*/ - /// @notice Emitted when a new simple (WHITELIST or BLACKLIST) policy is - /// created. For compound policies, see `CompoundPolicyCreated`. + /// @notice Emitted when a new policy is created. event PolicyCreated(uint64 indexed policyId, address indexed creator, PolicyType policyType); - /// @notice Emitted when a new compound policy is created. - event CompoundPolicyCreated( - uint64 indexed policyId, - address indexed creator, - uint64 senderPolicyId, - uint64 recipientPolicyId, - uint64 mintRecipientPolicyId - ); - /// @notice Emitted when a policy's admin is updated (including initial /// assignment at creation). event PolicyAdminUpdated(uint64 indexed policyId, address indexed updater, address indexed admin); - /// @notice Emitted when an account's whitelist status is updated for a - /// WHITELIST policy. - event WhitelistUpdated(uint64 indexed policyId, address indexed updater, address indexed account, bool allowed); + /// @notice Emitted when an account's status is updated for an ALLOWLIST policy. + event AllowlistUpdated(uint64 indexed policyId, address indexed updater, bool allowed, address[] account); - /// @notice Emitted when an account's blacklist status is updated for a - /// BLACKLIST policy. - event BlacklistUpdated(uint64 indexed policyId, address indexed updater, address indexed account, bool restricted); + /// @notice Emitted when an account's status is updated for a BLOCKLIST policy. + event BlocklistUpdated(uint64 indexed policyId, address indexed updater, bool blocked, address[] account); /*////////////////////////////////////////////////////////////// POLICY CREATION //////////////////////////////////////////////////////////////*/ - /// @notice Creates a new simple (WHITELIST or BLACKLIST) policy. - /// @dev Permissionless. Reverts with `InvalidPolicyType` if - /// `policyType` is `COMPOUND` (use `createCompoundPolicy`), - /// and with `ZeroAddress` if `admin` is `address(0)`. + /// @notice Creates a new policy. + /// @dev Permissionless. Reverts with `ZeroAddress` if `admin` is + /// `address(0)`, and with `InvalidPolicyType` if `policyType` + /// is not a valid `PolicyType` enum value. /// @param admin The address authorized to modify this policy. - /// @param policyType WHITELIST or BLACKLIST. + /// @param policyType ALLOWLIST or BLOCKLIST. /// @return newPolicyId The newly assigned policy ID. function createPolicy(address admin, PolicyType policyType) external returns (uint64 newPolicyId); @@ -149,96 +102,58 @@ interface IPolicyRegistry { external returns (uint64 newPolicyId); - /// @notice Creates a new compound policy referencing three constituent - /// simple policies. Compound policies are structurally - /// immutable: the constituent IDs cannot be changed after - /// creation, and there is no admin. To rotate the configuration, - /// create a new compound policy and re-point the consuming - /// token's `transferPolicyId`. - /// @dev Permissionless. Each constituent MUST exist and MUST be a - /// simple policy (WHITELIST, BLACKLIST) OR a built-in (IDs 0 - /// or 1). Reverts with `PolicyNotFound` for unknown IDs and - /// `PolicyNotSimple` if any constituent is itself COMPOUND. - function createCompoundPolicy(uint64 senderPolicyId, uint64 recipientPolicyId, uint64 mintRecipientPolicyId) - external - returns (uint64 newPolicyId); - /*////////////////////////////////////////////////////////////// POLICY ADMINISTRATION //////////////////////////////////////////////////////////////*/ - /// @notice Transfers admin rights for a simple policy. Caller must be - /// the current admin. Reverts on COMPOUND policies (they have - /// no admin). - function setPolicyAdmin(uint64 policyId, address newAdmin) external; + /// @notice Transfers admin rights for a policy. Caller must be the + /// current admin. + function updatePolicyAdmin(uint64 policyId, address newAdmin) external; - /// @notice Adds or removes an account from a WHITELIST policy. Caller - /// must be the policy admin. + /// @notice Adds or removes `accounts` from an ALLOWLIST policy, all + /// receiving the same `allowed` setting. Caller must be the + /// policy admin. /// @dev Reverts with `IncompatiblePolicyType` if the policy is not - /// WHITELIST. - function modifyPolicyWhitelist(uint64 policyId, address account, bool allowed) external; + /// ALLOWLIST. Emits one `AllowlistUpdated` event per account. + function updatePolicyAllowlist(uint64 policyId, bool allowed, address[] calldata accounts) external; - /// @notice Adds or removes an account from a BLACKLIST policy. Caller - /// must be the policy admin. + /// @notice Adds or removes `accounts` from a BLOCKLIST policy, all + /// receiving the same `blocked` setting. Caller must be the + /// policy admin. /// @dev Reverts with `IncompatiblePolicyType` if the policy is not - /// BLACKLIST. - function modifyPolicyBlacklist(uint64 policyId, address account, bool restricted) external; + /// BLOCKLIST. Emits one `BlocklistUpdated` event per account. + function updatePolicyBlocklist(uint64 policyId, bool blocked, address[] calldata accounts) external; /*////////////////////////////////////////////////////////////// AUTHORIZATION QUERIES //////////////////////////////////////////////////////////////*/ - /// @notice Composite check returning `isAuthorizedSender(p, u) && - /// isAuthorizedRecipient(p, u)`. Provided for callers that - /// want a single-call answer to "is `user` authorized for - /// both directions under this policy." + /// @notice Whether `user` is authorized under `policyId`. For an + /// ALLOWLIST policy, returns true iff `user` is in the set. + /// For a BLOCKLIST policy, returns true iff `user` is NOT in + /// the set. Built-in ID `0` always returns false; ID `1` + /// always returns true. function isAuthorized(uint64 policyId, address user) external view returns (bool); - /// @notice Whether `user` is authorized as a transfer sender under - /// `policyId`. For simple policies this is equivalent to a - /// single membership check; for compound policies it delegates - /// to the policy's `senderPolicyId`. - function isAuthorizedSender(uint64 policyId, address user) external view returns (bool); - - /// @notice Whether `user` is authorized as a transfer recipient under - /// `policyId`. For compound policies it delegates to the - /// policy's `recipientPolicyId`. - function isAuthorizedRecipient(uint64 policyId, address user) external view returns (bool); - - /// @notice Whether `user` is authorized as a mint recipient under - /// `policyId`. Distinct from `isAuthorizedRecipient` for - /// compound policies, which carry separate sender / recipient - /// / mint-recipient slots. For simple policies this returns - /// the same result as `isAuthorizedRecipient`. - function isAuthorizedMintRecipient(uint64 policyId, address user) external view returns (bool); - /*////////////////////////////////////////////////////////////// POLICY QUERIES //////////////////////////////////////////////////////////////*/ /// @notice The next policy ID that will be assigned by `createPolicy` / - /// `createPolicyWithAccounts` / `createCompoundPolicy`. Starts - /// at 2 (IDs 0 and 1 are reserved for the built-ins). - function policyIdCounter() external view returns (uint64); + /// `createPolicyWithAccounts`. Starts at 2 (IDs 0 and 1 are + /// reserved for the built-ins). + function nextPolicyId() external view returns (uint64); /// @notice Whether `policyId` exists. The built-in IDs (0, 1) always /// exist; custom IDs (>=2) exist iff they have been created. function policyExists(uint64 policyId) external view returns (bool); - /// @notice Returns the type and admin of `policyId`. - /// @dev For COMPOUND policies, `admin` is `address(0)`. For built-in - /// policies, `admin` is `address(0)` and `policyType` is - /// implementation-defined (the built-ins are not categorized as - /// WHITELIST or BLACKLIST since they have no member set). + /// @notice The type of `policyId`. /// Reverts with `PolicyNotFound` for unknown policy IDs. - function policyData(uint64 policyId) external view returns (PolicyType policyType, address admin); + function policyType(uint64 policyId) external view returns (PolicyType); - /// @notice Returns the constituent policy IDs of a compound policy. - /// @dev Reverts with `IncompatiblePolicyType` if the policy is not - /// COMPOUND, and with `PolicyNotFound` if the policy does not - /// exist. - function compoundPolicyData(uint64 policyId) - external - view - returns (uint64 senderPolicyId, uint64 recipientPolicyId, uint64 mintRecipientPolicyId); + /// @notice The admin address for `policyId`. Returns `address(0)` for + /// built-in policies (which have no admin). + /// Reverts with `PolicyNotFound` for unknown policy IDs. + function policyAdmin(uint64 policyId) external view returns (address); }