diff --git a/src/interfaces/Capabilities.sol b/src/interfaces/Capabilities.sol index 4cb7184..8d28449 100644 --- a/src/interfaces/Capabilities.sol +++ b/src/interfaces/Capabilities.sol @@ -10,68 +10,73 @@ pragma solidity >=0.8.20 <0.9.0; /// top. /// @dev Bits are append-only across protocol versions. Once a bit's meaning /// is published, it cannot be reused or repurposed; new features get -/// new higher-numbered bits. Default-token bits start at `1 << 0`; -/// variants (Stablecoin, Security, ...) may define additional bits in -/// their own ranges to avoid collisions. +/// new higher-numbered bits. Default-token bits live in 0..15; +/// variants (Security 16..23, Stablecoin 24..31) define additional +/// bits in their own ranges to avoid collisions. +/// +/// Granular pause control (which operations can be paused while the +/// token is in a partial-pause state) is governed by `PauseVectors`, +/// not by capability bits. A token whose `PAUSABLE` capability is +/// unset cannot be paused at all; a token whose `PAUSABLE` is set +/// can be paused on any combination of vectors. Capability bits +/// govern whether a function exists at all; pause vectors govern +/// which existing functions are temporarily halted. library Capabilities { /*////////////////////////////////////////////////////////////// - Default token bits (0..15) + Default-token bits (0..15) //////////////////////////////////////////////////////////////*/ - /// @notice `pause()` and `unpause()` are callable. + /// @notice `pause(uint256)` and `unpause()` are callable. When unset, + /// the token can never be paused: `pause` reverts with + /// `FeatureDisabled`, `paused()` always returns 0. uint256 internal constant PAUSABLE = 1 << 0; - /// @notice `mint()` and `mintWithMemo()` are callable. - uint256 internal constant MINTABLE = 1 << 1; - - /// @notice `burn()` and `burnWithMemo()` are callable. - uint256 internal constant BURNABLE = 1 << 2; - - /// @notice `burnBlocked()` is callable. Gated separately from `BURNABLE` - /// so issuers can permit normal burns while disabling - /// compliance-style force-burns, or vice versa. - uint256 internal constant BURN_BLOCKED = 1 << 3; - - /// @notice `grantRole`, `revokeRole`, and `setRoleAdmin` are callable. - /// When unset, the role configuration written by the factory at - /// creation is permanent. Holders may still `renounceRole` - /// themselves; renunciation is always allowed. - uint256 internal constant ADMIN_MUTABLE = 1 << 4; - - /// @notice `changeTransferPolicyId()` is callable. When unset, the policy - /// ID set at creation is permanent. Note: the membership of the - /// referenced policy can still change because that is controlled - /// by the policy admin in the registry, not by the token. - uint256 internal constant POLICY_MUTABLE = 1 << 5; - - /// @notice `setSupplyCap()` is callable. When unset, the supply cap set - /// at creation is permanent. - uint256 internal constant CAP_MUTABLE = 1 << 6; - - /// @notice `setContractURI()` is callable. When unset, the contract URI - /// set at creation is permanent. - uint256 internal constant URI_MUTABLE = 1 << 7; + /// @notice `setSupplyCap(uint256)` is callable. When unset, the + /// supply cap set at creation is permanent. + uint256 internal constant CAP_MUTABLE = 1 << 1; + + // Bits 2..15 reserved for future Default-token capabilities. + // + // Note that several capability bits from earlier drafts have been + // removed because the team decided their underlying behavior should + // simply not exist in the protocol surface, OR because the desired + // guarantee can be expressed using existing primitives without a + // dedicated bit: + // - MINTABLE: removed. Tokens that want "fixed supply forever" + // achieve it by setting `supplyCap == initialSupply` at creation + // with `CAP_MUTABLE` unset; `mint` then always reverts with + // `SupplyCapExceeded`. Tokens that want "no minting right now + // but maybe later" simply leave `MINT_ROLE` ungranted. + // - BURNABLE: burn is unconditionally available; tokens that don't + // want burns simply never grant BURN_ROLE. Per the PRD, there is + // no irreversible-disable opt-out for burn at the protocol level. + // - BURN_BLOCKED: force-burn from policy-blocked addresses is not + // in the Default surface at all; sanctions seizure is a + // periphery / variant concern. + // - ADMIN_MUTABLE: role management is always available per the + // OZ AccessControl pattern adopted in this draft. + // - POLICY_MUTABLE: admin can always swap the transfer policy ID + // (the policy itself can also evolve via its own admin in the + // registry). + // - URI_MUTABLE: contract URI updates are always available to + // admin; an issuer that wants a fixed URI simply never updates it. /*////////////////////////////////////////////////////////////// - Security-token bits (16..23) + Security-variant bits (16..23) //////////////////////////////////////////////////////////////*/ /// @notice On a Security token, `create()` is callable. When unset, the - /// compliant issuance path is permanently disabled (the token's - /// supply is effectively frozen except for `adminMint` / - /// `adminBurn`, if those are also enabled). + /// compliant issuance path is permanently disabled. uint256 internal constant SECURITY_CREATABLE = 1 << 16; /// @notice On a Security token, `redeem()` is callable. When unset, /// off-chain redemption via the security-specific path is - /// permanently disabled (holders can still self-burn via the - /// inherited `burn` if `BURNABLE` is set). + /// permanently disabled. uint256 internal constant SECURITY_REDEEMABLE = 1 << 17; /// @notice On a Security token, `updateShareRatio()` is callable. When /// unset, the token-to-share ratio set at creation (typically - /// 1:1) is permanent. Useful for securities that will never - /// split (most ETFs, single-class commodities). + /// 1:1) is permanent. uint256 internal constant SHARE_RATIO_MUTABLE = 1 << 18; /// @notice On a Security token, `updateName` / `updateSymbol` / @@ -85,66 +90,56 @@ library Capabilities { uint256 internal constant SECURITY_ADMIN_BATCH = 1 << 20; /*////////////////////////////////////////////////////////////// - Stablecoin-token bits (24..31) + Stablecoin-variant bits (24..31) //////////////////////////////////////////////////////////////*/ - /// @notice On a Stablecoin token, per-minter rate limiting is enforced - /// on `mint()` / `mintWithMemo()`. `configureMinter`, - /// `grantMinterRoleWithLimit`, `currentMintLimit`, and - /// `mintRateLimitConfig` are callable. When unset, the - /// stablecoin still has `MINT_ROLE` gating but no rate limiting: - /// a holder of MINT_ROLE may mint freely up to the inherited - /// `supplyCap`. - uint256 internal constant STABLECOIN_MINT_RATE_LIMITED = 1 << 24; - - /// @notice On a Stablecoin token, ERC-3009 `transferWithAuthorization`, - /// `receiveWithAuthorization`, and `cancelAuthorization` are - /// callable. When unset, the gasless-transfer surface is - /// permanently disabled (holders fall back to ERC-20 + EIP-2612 - /// permit). - uint256 internal constant STABLECOIN_AUTHORIZATIONS = 1 << 25; + // The Stablecoin variant currently has no variant-specific + // capability bits. Bits 24..31 are reserved for future stablecoin + // additions. + // + // Earlier drafts defined STABLECOIN_MINT_RATE_LIMITED (per-minter + // rate limiting) and STABLECOIN_AUTHORIZATIONS (ERC-3009). Both + // were removed when the corresponding surface moved out of + // `IStablecoin` to EVM periphery contracts. See `IStablecoin` for + // the rationale. /*////////////////////////////////////////////////////////////// Presets //////////////////////////////////////////////////////////////*/ - /// @notice Every Default-token feature enabled. The standard configuration - /// for tokens that expect to operate under active governance: - /// stablecoins, wrapped assets, institutional-issued tokens. - uint256 internal constant ALL = type(uint256).max; + /// @notice All currently-defined optional features enabled. Useful as + /// the maximum-capability baseline for tokens under active + /// governance. + uint256 internal constant ALL = PAUSABLE | CAP_MUTABLE | SECURITY_CREATABLE | SECURITY_REDEEMABLE + | SHARE_RATIO_MUTABLE | SECURITY_METADATA_MUTABLE | SECURITY_ADMIN_BATCH; /// @notice Zero optional features. The token is a permissioned-free /// ERC-20 with permit and memo support and nothing else: no - /// admin, no pause, no further mints or burns after the initial - /// supply, no policy changes, no URI changes. Supply is whatever - /// was minted at creation, locked forever. Suitable for - /// permissionless meme coins and similar credibly-neutral tokens. + /// pause, no cap updates. Combined with + /// `supplyCap == initialSupply` at creation, this is the + /// "fixed supply forever" memecoin shape: future mints + /// always revert because the cap can never be raised. uint256 internal constant IMMUTABLE_MEMECOIN = 0; - /// @notice Admin can pause, change the transfer policy, manage roles, and - /// update the contract URI, but supply is permanently fixed (no - /// further mints or burns of any kind). Suitable for tokens with - /// a one-time issuance event followed by ongoing operational + /// @notice Pausable, permanently-fixed supply. The supply cap (set + /// to initial supply at creation) is locked and admin can + /// pause and manage roles. No further mints are possible + /// because the cap can never be raised. Suitable for tokens + /// with a one-time issuance followed by ongoing operational /// governance. - uint256 internal constant FIXED_SUPPLY = PAUSABLE | ADMIN_MUTABLE | POLICY_MUTABLE | URI_MUTABLE; + uint256 internal constant FIXED_SUPPLY = PAUSABLE; /// @notice Standard equity-style security token: supports compliant /// issuance via `create`, user redemption via `redeem`, /// share-ratio updates (for splits), all metadata updates, and - /// cold-path admin batch operations. Inherited mint/burn paths - /// are disabled in favor of the security-specific functions; - /// BURN_BLOCKED stays on for sanctions enforcement. - uint256 internal constant STANDARD_EQUITY = PAUSABLE | BURN_BLOCKED | ADMIN_MUTABLE | POLICY_MUTABLE | CAP_MUTABLE - | URI_MUTABLE | SECURITY_CREATABLE | SECURITY_REDEEMABLE | SHARE_RATIO_MUTABLE | SECURITY_METADATA_MUTABLE - | SECURITY_ADMIN_BATCH; - - /// @notice Standard payment-rail stablecoin: supports rate-limited mint - /// (per-minter quotas), burn, ERC-3009 gasless transfers, pause, - /// and full admin / policy / URI mutability. Does NOT include - /// BURN_BLOCKED (matches the "freeze, never seize" philosophy - /// of CDP Custom Stablecoin and similar). Issuers who want - /// force-burn for sanctions enforcement can OR `BURN_BLOCKED` - /// in at creation. - uint256 internal constant STANDARD_STABLECOIN = PAUSABLE | MINTABLE | BURNABLE | ADMIN_MUTABLE | POLICY_MUTABLE - | CAP_MUTABLE | URI_MUTABLE | STABLECOIN_MINT_RATE_LIMITED | STABLECOIN_AUTHORIZATIONS; + /// cold-path admin batch operations. Pausable; supply cap + /// mutable. + uint256 internal constant STANDARD_EQUITY = PAUSABLE | CAP_MUTABLE | SECURITY_CREATABLE | SECURITY_REDEEMABLE + | SHARE_RATIO_MUTABLE | SECURITY_METADATA_MUTABLE | SECURITY_ADMIN_BATCH; + + /// @notice Standard payment-rail stablecoin: pausable, supply cap + /// mutable. Per-minter rate limiting and ERC-3009 are not + /// on the protocol surface; issuers add them via periphery + /// contracts that hold `MINT_ROLE` on the precompile. + uint256 internal constant STANDARD_STABLECOIN = PAUSABLE | CAP_MUTABLE; } diff --git a/src/interfaces/IDefaultToken.sol b/src/interfaces/IDefaultToken.sol index a03ab13..f2cdbda 100644 --- a/src/interfaces/IDefaultToken.sol +++ b/src/interfaces/IDefaultToken.sol @@ -6,158 +6,408 @@ pragma solidity >=0.8.20 <0.9.0; /// Variants (Stablecoin, Security, ...) extend this interface; nothing /// on this surface is variant-specific. A token created at the Default /// variant address presents exactly this interface. +/// /// @dev Backward-compatible with ERC-20 at the function-selector level: /// `transfer`, `transferFrom`, `approve`, `balanceOf`, `allowance`, -/// `totalSupply`, `name`, `symbol`, `decimals` all match ERC-20 selectors. -/// Memo'd siblings live alongside, and their existence does not change -/// the ERC-20 selectors any wallet or contract already expects. +/// `totalSupply`, `name`, `symbol`, `decimals` all match ERC-20 +/// selectors and event signatures. Memo'd siblings live alongside, +/// and their existence does not change the ERC-20 selectors any +/// wallet or contract already expects. +/// +/// **Role model.** Standard OpenZeppelin AccessControl semantics: +/// five named roles (`DEFAULT_ADMIN_ROLE`, `MINT_ROLE`, `BURN_ROLE`, +/// `PAUSE_ROLE`, `UNPAUSE_ROLE`) plus arbitrary user-defined roles. +/// `grantRole`, `revokeRole`, `renounceRole`, and `setRoleAdmin` +/// work uniformly across all roles. The only protocol-level +/// constraint is that the LAST holder of `DEFAULT_ADMIN_ROLE` +/// cannot renounce: the token must always have at least one admin. +/// +/// **Pause model.** Pause is granular: `pause(uint256 vectors)` +/// accepts a bitmask indicating which classes of operation to halt +/// (transfer, mint, burn, ...). Multiple `pause` calls are +/// additive. `unpause()` clears all paused vectors at once. See +/// `PauseVectors` for the bit definitions. +/// +/// **Capability bits.** Every token's optional features are gated +/// by an immutable `capabilities()` bitfield set at creation. +/// 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). /// -/// Every token's optional features are gated by an immutable -/// `capabilities()` bitfield set at creation. Functions whose -/// capability bit is unset revert with `FeatureDisabled` regardless -/// of role state. See `Capabilities` for the bit definitions. +/// **Permit.** EIP-2612 permit, EOA signatures only. ERC-1271 +/// contract signatures are NOT supported on the default surface +/// (smart-contract accounts use call-batching or paymaster flows +/// instead to set allowances). EIP-712 domain is +/// `(chainId, verifyingContract)` only, with `name` and `version` +/// empty. ERC-5267 `eip712Domain()` is exposed for domain +/// introspection by integrators. interface IDefaultToken { /*////////////////////////////////////////////////////////////// ERRORS //////////////////////////////////////////////////////////////*/ + /// @notice `account` does not hold `neededRole`. Used for ALL + /// role-based access checks: function-level role gates + /// (`MINT_ROLE`, `BURN_ROLE`, etc.), `grantRole` / + /// `revokeRole` when the caller does not hold the admin role + /// for the target role, and `setRoleAdmin` when the caller + /// does not hold the current admin role for the target role. + /// @dev Matches OZ AccessControl's `AccessControlUnauthorizedAccount` + /// error exactly. Since `getRoleAdmin(role)` defaults to + /// `DEFAULT_ADMIN_ROLE` for any role that has not had a + /// custom admin set, calls like + /// `grantRole(SOME_UNREGISTERED_ROLE, alice)` revert with + /// `neededRole == DEFAULT_ADMIN_ROLE` rather than a + /// "role does not exist" error: every `bytes32` is a valid + /// role identifier in this model. + error AccessControlUnauthorizedAccount(address account, bytes32 neededRole); + + /// @notice Caller failed a positional authorization check that is NOT + /// expressible as "missing role X" (e.g. caller must be the + /// specific account whose state is being mutated, in + /// contexts where no role applies). Used sparingly; most + /// authorization failures revert with + /// `AccessControlUnauthorizedAccount`. error Unauthorized(); - error ContractPaused(); - error AlreadyPaused(); - error NotPaused(); - error InsufficientAllowance(); - error InsufficientBalance(uint256 currentBalance, uint256 requestedAmount); + + /// @notice One or more pause vectors covering this operation are + /// currently set. `pausedVector` is the specific vector that + /// blocked the call. + error ContractPaused(uint256 pausedVector); + + /// @notice `spender`'s allowance from the relevant token owner is + /// less than `needed` for the requested `transferFrom`. + /// @dev Matches OZ ERC20 / ERC-6093 exactly. + error InsufficientAllowance(address spender, uint256 allowance, uint256 needed); + + /// @notice `sender`'s balance is less than `needed` for the requested + /// transfer, burn, or redeem. + /// @dev Matches OZ ERC20 / ERC-6093 exactly. + error InsufficientBalance(address sender, uint256 balance, uint256 needed); + + /// @notice The transfer's source address is invalid (typically + /// `address(0)`, which cannot hold balance to send from). + /// @dev Matches OZ ERC20 / ERC-6093 exactly. + error InvalidSender(address sender); + + /// @notice The transfer's destination address is invalid (typically + /// `address(0)`; mints and burns use the literal zero + /// address as the from/to and are not subject to this check). + /// @dev Matches OZ ERC20 / ERC-6093 exactly. + error InvalidReceiver(address receiver); + + /// @notice The approval's `owner` address is invalid (typically + /// `address(0)`). + /// @dev Matches OZ ERC20 / ERC-6093 exactly. + error InvalidApprover(address approver); + + /// @notice The approval's `spender` address is invalid (typically + /// `address(0)`). + /// @dev Matches OZ ERC20 / ERC-6093 exactly. + error InvalidSpender(address spender); + + /// @notice An amount argument was zero where a non-zero value is + /// required (e.g. `pause(0)`). NOT used for ERC-20 amount + /// arguments: per OZ / ERC-6093, ERC-20 functions do not + /// validate `amount > 0`. error InvalidAmount(); - error InvalidRecipient(); - error InvalidSupplyCap(); - error SupplyCapExceeded(); - error PolicyForbids(); - error ProtectedAddress(); - error PermitExpired(); - error InvalidSignature(); + + /// @notice The proposed supply cap is below the current `totalSupply`, + /// which would invalidate already-issued supply. + error InvalidSupplyCap(uint256 currentSupply, uint256 proposedCap); + + /// @notice The mint would push `totalSupply` past the configured cap. + error SupplyCapExceeded(uint256 cap, uint256 attempted); + + /// @notice The active transfer policy denied the operation. `policyId` + /// is the ID currently set as `transferPolicyId`. + error PolicyForbids(uint64 policyId); + + /// @notice The provided policy ID does not exist in the policy + /// registry. + error PolicyNotFound(uint64 policyId); + + /// @notice An EIP-2612 `permit` was submitted with a `deadline` + /// strictly less than the current block timestamp. + /// @dev Matches OZ ERC20Permit's `ExpiredSignature` error + /// exactly. + error ExpiredSignature(uint256 deadline); + + /// @notice ECDSA recovery on an EIP-2612 `permit` returned `signer`, + /// which does not match the claimed `owner`. + /// @dev Matches OZ ERC20Permit's `InvalidSigner` error + /// exactly. + error InvalidSigner(address signer, address owner); + + /// @notice The capability bit for this operation is not set on the + /// token. Capability state is immutable; this revert is + /// permanent. error FeatureDisabled(uint256 capability); - error EnforcedDefaultAdminRules(); - error InvalidDefaultAdmin(address account); - error EnforcedDefaultAdminDelay(uint48 schedule); - error NoPendingDefaultAdmin(); + + /// @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`. + error LastAdminCannotRenounce(); + + /// @notice The `callerConfirmation` argument to `renounceRole` was not + /// `msg.sender`. This guard prevents accidental renunciation + /// caused by a fat-fingered call to a different account's + /// role. + /// @dev Matches OZ AccessControl's `AccessControlBadConfirmation` + /// error exactly. + error AccessControlBadConfirmation(); /*////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////*/ + /// @notice ERC-20 standard transfer event. Emitted on every successful + /// transfer (including memo'd variants), mint + /// (`from = address(0)`), and burn (`to = address(0)`). event Transfer(address indexed from, address indexed to, uint256 amount); - event Approval(address indexed owner, address indexed spender, uint256 amount); - - event TransferWithMemo(address indexed from, address indexed to, uint256 amount, bytes32 indexed memo); - event Mint(address indexed to, uint256 amount); - event MintWithMemo(address indexed to, uint256 amount, bytes32 indexed memo); - event Burn(address indexed from, uint256 amount); - event BurnWithMemo(address indexed from, uint256 amount, bytes32 indexed memo); - event BurnBlocked(address indexed from, uint256 amount); - - event RoleMembershipUpdated(bytes32 indexed role, address indexed account, address indexed sender, bool member); - event RoleAdminUpdated(bytes32 indexed role, bytes32 indexed newAdminRole, address indexed sender); + /// @notice ERC-20 standard approval event. + event Approval(address indexed owner, address indexed spender, uint256 amount); - event PauseStateUpdated(address indexed updater, bool isPaused); + /// @notice Emitted by `transferWithMemo`, `transferFromWithMemo`, + /// `mintWithMemo`, and `burnWithMemo` immediately AFTER the + /// underlying ERC-20 `Transfer` event. The memo carries no + /// from/to/amount fields; indexers join it to the preceding + /// `Transfer` log via `(transactionHash, logIndex - 1)`. + /// @dev Variants may emit this event from additional functions + /// (e.g. `redeem` on a Security token); the event signature + /// is shared. + event Memo(bytes32 indexed memo); + + /// @notice Emitted when `account` is granted `role`. `sender` is the + /// account that originated the call (the admin for `role`, + /// or the same as `account` if the grant happens via factory + /// init or other internal path). + /// @dev Matches OZ AccessControl's `RoleGranted` event exactly. + event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender); + + /// @notice Emitted when `role` is revoked from `account`. `sender` + /// is the account that originated the call: + /// - if via `revokeRole`, it is the admin role bearer + /// - if via `renounceRole`, it is the role bearer (i.e. + /// `account`) + /// @dev Matches OZ AccessControl's `RoleRevoked` event exactly. + event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender); + + /// @notice Emitted when the admin role for `role` is changed via + /// `setRoleAdmin`. `DEFAULT_ADMIN_ROLE` is the implicit + /// starting admin for all roles, despite this event NOT + /// being emitted to signal that initial state. + /// @dev Matches OZ AccessControl's `RoleAdminChanged` event + /// exactly. Note OZ does NOT include a `sender` parameter + /// here; this is intentional alignment. + event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole); + + /// @notice Emitted by `pause`. `vectors` is the bitmask added to the + /// current paused state (the result of `current | vectors`, + /// not the argument). `updater` is the caller. + event Paused(address indexed updater, uint256 vectors); + + /// @notice Emitted by `unpause`. All paused vectors are cleared. + event Unpaused(address indexed updater); + + /// @notice Emitted by `changeTransferPolicyId`. Includes the prior ID + /// for indexer convenience. + event TransferPolicyUpdated(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); + + /// @notice Emitted by `setContractURI`. Per ERC-7572, this event is + /// intentionally parameterless: integrators re-fetch + /// `contractURI()` after seeing it. + event ContractURIUpdated(); - event TransferPolicyUpdated(address indexed updater, uint64 indexed newPolicyId); + /// @notice Emitted by `setName`. Carries the new name string for + /// indexer consumption. + event NameUpdated(address indexed updater, string newName); - event SupplyCapUpdated(address indexed updater, uint256 newSupplyCap); + /// @notice Emitted by `setSymbol`. Carries the new symbol string for + /// indexer consumption. + event SymbolUpdated(address indexed updater, string newSymbol); - event ContractURIUpdated(); + /// @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); - event DefaultAdminTransferScheduled(address indexed newAdmin, uint48 acceptSchedule); - event DefaultAdminTransferCanceled(); - event DefaultAdminDelayChangeScheduled(uint48 newDelay, uint48 effectSchedule); - event DefaultAdminDelayChangeCanceled(); + /// @notice Emitted by `setMinimumRedeemable`. Includes the prior + /// minimum for indexer convenience. + event MinimumRedeemableUpdated(address indexed updater, uint256 oldMinimum, uint256 newMinimum); /*////////////////////////////////////////////////////////////// ROLE IDENTIFIERS //////////////////////////////////////////////////////////////*/ - /// @notice The default top-level admin role. The admin manages all other - /// roles. The DEFAULT_ADMIN_ROLE itself can only be transferred via - /// the two-step `beginDefaultAdminTransfer` / `acceptDefaultAdminTransfer` - /// flow with a configurable delay; `grantRole` and `revokeRole` - /// REVERT when called for `DEFAULT_ADMIN_ROLE`. + /// @notice The default top-level admin role, equal to `bytes32(0)` per + /// the OpenZeppelin AccessControl convention. The admin + /// manages all other roles via `grantRole`, `revokeRole`, and + /// `setRoleAdmin`. The admin can also `changeTransferPolicyId`, + /// `setSupplyCap`, `setContractURI`, `setName`, and `setSymbol`. + /// @dev Unlike earlier drafts, there is NO two-step delay-protected + /// transfer for this role. `grantRole(DEFAULT_ADMIN_ROLE, ...)` + /// and `revokeRole(DEFAULT_ADMIN_ROLE, ...)` work uniformly. + /// The only constraint is that the last admin cannot renounce + /// (see `LastAdminCannotRenounce`). function DEFAULT_ADMIN_ROLE() external view returns (bytes32); - /// @notice Required to call `mint` / `mintWithMemo`. Held separately from - /// BURN_ROLE so issuance and destruction authority can be split - /// across teams (e.g. treasury team mints, redemption team burns). + /// @notice Required to call `mint` and `mintWithMemo`. Held separately + /// from `BURN_ROLE` so issuance and destruction authority can + /// be split across teams (e.g. treasury team mints, redemption + /// team burns). function MINT_ROLE() external view returns (bytes32); - /// @notice Required to call `burn` / `burnWithMemo`. See MINT_ROLE for - /// rationale on the split. + /// @notice Required to call `burn` and `burnWithMemo`. Note that burn + /// operates on the caller's own balance only; there is no + /// force-burn function on the Default surface. function BURN_ROLE() external view returns (bytes32); - /// @notice Required to call `pause`. Held separately from UNPAUSE_ROLE so - /// emergency-stop authority can be delegated to a 24/7 ops team - /// without also granting unpause authority. + /// @notice Required to call `pause`. Held separately from + /// `UNPAUSE_ROLE` so emergency-stop authority can be delegated + /// to a 24/7 ops team without also granting unpause authority. function PAUSE_ROLE() external view returns (bytes32); - /// @notice Required to call `unpause`. See PAUSE_ROLE for rationale on the split. + /// @notice Required to call `unpause`. Distinct from `PAUSE_ROLE` so + /// resumption requires a deliberate, typically more senior + /// action than the pause itself. function UNPAUSE_ROLE() external view returns (bytes32); - /// @notice Required to call `burnBlocked`. Holders may force-burn balance - /// from an address that is currently not authorized as a sender by - /// the active transfer policy. - function BURN_BLOCKED_ROLE() external view returns (bytes32); - /*////////////////////////////////////////////////////////////// CAPABILITIES //////////////////////////////////////////////////////////////*/ - /// @notice The immutable feature bitfield assigned at creation. Each bit - /// indicates that the corresponding optional function CAN be - /// called on this token. Bits not set here mean the corresponding - /// function reverts with `FeatureDisabled`, permanently. See - /// `Capabilities` for the bit definitions. + /// @notice The immutable feature bitfield assigned at creation. Each + /// bit indicates that the corresponding optional function CAN + /// be called on this token. Bits not set here mean the + /// corresponding function reverts with `FeatureDisabled`, + /// permanently. See `Capabilities` for the bit definitions. function capabilities() external view returns (uint256); - /// @notice Convenience views for individual capability bits. Each returns - /// `(capabilities() & Capabilities.X) != 0`. + /// @notice Convenience view: `(capabilities() & Capabilities.PAUSABLE) != 0`. function isPausable() external view returns (bool); - function isMintable() external view returns (bool); - function isBurnable() external view returns (bool); - function isBurnBlockedEnabled() external view returns (bool); - function isAdminMutable() external view returns (bool); - function isPolicyMutable() external view returns (bool); + + /// @notice Convenience view: `(capabilities() & Capabilities.CAP_MUTABLE) != 0`. function isCapMutable() external view returns (bool); - function isURIMutable() external view returns (bool); /*////////////////////////////////////////////////////////////// ERC-20 //////////////////////////////////////////////////////////////*/ + /// @notice Token name. Set at creation, mutable via `setName`. function name() external view returns (string memory); + /// @notice Token symbol. Set at creation, mutable via `setSymbol`. function symbol() external view returns (string memory); + /// @notice Number of decimal places. Set at creation, immutable + /// thereafter. The factory determines whether `decimals` is + /// a per-token parameter or a fixed value (the choice is a + /// factory concern, not a token concern). function decimals() external view returns (uint8); + /// @notice Total token supply currently in circulation. function totalSupply() external view returns (uint256); + /// @notice Balance of `account`. function balanceOf(address account) external view returns (uint256); + /// @notice Allowance granted by `owner` to `spender`. function allowance(address owner, address spender) external view returns (uint256); + /// @notice Transfers `amount` from `msg.sender` to `to`. Reverts with: + /// - `ContractPaused(TRANSFER)` if the `TRANSFER` pause vector + /// is set. + /// - `PolicyForbids(transferPolicyId)` if the active transfer + /// policy denies the transfer. + /// - `InsufficientBalance(msg.sender, balance, amount)` + /// if the caller does not have enough balance. + /// - `InvalidReceiver(to)` if `to == address(0)`. + /// @dev Policy check evaluates `msg.sender` (the sender of value) + /// and `to` (the recipient). When the token is configured as + /// a gas asset, fee debits go through this same path. function transfer(address to, uint256 amount) external returns (bool); + /// @notice Transfers `amount` from `from` to `to` using `msg.sender`'s + /// allowance. Reverts as `transfer` does, plus: + /// - `InsufficientAllowance(msg.sender, allowance, amount)` + /// if the caller does not have enough allowance from `from`. + /// - `InvalidSender(from)` if `from == address(0)`. + /// @dev Policy check evaluates `from` (the sender of value), `to` + /// (the recipient), AND `msg.sender` (the spender, when + /// distinct from `from`). A sanctioned spender cannot move + /// tokens for a non-sanctioned holder. function transferFrom(address from, address to, uint256 amount) external returns (bool); + /// @notice Sets `spender`'s allowance to `amount`. NOT gated by the + /// transfer policy or by pause; only the act of MOVING balance + /// is gated. A user on the policy blocklist may still + /// `approve` (the approval cannot be acted on by the spender, + /// since `transferFrom` would revert). + /// @dev Reverts with `InvalidApprover(msg.sender)` if the + /// caller is `address(0)` (theoretically unreachable for + /// normal callers but enforced for parity with OZ ERC20), + /// and `InvalidSpender(spender)` if + /// `spender == address(0)`. function approve(address spender, uint256 amount) external returns (bool); + /*////////////////////////////////////////////////////////////// + METADATA UPDATES + //////////////////////////////////////////////////////////////*/ + + /// @notice Updates the token's `name`. Requires `DEFAULT_ADMIN_ROLE`. + /// No length restrictions. Emits `NameUpdated`. + /// @dev Several customers (Coinbase Tokenized Equities, Coinbase + /// Wrapped Assets) need the ability to update name and symbol + /// post-deployment for re-branding or legal-restructuring + /// events. There is no capability bit for this; tokens that + /// do not want to update their name simply never call this + /// function. + function setName(string calldata newName) external; + + /// @notice Updates the token's `symbol`. Requires `DEFAULT_ADMIN_ROLE`. + /// No length restrictions. Emits `SymbolUpdated`. + function setSymbol(string calldata newSymbol) external; + /*////////////////////////////////////////////////////////////// MEMO TRANSFER VARIANTS //////////////////////////////////////////////////////////////*/ - /// @notice Same as `transfer`, but additionally emits `TransferWithMemo` - /// carrying a 32-byte caller-supplied memo. The standard - /// `Transfer` event is also emitted for ERC-20 indexer compat. - /// @dev A memo of `bytes32(0)` is permitted; it indicates "no memo" - /// while still emitting the memo event. + /// @notice Same as `transfer`, but additionally emits `Memo(memo)` + /// immediately after the standard `Transfer` event. The + /// standard `Transfer` event is also emitted for ERC-20 + /// indexer compatibility. + /// @dev The memo event carries only the memo. Indexers join it to + /// the preceding `Transfer` log via + /// `(transactionHash, logIndex - 1)`. Same access control and + /// policy checks as `transfer`. A memo of `bytes32(0)` is + /// permitted; it indicates "no memo" while still emitting the + /// memo event. function transferWithMemo(address to, uint256 amount, bytes32 memo) external returns (bool); /// @notice Same as `transferFrom`, with a memo. See `transferWithMemo`. @@ -167,183 +417,191 @@ interface IDefaultToken { MINT / BURN //////////////////////////////////////////////////////////////*/ - /// @notice Mints `amount` to `to`. Requires `MINTABLE` capability and - /// `MINT_ROLE`. Subject to the token's transfer policy: the - /// recipient must satisfy `isAuthorizedMintRecipient` on the - /// active policy. - /// @dev Emits both `Transfer(address(0), to, amount)` (ERC-20) and - /// `Mint(to, amount)`. + /// @notice Mints `amount` to `to`. Requires `MINT_ROLE`. Subject to: + /// 1. `totalSupply + amount <= supplyCap` (else + /// `SupplyCapExceeded`). + /// 2. The `MINT` pause vector is unset (else + /// `ContractPaused(MINT)`). + /// 3. The active transfer policy authorizes `to` as a mint + /// recipient (else `PolicyForbids`). + /// @dev There is no `MINTABLE` capability bit. To make a token + /// permanently fixed-supply, set `supplyCap == initialSupply` + /// at creation with `CAP_MUTABLE` unset; future mint calls + /// will revert with `SupplyCapExceeded` because the cap can + /// never be raised. To pause minting temporarily, set the + /// `MINT` pause vector or revoke `MINT_ROLE`. + /// + /// Per-minter rate limiting is NOT enshrined at any level + /// (Default or variant). Minter quotas live in EVM + /// periphery contracts: a controller / wrapper that holds + /// `MINT_ROLE` and enforces per-caller quotas before + /// invoking `mint` on the precompile. Bridge's + /// `TIP20Controller` and CDP Custom Stablecoin's mint flow + /// are both expressible this way. + /// Emits `Transfer(address(0), to, amount)`. function mint(address to, uint256 amount) external; - /// @notice Same as `mint`, with a 32-byte memo. Emits `MintWithMemo` in - /// addition to `Mint` and `Transfer`. + /// @notice Same as `mint`, with a memo. Emits `Memo(memo)` immediately + /// after the standard `Transfer` event. function mintWithMemo(address to, uint256 amount, bytes32 memo) external; - /// @notice Burns `amount` from the caller's balance. Requires `BURNABLE` - /// capability and `BURN_ROLE`. - /// @dev Emits both `Transfer(caller, address(0), amount)` and - /// `Burn(caller, amount)`. + /// @notice Burns `amount` from the caller's own balance. Requires + /// `BURN_ROLE`. Subject to the `BURN` pause vector being unset + /// (else `ContractPaused(BURN)`). NOT subject to the transfer + /// policy: burn destroys the caller's own supply with no + /// recipient. Reverts with + /// `InsufficientBalance(caller, balance, amount)` if the + /// caller does not have enough balance. + /// @dev There is no force-burn function on the Default surface. + /// Sanctions seizure flows live in token variants (e.g. + /// Security via `adminBurn`) or in periphery contracts. + /// Emits `Transfer(caller, address(0), amount)`. function burn(uint256 amount) external; - /// @notice Same as `burn`, with a 32-byte memo. Emits `BurnWithMemo` in - /// addition to `Burn` and `Transfer`. + /// @notice Same as `burn`, with a memo. Emits `Memo(memo)` immediately + /// after the standard `Transfer` event. function burnWithMemo(uint256 amount, bytes32 memo) external; - /// @notice Force-burns `amount` from an address that is currently NOT - /// authorized as a sender by the active transfer policy. Used for - /// sanctions seizures and similar compliance enforcement. - /// @dev Requires `BURN_BLOCKED` capability and `BURN_BLOCKED_ROLE`. - /// Reverts with `ProtectedAddress` if `from` IS authorized to - /// send under the active policy (i.e. only blocked addresses can - /// be force-burned). Emits `Transfer(from, address(0), amount)` - /// and `BurnBlocked(from, amount)`. - function burnBlocked(address from, uint256 amount) 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 //////////////////////////////////////////////////////////////*/ - /// @notice Returns whether `account` is a member of `role`. `role` may be - /// any `bytes32` value; user-defined roles are supported and have - /// no built-in effect on the token's own functions but may be - /// consumed by external contracts. + /// @notice Returns whether `account` is a member of `role`. `role` may + /// be any `bytes32` value; user-defined roles are supported and + /// have no built-in effect on the token's own functions but + /// may be consumed by external contracts. function hasRole(bytes32 role, address account) external view returns (bool); - /// @notice Returns the role required to grant or revoke `role`. Defaults - /// to `DEFAULT_ADMIN_ROLE` if not explicitly set via `setRoleAdmin`. + /// @notice Returns the role required to grant or revoke `role`. + /// Defaults to `DEFAULT_ADMIN_ROLE` if not explicitly set via + /// `setRoleAdmin`. function getRoleAdmin(bytes32 role) external view returns (bytes32); - /// @notice Grants `role` to `account`. Requires `ADMIN_MUTABLE` - /// capability and the admin role for `role` (see `getRoleAdmin`). - /// @dev REVERTS with `EnforcedDefaultAdminRules` when `role` is - /// `DEFAULT_ADMIN_ROLE`. Use `beginDefaultAdminTransfer` for - /// that role. + /// @notice Grants `role` to `account`. Caller MUST hold the admin + /// role for `role` (see `getRoleAdmin`). function grantRole(bytes32 role, address account) external; - /// @notice Revokes `role` from `account`. Requires `ADMIN_MUTABLE` - /// capability and the admin role for `role`. - /// @dev REVERTS with `EnforcedDefaultAdminRules` when `role` is - /// `DEFAULT_ADMIN_ROLE`. The default admin can only voluntarily - /// exit via `renounceRole`. + /// @notice Revokes `role` from `account`. Caller MUST hold the admin + /// role for `role`. function revokeRole(bytes32 role, address account) external; - /// @notice Caller revokes `role` from themselves. Always permitted, even - /// when `ADMIN_MUTABLE` is unset, so role holders can voluntarily - /// exit a frozen role configuration. - /// @dev Permitted for `DEFAULT_ADMIN_ROLE` and ALSO subject to the - /// configured `defaultAdminDelay`: a default-admin renunciation - /// is scheduled and only takes effect after the delay elapses. - /// Implementations should use the same scheduling machinery as - /// `beginDefaultAdminTransfer` (with `newAdmin == address(0)`). - function renounceRole(bytes32 role) external; - - /// @notice Sets the admin role for `role`. Requires `ADMIN_MUTABLE` - /// capability and the current admin role for `role`. - /// @dev REVERTS with `EnforcedDefaultAdminRules` when `role` is - /// `DEFAULT_ADMIN_ROLE`; the default admin's admin role is - /// always itself. + /// @notice Caller renounces `role` for themselves. Always permitted + /// (no admin authorization needed). + /// @dev `callerConfirmation` MUST equal `msg.sender`; otherwise + /// reverts with `AccessControlBadConfirmation`. This guard + /// prevents a fat-fingered call from accidentally renouncing + /// for a different account. + /// + /// Reverts with `LastAdminCannotRenounce` if `role` is + /// `DEFAULT_ADMIN_ROLE` and `msg.sender` is the only current + /// admin: the token must always have at least one admin. + /// Rotate to a new admin first via `grantRole`, then + /// renounce. + function renounceRole(bytes32 role, address callerConfirmation) external; + + /// @notice Sets the admin role for `role`. Caller MUST hold the + /// current admin role for `role`. Useful for delegating role + /// management to a different role hierarchy. function setRoleAdmin(bytes32 role, bytes32 newAdminRole) external; - /*////////////////////////////////////////////////////////////// - TWO-STEP DEFAULT ADMIN - //////////////////////////////////////////////////////////////*/ - - /// @notice The current holder of `DEFAULT_ADMIN_ROLE`. The default admin - /// is always exactly one address (or `address(0)` after a - /// completed renunciation). - function defaultAdmin() external view returns (address); - - /// @notice Returns the address that has been scheduled to receive - /// `DEFAULT_ADMIN_ROLE` and the block timestamp at which the - /// transfer becomes acceptable. Returns `(address(0), 0)` if no - /// transfer is scheduled. - function pendingDefaultAdmin() external view returns (address newAdmin, uint48 acceptSchedule); - - /// @notice The current delay (in seconds) that applies to a default-admin - /// transfer or renunciation. A scheduled transfer becomes - /// acceptable `defaultAdminDelay()` seconds after `beginDefaultAdminTransfer`. - function defaultAdminDelay() external view returns (uint48); - - /// @notice Returns the next scheduled delay value and the block timestamp - /// at which it takes effect. Returns `(0, 0)` if no delay change - /// is scheduled. - function pendingDefaultAdminDelay() external view returns (uint48 newDelay, uint48 effectSchedule); - - /// @notice The minimum wait (in seconds) before a delay-INCREASE takes - /// effect. Delay decreases are subject to the current delay; - /// delay increases are subject to whichever is greater of the - /// current delay or this floor. Prevents an admin from defending - /// a key compromise by instantaneously extending the delay to - /// lock out the rightful owner. - function defaultAdminDelayIncreaseWait() external view returns (uint48); - - /// @notice Schedules a transfer of `DEFAULT_ADMIN_ROLE` to `newAdmin`. - /// The transfer becomes acceptable `defaultAdminDelay()` seconds - /// after this call; until then `newAdmin` may call - /// `acceptDefaultAdminTransfer`. The current admin may - /// `cancelDefaultAdminTransfer` at any time before acceptance. - /// @dev Requires `DEFAULT_ADMIN_ROLE`. Setting `newAdmin == address(0)` - /// schedules a renunciation. Calling again replaces any prior - /// pending transfer with the new one (resets the delay clock). - function beginDefaultAdminTransfer(address newAdmin) external; - - /// @notice Cancels any pending default-admin transfer. Requires - /// `DEFAULT_ADMIN_ROLE`. No-op if no transfer is pending. - function cancelDefaultAdminTransfer() external; - - /// @notice Accepts the pending default-admin transfer. Caller MUST be - /// the address scheduled via `beginDefaultAdminTransfer`, and - /// the current `block.timestamp` must be at or after the - /// scheduled `acceptSchedule`. Atomically transfers - /// `DEFAULT_ADMIN_ROLE` from the previous admin to the caller. - /// @dev Reverts with `NoPendingDefaultAdmin` if no transfer is - /// pending, with `EnforcedDefaultAdminDelay` if called before - /// the schedule, or with `Unauthorized` if called by anyone - /// other than the pending admin. - function acceptDefaultAdminTransfer() external; - - /// @notice Schedules a change to `defaultAdminDelay`. Requires - /// `DEFAULT_ADMIN_ROLE`. Decreases take effect after the - /// current delay elapses; increases take effect after the - /// greater of the current delay or `defaultAdminDelayIncreaseWait`. - function changeDefaultAdminDelay(uint48 newDelay) external; - - /// @notice Cancels any pending delay change. Requires `DEFAULT_ADMIN_ROLE`. - /// No-op if no change is pending. - function rollbackDefaultAdminDelay() external; - /*////////////////////////////////////////////////////////////// PAUSE //////////////////////////////////////////////////////////////*/ - /// @notice Whether the contract is currently paused. While paused, - /// `transfer`, `transferFrom`, and their memo siblings revert - /// with `ContractPaused`. Mints, burns, role changes, policy - /// changes, and other admin actions are NOT blocked by pause. - function paused() external view returns (bool); - - /// @notice Pauses the contract. Requires `PAUSABLE` capability and - /// `PAUSE_ROLE`. Reverts with `AlreadyPaused` if already paused. - function pause() external; - - /// @notice Unpauses the contract. Requires `PAUSABLE` capability and - /// `UNPAUSE_ROLE`. Reverts with `NotPaused` if not currently - /// paused. + /// @notice The current paused-vector bitmask. A bit set in the result + /// means the corresponding class of operation (per + /// `PauseVectors`) is currently halted. Returns 0 when no + /// vectors are paused. Always returns 0 if the token's + /// `PAUSABLE` capability is unset. + function paused() external view returns (uint256); + + /// @notice Convenience view: returns whether `vector` is set in the + /// current paused bitmask. Equivalent to + /// `(paused() & vector) != 0`. + function isPaused(uint256 vector) external view returns (bool); + + /// @notice Pauses the operations indicated by `vectors`. Multiple + /// calls are additive: the new paused state is + /// `currentPaused | vectors`. Requires `PAUSABLE` capability + /// and `PAUSE_ROLE`. Reverts with `InvalidAmount` if + /// `vectors == 0`. + /// @dev See `PauseVectors` for the bit definitions. Pausing a + /// vector that is already set is a no-op for the bitmask but + /// still emits `Paused(updater, vectors)` with the argument + /// as supplied (for indexer trace). + function pause(uint256 vectors) external; + + /// @notice Unpauses ALL currently-paused vectors. Requires `PAUSABLE` + /// capability and `UNPAUSE_ROLE`. The Default surface does + /// not support unpausing a subset of vectors; admin must + /// unpause everything and re-pause the still-blocked vectors + /// in a follow-up call if granular resumption is desired. + /// @dev No-op if no vectors are currently paused; still emits + /// `Unpaused(updater)`. function unpause() external; /*////////////////////////////////////////////////////////////// POLICY //////////////////////////////////////////////////////////////*/ - /// @notice The policy ID currently gating this token's transfers and mints. - /// Newly created tokens default to ID 1 (always-allow), which is - /// a no-op gate. Setting to ID 0 (always-reject) functions as a - /// soft pause that survives across `unpause`. + /// @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 transferPolicyId() external view returns (uint64); - /// @notice Sets a new transfer policy. Requires `POLICY_MUTABLE` - /// capability and `DEFAULT_ADMIN_ROLE`. The policy must exist in - /// the policy registry. Takes effect immediately for the next + /// @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; @@ -352,62 +610,77 @@ interface IDefaultToken { //////////////////////////////////////////////////////////////*/ /// @notice The maximum total supply enforced on `mint`. A value of - /// `type(uint256).max` indicates no cap (the default). + /// `type(uint256).max` indicates no cap (the default for + /// tokens that do not specify a cap at creation). function supplyCap() external view returns (uint256); - /// @notice Sets a new supply cap. Requires `CAP_MUTABLE` capability and - /// `DEFAULT_ADMIN_ROLE`. Reverts with `InvalidSupplyCap` if the - /// new cap is below the current `totalSupply` (we never - /// invalidate already-issued supply). + /// @notice Sets a new supply cap. Requires `CAP_MUTABLE` capability + /// and `DEFAULT_ADMIN_ROLE`. Reverts with `InvalidSupplyCap` + /// if the new cap is below the current `totalSupply` (we + /// never invalidate already-issued supply). The cap may be + /// raised or lowered freely otherwise. Emits + /// `SupplyCapUpdated`. function setSupplyCap(uint256 newSupplyCap) external; /*////////////////////////////////////////////////////////////// - PERMIT (EIP-2612 + ERC-1271) + PERMIT (EIP-2612 + ERC-5267) //////////////////////////////////////////////////////////////*/ - /// @notice The current EIP-712 domain separator for this token. Computed - /// dynamically each call so it remains correct after a chain fork - /// that changes `block.chainid`. + /// @notice The current EIP-712 domain separator for this token. + /// Computed dynamically each call so it remains correct after + /// a chain fork that changes `block.chainid`. + /// @dev Domain content: `chainId` and `verifyingContract` only. + /// `name` and `version` are intentionally empty strings. function DOMAIN_SEPARATOR() external view returns (bytes32); - /// @notice The current permit nonce for `owner`. Incremented by exactly 1 - /// on each successful `permit` of either form. + /// @notice The current permit nonce for `owner`. Incremented by + /// exactly 1 on each successful `permit`. function nonces(address owner) external view returns (uint256); /// @notice EIP-2612 canonical permit. Recovers `owner` via ECDSA from - /// `(v, r, s)`. Reverts with `PermitExpired` if `block.timestamp > deadline`, - /// or `InvalidSignature` if recovery does not yield `owner`. - function permit( - address owner, - address spender, - uint256 value, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) external; - - /// @notice Permit accepting a packed `signature` for either an EOA owner - /// or a contract owner. - /// @dev If `owner.code.length == 0`, treats `signature` as 65-byte - /// packed ECDSA (`abi.encodePacked(r, s, v)`). If - /// `owner.code.length > 0`, calls `IERC1271(owner).isValidSignature(digest, signature)` - /// and accepts iff the magic value `0x1626ba7e` is returned. - /// Same nonce, same digest, same `PermitExpired` semantics as the - /// canonical form. - function permit(address owner, address spender, uint256 value, uint256 deadline, bytes calldata signature) + /// `(v, r, s)`. EOA signatures only; ERC-1271 contract + /// signatures are NOT supported on the Default surface. + /// Reverts with `ExpiredSignature(deadline)` if + /// `block.timestamp > deadline`, or + /// `InvalidSigner(recovered, owner)` if recovery does + /// not yield `owner`. + /// @dev Smart-contract accounts that need permit-style flows should + /// use call-batching (e.g. the EIP-7702 path) or paymaster- + /// based gasless flows; we deliberately do not enshrine + /// ERC-1271 dispatch here. Permit2 remains usable as a + /// periphery alternative. + function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external; + /// @notice ERC-5267 EIP-712 domain introspection. Returns the parts + /// of the EIP-712 domain populated for this token. + /// @dev For Default tokens, `fields` is `0x0c` (bits 2 and 3 set, + /// indicating `chainId` and `verifyingContract` are populated). + /// `name`, `version`, and `salt` are empty / zero. + /// `extensions` is empty. + function eip712Domain() + external + view + returns ( + bytes1 fields, + string memory name, + string memory version, + uint256 chainId, + address verifyingContract, + bytes32 salt, + uint256[] memory extensions + ); + /*////////////////////////////////////////////////////////////// CONTRACT URI (ERC-7572) //////////////////////////////////////////////////////////////*/ - /// @notice An offchain URI pointing at contract-level metadata for this - /// token (ERC-7572). + /// @notice An off-chain URI pointing at contract-level metadata for + /// this token, per ERC-7572. function contractURI() external view returns (string memory); - /// @notice Updates `contractURI`. Requires `URI_MUTABLE` capability and - /// `DEFAULT_ADMIN_ROLE`. Emits the parameterless - /// `ContractURIUpdated` event per ERC-7572. + /// @notice Updates `contractURI`. Requires `DEFAULT_ADMIN_ROLE`. Emits + /// the parameterless `ContractURIUpdated` event per ERC-7572; + /// integrators re-fetch `contractURI()` after observing it. function setContractURI(string calldata newURI) external; } diff --git a/src/interfaces/ISecurityToken.sol b/src/interfaces/ISecurityToken.sol index 6b54cd8..786b9dc 100644 --- a/src/interfaces/ISecurityToken.sol +++ b/src/interfaces/ISecurityToken.sol @@ -5,36 +5,84 @@ import {IDefaultToken} from "./IDefaultToken.sol"; /// @title ISecurityToken /// @notice A B-20 token variant for tokenized securities (equities, ETFs, -/// commodities, etc.). Extends IDefaultToken with primitives specific -/// to securities: split-safe share accounting, holder announcements, -/// security-identifier metadata, compliant issuance via `create`, and -/// user-side `redeem` for off-chain settlement. -/// @dev Security tokens enforce announcement coupling on every -/// metadata-changing operation: each call must reference an -/// announcement ID that was emitted via `announcement(...)` earlier -/// in the same transaction. Implementations enforce this via -/// transient storage so the chain itself, not the issuer's policy, -/// guarantees the audit trail invariant. +/// commodities, etc.). Extends `IDefaultToken` with primitives +/// specific to securities: holder-impacting announcements, +/// split-safe share-ratio accounting, security-identifier +/// metadata, compliant issuance via `create`, and cold-path +/// admin batch mint / burn for unusual corporate actions. /// -/// Security tokens typically configure their `capabilities()` with -/// `MINTABLE` unset, replacing the inherited `mint`/`mintWithMemo` -/// path with the security-specific `create` (rate-limited compliant -/// issuance) and `adminMint` (cold-path batch issuance) functions. -/// `BURNABLE` is similarly typically unset; holders burn via -/// `redeem` and admins burn via `adminBurn`. See `Capabilities` for -/// the bit definitions and the `BURN_BLOCKED` bit for sanctions -/// enforcement. +/// @dev **Inherited surface.** `IDefaultToken` 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. +/// +/// **Security-specific additions.** This interface adds: +/// 1. `announcement(...)` plus an `ANNOUNCE_ROLE` for posting +/// holder-impacting disclosures (corporate actions, name +/// changes, splits, etc.). +/// 2. **Announcement coupling**: every security-specific +/// metadata-changing operation (`updateShareRatio`, +/// `updateSecurityIdentifier`, `updateName`, `updateSymbol`, +/// `adminMint`, `adminBurn`) MUST reference an announcement +/// ID emitted via `announcement(...)` earlier in the same +/// transaction. Implementations enforce this via transient +/// storage so the chain itself, not the issuer's policy, +/// guarantees the audit-trail invariant. +/// 3. `shareRatio` + `toShares` + `sharesOf` for split-safe +/// DeFi-compatible share accounting. +/// 4. `create(...)` plus `ISSUER_ROLE` and a per-caller rate +/// limit for the compliant primary-market issuance path. +/// Distinct from the inherited `mint` because securities +/// have legal definitions around what constitutes "creation". +/// 5. `adminMint(...)` / `adminBurn(...)` cold-path batch +/// operations for unusual corporate actions. +/// 6. `updateName(...)` / `updateSymbol(...)` security-specific +/// paths that take an announcement ID. These are the +/// canonical name/symbol update functions for security +/// tokens; the inherited `setName` / `setSymbol` from +/// `IDefaultToken` are present in the interface but +/// implementations typically revert them on security tokens +/// so that name/symbol changes always carry an announcement. +/// 7. `securityIdentifier` / `updateSecurityIdentifier` for +/// ISIN, CUSIP, FIGI, and similar off-chain registry IDs. +/// +/// **Operationally typical configuration.** Security-token +/// issuers usually do NOT grant `MINT_ROLE` (the inherited mint +/// path is disabled in favor of `create` and `adminMint`) and +/// do NOT grant `BURN_ROLE` (holders use `redeem` for off-chain +/// settlement; admins use `adminBurn` for cold-path destruction). +/// Capability bits relevant to securities live in the +/// `Capabilities` library bits 16..23 (e.g. `SECURITY_CREATABLE`, +/// `SHARE_RATIO_MUTABLE`). interface ISecurityToken is IDefaultToken { /*////////////////////////////////////////////////////////////// ERRORS //////////////////////////////////////////////////////////////*/ + /// @notice A security-specific operation was called without a + /// matching prior `announcement(id, ...)` in the same + /// transaction. error AnnouncementRequired(string id); + + /// @notice An announcement ID was reused. Each ID may be consumed + /// exactly once across the lifetime of the token. error AnnouncementIdAlreadyUsed(string id); + + /// @notice `updateShareRatio` was called with a zero numerator or + /// denominator. error InvalidShareRatio(); + + /// @notice `create` was called by a caller whose remaining create + /// allowance under the configured rate limit is less than + /// the requested amount. error CreateRateLimitExceeded(address caller); - error RedeemNotAuthorized(address caller); - error RedeemBelowMinimum(uint256 amount, uint256 minimum); + + /// @notice `updateSecurityIdentifier` was called with an empty + /// `identifierType` string. error InvalidIdentifierType(); /*////////////////////////////////////////////////////////////// @@ -42,12 +90,13 @@ interface ISecurityToken is IDefaultToken { //////////////////////////////////////////////////////////////*/ /// @notice A holder-impacting announcement. Posted before any - /// metadata-changing operation that references the same `id`. + /// metadata-changing operation that references the same + /// `id`. event Announcement(address indexed caller, string id, string description, string uri); - /// @notice The token-to-share ratio changed (typically a stock split or - /// reverse split). Indexers should refresh `sharesOf` views for - /// all holders on receipt. + /// @notice The token-to-share ratio changed (typically a stock split + /// or reverse split). Indexers should refresh `sharesOf` + /// views for all holders on receipt. event ShareRatioUpdated( address indexed caller, string announcementId, @@ -57,16 +106,9 @@ interface ISecurityToken is IDefaultToken { uint48 newDenominator ); - /// @notice The token's name changed (e.g. corporate rebrand: Facebook to - /// Meta). Wallets and explorers should refresh their cache. - event NameUpdated(address indexed caller, string announcementId, string newName); - - /// @notice The token's symbol/ticker changed. Same indexer implications - /// as `NameUpdated`. - event SymbolUpdated(address indexed caller, string announcementId, string newSymbol); - /// @notice A security identifier (ISIN, CUSIP, FIGI, etc.) was set, - /// changed, or removed. `value` is the empty string on removal. + /// changed, or removed. `value` is the empty string on + /// removal. event SecurityIdentifierUpdated( address indexed caller, string announcementId, string identifierType, string value ); @@ -80,33 +122,47 @@ interface ISecurityToken is IDefaultToken { /// @notice Supply destroyed via the cold-path admin batch. event AdminBurned(address indexed caller, string announcementId, uint256 totalAmount); - /// @notice User-initiated burn for off-chain redemption. - event Redeemed(address indexed from, uint256 amount); - - event MinimumRedeemableUpdated(uint256 newMinimum); - event RedeemPolicyIdUpdated(uint64 indexed newPolicyId); + /// @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 + // `IDefaultToken` 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. + /*////////////////////////////////////////////////////////////// ROLE IDENTIFIERS //////////////////////////////////////////////////////////////*/ - /// @notice Required to call `announcement`. Held separately so a 24/7 - /// disclosure team can post announcements without holding - /// supply-changing or admin authority. + /// @notice Required to call `announcement`. Held separately so a + /// 24/7 disclosure team can post announcements without + /// holding supply-changing or admin authority. function ANNOUNCE_ROLE() external view returns (bytes32); + /// @notice Required to call `create` (compliant primary-market + /// issuance), `adminMint` (cold-path batch issuance), and + /// `adminBurn` (cold-path batch destruction). Distinct from + /// the inherited `MINT_ROLE` so security-specific issuance + /// authority can be split from the generic mint surface + /// (which is typically not granted at all on security + /// tokens). + function ISSUER_ROLE() external view returns (bytes32); + /*////////////////////////////////////////////////////////////// ANNOUNCEMENTS //////////////////////////////////////////////////////////////*/ - /// @notice Posts a holder-impacting announcement. The announcement does - /// not store its `description` or `uri` on-chain (per current - /// design, see DESIGN_NOTES); the data lives only in the emitted - /// event log. The `id` is consumed: subsequent calls in the - /// same transaction that reference this `id` are gated on it - /// having been announced first; subsequent calls in later - /// transactions may not reuse it. + /// @notice Posts a holder-impacting announcement. The announcement + /// does not store its `description` or `uri` on-chain (per + /// current design, see DESIGN_NOTES); the data lives only + /// in the emitted event log. The `id` is consumed: + /// subsequent calls in the same transaction that reference + /// this `id` are gated on it having been announced first; + /// subsequent calls in later transactions may not reuse it. /// @dev Requires `ANNOUNCE_ROLE`. Reverts with /// `AnnouncementIdAlreadyUsed` on `id` reuse. function announcement(string calldata id, string calldata description, string calldata uri) external; @@ -118,14 +174,14 @@ interface ISecurityToken is IDefaultToken { SHARE RATIO //////////////////////////////////////////////////////////////*/ - /// @notice The current token-to-share ratio. A 1:1 ratio (numerator == - /// denominator) means raw token balances equal share counts. - /// A 2:1 ratio (e.g. after a 2-for-1 split) means each raw - /// token represents 2 shares. + /// @notice The current token-to-share ratio. A 1:1 ratio (numerator + /// == denominator) means raw token balances equal share + /// counts. A 2:1 ratio (e.g. after a 2-for-1 split) means + /// each raw token represents 2 shares. function shareRatio() external view returns (uint48 numerator, uint48 denominator); - /// @notice Converts a raw token balance to its current share count via - /// the active share ratio. Equivalent to + /// @notice Converts a raw token balance to its current share count + /// via the active share ratio. Equivalent to /// `balance * denominator / numerator`. function toShares(uint256 balance) external view returns (uint256); @@ -134,26 +190,29 @@ interface ISecurityToken is IDefaultToken { /// @notice Sets a new share ratio (typically following an off-chain /// stock split or reverse split). Holder balances are NOT - /// rewritten; the displayed share count derives from the new - /// ratio at read time, preserving DeFi composability. - /// @dev Requires `DEFAULT_ADMIN_ROLE` and an `Announcement(id, ...)` - /// emitted earlier in the same transaction with the same id. - /// Both numerator and denominator must be non-zero. + /// rewritten; the displayed share count derives from the + /// new ratio at read time, preserving DeFi composability. + /// @dev Requires `DEFAULT_ADMIN_ROLE` and an + /// `Announcement(id, ...)` emitted earlier in the same + /// transaction with the same id. Both numerator and + /// denominator must be non-zero. function updateShareRatio(string calldata announcementId, uint48 newNumerator, uint48 newDenominator) external; /*////////////////////////////////////////////////////////////// ISSUANCE: create //////////////////////////////////////////////////////////////*/ - /// @notice The compliant issuance path. Mints `amount` to `to` subject - /// to the standard transfer-policy mint-recipient check AND to a - /// per-caller rate limit configured by the admin. - /// @dev Requires `ISSUER_ROLE`. Subject to the inherited supply cap - /// (`supplyCap`). Distinct from the inherited `mint` semantically - /// because securities have legal definitions around what - /// constitutes "creation"; this is the function product surfaces - /// should call. Tokens that want to disable normal issuance after - /// a bootstrap period can revoke `ISSUER_ROLE` from all callers. + /// @notice The compliant issuance path. Mints `amount` to `to` + /// subject to the standard transfer-policy mint-recipient + /// check AND to a per-caller rate limit configured by the + /// admin. + /// @dev Requires `ISSUER_ROLE`. Subject to the inherited supply + /// cap (`supplyCap`). Distinct from the inherited `mint` + /// semantically because securities have legal definitions + /// around what constitutes "creation"; this is the function + /// product surfaces should call. Tokens that want to disable + /// normal issuance after a bootstrap period can revoke + /// `ISSUER_ROLE` from all callers. function create(address to, uint256 amount) external; /// @notice The remaining create allowance for `caller` under their @@ -162,95 +221,68 @@ interface ISecurityToken is IDefaultToken { /// @notice Configures the per-call create rate limit for `caller`: /// `maxAmount` total over each `interval` (seconds). - /// @dev Requires `DEFAULT_ADMIN_ROLE`. Setting `maxAmount` to 0 or - /// interval to 0 effectively disables that caller's create. + /// @dev Requires `DEFAULT_ADMIN_ROLE`. Setting `maxAmount` to 0 + /// or interval to 0 effectively disables that caller's + /// create. function configureCreateRateLimit(address caller, uint256 maxAmount, uint256 interval) external; /*////////////////////////////////////////////////////////////// ISSUANCE: cold-path batch //////////////////////////////////////////////////////////////*/ - /// @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 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. + /// @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 + /// 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; - /// @notice Cold-path batch burn. Used for cold-path corporate actions - /// (reverse-tender settlement, mass-corrections under regulatory - /// direction, etc.). NOT subject to the contract pause: admins - /// can adminBurn even while transfers are paused. - /// @dev Requires `BURN_BLOCKED_ROLE` and an `Announcement(id, ...)` + /// @notice Cold-path batch burn. Used for cold-path corporate + /// actions (reverse-tender settlement, mass-corrections + /// under regulatory direction, etc.). NOT subject to the + /// inherited pause vectors: admins can `adminBurn` even + /// while transfers and burns are paused. + /// @dev Requires `ISSUER_ROLE` and an `Announcement(id, ...)` /// emitted earlier in the same transaction with the same - /// `announcementId`. Reverts atomically if any single account - /// lacks sufficient balance. + /// `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; - /*////////////////////////////////////////////////////////////// - USER REDEEM - //////////////////////////////////////////////////////////////*/ - - /// @notice User-initiated burn for off-chain settlement. The caller - /// destroys `amount` of their own balance in exchange for the - /// off-chain commitment to settle the equivalent shares to - /// their brokerage account. - /// @dev Requires the caller to be authorized under the token's - /// current `redeemPolicyId` (typically a Coinbase-managed - /// allowlist of KYC'd, brokerage-connected accounts). Reverts - /// with `RedeemBelowMinimum` if `amount < minimumRedeemable`. - function redeem(uint256 amount) external; - - /// @notice The minimum amount that can be redeemed in a single call. - /// Set by the admin to amortize per-redeem off-chain settlement - /// overhead. - function minimumRedeemable() external view returns (uint256); - - /// @notice Updates `minimumRedeemable`. Requires `DEFAULT_ADMIN_ROLE`. - function setMinimumRedeemable(uint256 newMinimum) external; - - /// @notice The policy ID gating who can call `redeem`. Distinct from - /// `transferPolicyId`; the redeem allowlist is typically more - /// restrictive (only brokerage-verified accounts), while - /// transfers may permit a broader set of holders. - /// @dev The policy referenced here should be a simple WHITELIST - /// policy in the registry, with admin held by whoever manages - /// the brokerage onboarding pipeline (typically the issuer). - function redeemPolicyId() external view returns (uint64); - - /// @notice Updates `redeemPolicyId`. Requires `DEFAULT_ADMIN_ROLE`. - function setRedeemPolicyId(uint64 newPolicyId) external; - /*////////////////////////////////////////////////////////////// SECURITY IDENTIFIERS //////////////////////////////////////////////////////////////*/ - /// @notice Returns the value of the named identifier (e.g. ISIN, CUSIP, - /// FIGI). Returns the empty string if not set. + /// @notice Returns the value of the named identifier (e.g. ISIN, + /// CUSIP, FIGI). Returns the empty string if not set. function securityIdentifier(string calldata identifierType) external view returns (string memory); /// @notice Returns all currently-set identifiers as `[type, value]` /// pairs. Order is not guaranteed; callers should treat the - /// array as a set. The expected count is small (a handful per - /// security), so enumeration is safe. + /// array as a set. The expected count is small (a handful + /// per security), so enumeration is safe. function getSecurityIdentifiers() external view returns (string[2][] memory); - /// @notice Sets, updates, or removes a security identifier. If `remove` - /// is true, the entry is deleted (`value` is ignored). - /// @dev Requires `DEFAULT_ADMIN_ROLE` and an `Announcement(id, ...)` - /// emitted earlier in the same transaction. Reverts with - /// `InvalidIdentifierType` on empty `identifierType`. + /// @notice Sets, updates, or removes a security identifier. If + /// `remove` is true, the entry is deleted (`value` is + /// ignored). + /// @dev Requires `DEFAULT_ADMIN_ROLE` and an + /// `Announcement(id, ...)` emitted earlier in the same + /// transaction. Reverts with `InvalidIdentifierType` on + /// empty `identifierType`. function updateSecurityIdentifier( string calldata announcementId, string calldata identifierType, @@ -262,19 +294,34 @@ interface ISecurityToken is IDefaultToken { NAME / SYMBOL UPDATES //////////////////////////////////////////////////////////////*/ - /// @notice Updates the token's name (e.g. corporate rebrand). Reads via - /// the inherited `name()` accessor reflect the new value - /// immediately. Affects EIP-712 domain separator computation - /// (used by `permit`); callers signing permits should re-read - /// `name()` immediately before signing. - /// @dev Requires `DEFAULT_ADMIN_ROLE` and an `Announcement(id, ...)` - /// emitted earlier in the same transaction. + /// @notice Updates the token's name (e.g. corporate rebrand). + /// Reads via the inherited `name()` accessor reflect the + /// new value immediately. Affects EIP-712 domain separator + /// computation (used by `permit`); callers signing permits + /// should re-read the relevant domain fields immediately + /// before signing. Emits the inherited `NameUpdated` event + /// from `IDefaultToken`. + /// @dev Requires `DEFAULT_ADMIN_ROLE` and an + /// `Announcement(id, ...)` emitted earlier in the same + /// transaction with the same id. + /// + /// Note: `IDefaultToken.setName(newName)` is also in this + /// interface (inherited) but security-token implementations + /// typically revert it so that all name changes carry an + /// announcement. Use `updateName` here for the canonical + /// security path. function updateName(string calldata announcementId, string calldata newName) external; - /// @notice Updates the token's symbol (e.g. ticker change). Reads via - /// the inherited `symbol()` accessor reflect the new value - /// immediately. - /// @dev Requires `DEFAULT_ADMIN_ROLE` and an `Announcement(id, ...)` - /// emitted earlier in the same transaction. + /// @notice Updates the token's symbol (e.g. ticker change). Reads + /// via the inherited `symbol()` accessor reflect the new + /// value immediately. Emits the inherited `SymbolUpdated` + /// event from `IDefaultToken`. + /// @dev Requires `DEFAULT_ADMIN_ROLE` and an + /// `Announcement(id, ...)` emitted earlier in the same + /// transaction with the same id. + /// + /// Same caveat as `updateName`: the inherited + /// `setSymbol(newSymbol)` is typically reverted by security + /// implementations. function updateSymbol(string calldata announcementId, string calldata newSymbol) external; } diff --git a/src/interfaces/IStablecoin.sol b/src/interfaces/IStablecoin.sol index c71d12d..409de46 100644 --- a/src/interfaces/IStablecoin.sol +++ b/src/interfaces/IStablecoin.sol @@ -5,69 +5,44 @@ import {IDefaultToken} from "./IDefaultToken.sol"; /// @title IStablecoin /// @notice A B-20 token variant for value-pegged tokens (USD, EUR, XAU, etc.). -/// Inherits the full IDefaultToken surface and adds three things -/// specific to stablecoin issuance and payment use cases: +/// Inherits the full `IDefaultToken` surface and adds a single +/// immutable `currency()` identifier for routing, categorization, +/// and wallet display. /// -/// 1. An immutable `currency()` identifier for routing, categorization, -/// and wallet display. -/// 2. Per-minter rate limiting (rolling capacity per minter address) -/// for risk management and multi-party governance. -/// 3. ERC-3009 transfer-with-authorization for gasless and -/// front-run-resistant transfers (USDC-parity payment surface). +/// @dev Per the team PRD, stablecoin-specific features that earlier +/// drafts attempted to enshrine here have been moved out of the +/// protocol surface entirely: /// -/// @dev Stablecoin compliance (sanctions, jurisdiction restrictions, -/// blocklisting) is delegated to the policy engine via IDefaultToken's -/// `transferPolicyId`, not implemented here. Issuers point their -/// stablecoin at a compound policy with the appropriate sender, -/// recipient, and mint-recipient rules. +/// - **Per-minter rate limiting** lives in EVM periphery +/// contracts (a stablecoin issuer's own controller / wrapper +/// contract that holds `MINT_ROLE` and enforces per-caller +/// quotas before invoking `mint` on the precompile). The +/// Bridge `TIP20Controller` pattern and the CDP Custom +/// Stablecoin pattern are both expressible this way. +/// - **ERC-3009 transfer-with-authorization** is not on the +/// default surface. Stablecoin issuers that need gasless +/// payment flows can layer it via periphery contracts (or +/// rely on EIP-2612 permit, which IS on the default surface, +/// plus call-batching on the wallet side). +/// - **Sanctions seizure** ("force-burn from blocked addresses") +/// is not on the default surface either. CCS uses the +/// "freeze, never seize" philosophy and never burns; +/// stablecoin issuers that need seizure flows do them via +/// periphery contracts that hold roles for the underlying +/// operations. /// -/// The "freeze, never seize" philosophy (CDP Custom Stablecoin) vs. -/// the "force-burn for sanctions" philosophy (Tangor) is expressed -/// via the `BURN_BLOCKED` capability bit. Stablecoin issuers default -/// to freeze; can opt into seize by enabling `BURN_BLOCKED` at -/// creation. +/// Compliance (sanctions, jurisdiction restrictions, KYC) is +/// delegated to the policy engine via `IDefaultToken.transferPolicyId`. +/// Issuers point their stablecoin at a compound policy with the +/// appropriate sender, recipient, mint-recipient, and redeemer +/// slots configured per their compliance regime. interface IStablecoin is IDefaultToken { - /*////////////////////////////////////////////////////////////// - ERRORS - //////////////////////////////////////////////////////////////*/ - - error MintRateLimitNotConfigured(address minter); - error MintRateLimitExceeded(address minter, uint256 amount, uint256 remaining); - error InvalidRateLimitConfig(); - - error AuthorizationAlreadyUsed(address authorizer, bytes32 nonce); - error AuthorizationNotYetValid(uint256 validAfter); - error AuthorizationExpired(uint256 validBefore); - error CallerMustBePayee(address caller, address payee); - error InvalidAuthorization(); - - /*////////////////////////////////////////////////////////////// - EVENTS - //////////////////////////////////////////////////////////////*/ - - event MintRateLimitConfigured(address indexed minter, uint256 limit, uint40 interval); - event MintRateLimitRemoved(address indexed minter); - event MintRateLimitConsumed(address indexed minter, uint256 amount, uint256 remaining); - - event AuthorizationUsed(address indexed authorizer, bytes32 indexed nonce); - event AuthorizationCanceled(address indexed authorizer, bytes32 indexed nonce); - - /*////////////////////////////////////////////////////////////// - ROLE IDENTIFIERS - //////////////////////////////////////////////////////////////*/ - - /// @notice Required to call `configureMinter` and `removeMinterRateLimit`. - /// Held separately from `MINT_ROLE` so the authority that grants - /// minting rights (typically `DEFAULT_ADMIN_ROLE`) can be - /// distinct from the authority that tunes per-minter quotas. - function MINT_RATE_LIMIT_ROLE() external view returns (bytes32); - /*////////////////////////////////////////////////////////////// CURRENCY IDENTIFIER //////////////////////////////////////////////////////////////*/ - /// @notice The reference asset this stablecoin is designed to track. Set - /// at creation by the factory; immutable thereafter. + /// @notice The reference asset this stablecoin is designed to track. + /// Set at creation by the factory; immutable thereafter. /// @dev Two stablecoins tracking the same asset return the same /// identifier. Conventions: /// - ISO-4217 codes for fiat / commodity references: "USD", @@ -78,146 +53,4 @@ interface IStablecoin is IDefaultToken { /// (governance, utility tokens that nonetheless want the /// stablecoin variant for the operational surface). function currency() external view returns (string memory); - - /*////////////////////////////////////////////////////////////// - PER-MINTER RATE LIMITING - //////////////////////////////////////////////////////////////*/ - - /// @notice Configures or replaces the rate-limit for an existing minter. - /// The minter MUST already hold `MINT_ROLE`. Setting a new limit - /// RESETS the remaining capacity to the full `limit`. - /// @dev Requires `STABLECOIN_MINT_RATE_LIMITED` capability and - /// `MINT_RATE_LIMIT_ROLE`. Reverts with `InvalidRateLimitConfig` - /// if `limit == 0` or `interval == 0`. Reverts with - /// `Unauthorized` if `minter` does not hold `MINT_ROLE`. - function configureMinter(address minter, uint216 limit, uint40 interval) external; - - /// @notice Atomically grants `MINT_ROLE` to `minter` and configures their - /// rate-limit in a single transaction. Eliminates the race where - /// a freshly-granted minter has the role but no rate-limit - /// configured yet (and therefore reverts on first mint). - /// @dev Requires `STABLECOIN_MINT_RATE_LIMITED` capability and - /// `DEFAULT_ADMIN_ROLE` (since it grants a role). - function grantMinterRoleWithLimit(address minter, uint216 limit, uint40 interval) external; - - /// @notice Removes a minter's rate-limit configuration without revoking - /// their `MINT_ROLE`. Subsequent `mint` calls by `minter` will - /// revert with `MintRateLimitNotConfigured` until configured - /// again. - /// @dev Requires `STABLECOIN_MINT_RATE_LIMITED` capability and - /// `MINT_RATE_LIMIT_ROLE`. Implementations SHOULD also clear - /// the rate-limit automatically when `MINT_ROLE` is revoked - /// from a minter via `revokeRole`. - function removeMinterRateLimit(address minter) external; - - /// @notice Returns the current available mint capacity for `minter` at - /// the current block timestamp, accounting for elapsed time - /// since the last consumption. - /// @dev Reverts with `MintRateLimitNotConfigured` if `minter` has no - /// active rate-limit configuration. - function currentMintLimit(address minter) external view returns (uint256); - - /// @notice Returns the configured `(limit, interval)` for `minter`. - /// Returns `(0, 0)` if `minter` has no active configuration. - function mintRateLimitConfig(address minter) external view returns (uint216 limit, uint40 interval); - - /*////////////////////////////////////////////////////////////// - ERC-3009 AUTHORIZATIONS - //////////////////////////////////////////////////////////////*/ - - /// @notice EIP-712 typehash for `transferWithAuthorization`. Computed as - /// keccak256("TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)") - function TRANSFER_WITH_AUTHORIZATION_TYPEHASH() external view returns (bytes32); - - /// @notice EIP-712 typehash for `receiveWithAuthorization`. Computed as - /// keccak256("ReceiveWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)") - function RECEIVE_WITH_AUTHORIZATION_TYPEHASH() external view returns (bytes32); - - /// @notice EIP-712 typehash for `cancelAuthorization`. Computed as - /// keccak256("CancelAuthorization(address authorizer,bytes32 nonce)") - function CANCEL_AUTHORIZATION_TYPEHASH() external view returns (bytes32); - - /// @notice Whether `nonce` for `authorizer` has been consumed (via use or - /// cancellation). ERC-3009 nonces are 32-byte random values, NOT - /// sequential, so multiple authorizations can be in flight - /// concurrently and consumed independently. - function authorizationState(address authorizer, bytes32 nonce) external view returns (bool used); - - /// @notice Executes a transfer from `from` to `to` using a signed - /// authorization. Anyone may submit. The transfer is subject to - /// the active transfer policy and pause state, same as a normal - /// `transfer`. - /// @dev Requires `STABLECOIN_AUTHORIZATIONS` capability. Reverts with - /// `AuthorizationNotYetValid` if `block.timestamp <= validAfter`, - /// `AuthorizationExpired` if `block.timestamp >= validBefore`, - /// `AuthorizationAlreadyUsed` on nonce reuse, and - /// `InvalidAuthorization` on signature recovery failure. The - /// `(v, r, s)` form is the canonical ECDSA path; the `bytes` - /// overload accepts either ECDSA OR ERC-1271 contract sigs. - function transferWithAuthorization( - address from, - address to, - uint256 value, - uint256 validAfter, - uint256 validBefore, - bytes32 nonce, - uint8 v, - bytes32 r, - bytes32 s - ) external; - - /// @notice Same as `transferWithAuthorization` (canonical ECDSA), but - /// the caller MUST be `to`. Prevents front-running by ensuring - /// only the intended payee can submit. Useful when the payer - /// signs for a specific recipient and wants no relayer to be - /// able to redirect. - /// @dev Reverts with `CallerMustBePayee` if `msg.sender != to`. - function receiveWithAuthorization( - address from, - address to, - uint256 value, - uint256 validAfter, - uint256 validBefore, - bytes32 nonce, - uint8 v, - bytes32 r, - bytes32 s - ) external; - - /// @notice Cancels a previously-signed authorization nonce so it cannot - /// be used. The cancellation is itself a signed message; anyone - /// may submit. Reverts with `AuthorizationAlreadyUsed` if the - /// nonce has already been used or canceled. - function cancelAuthorization(address authorizer, bytes32 nonce, uint8 v, bytes32 r, bytes32 s) external; - - /// @notice `transferWithAuthorization` accepting either an ECDSA - /// (65-byte packed `(r, s, v)`) signature for EOA authorizers or - /// an ERC-1271 signature for contract authorizers. Validity is - /// determined by whether `from.code.length > 0`. - function transferWithAuthorization( - address from, - address to, - uint256 value, - uint256 validAfter, - uint256 validBefore, - bytes32 nonce, - bytes calldata signature - ) external; - - /// @notice `receiveWithAuthorization` accepting either an ECDSA or - /// ERC-1271 signature. See the canonical `receiveWithAuthorization` - /// for the front-run-resistance constraint. - function receiveWithAuthorization( - address from, - address to, - uint256 value, - uint256 validAfter, - uint256 validBefore, - bytes32 nonce, - bytes calldata signature - ) external; - - /// @notice `cancelAuthorization` accepting either an ECDSA or ERC-1271 - /// signature. - function cancelAuthorization(address authorizer, bytes32 nonce, bytes calldata signature) external; } diff --git a/src/interfaces/ITokenFactory.sol b/src/interfaces/ITokenFactory.sol index e516ef8..fa31f95 100644 --- a/src/interfaces/ITokenFactory.sol +++ b/src/interfaces/ITokenFactory.sol @@ -34,28 +34,38 @@ interface ITokenFactory { } /// @notice Creation parameters for a Default-variant token. - /// @param name ERC-20 token name. - /// @param symbol ERC-20 token symbol. + /// @param name ERC-20 token name. Mutable post-creation + /// via `setName` (admin-only). + /// @param symbol ERC-20 token symbol. Mutable + /// post-creation via `setSymbol` + /// (admin-only). /// @param decimals ERC-20 token decimals (issuer choice). + /// Immutable after creation. /// @param admin Initial holder of `DEFAULT_ADMIN_ROLE`. - /// @param defaultAdminDelay Initial value for the two-step admin - /// transfer delay (seconds). Zero means - /// transfers are effectively single-step - /// at creation; admin can raise via - /// `changeDefaultAdminDelay` later. - /// @param capabilities Immutable capability bitfield. + /// @param capabilities Immutable capability bitfield. See + /// `Capabilities` for the bit definitions. /// @param initialSupply Amount minted atomically at creation. /// Bypasses the transfer-policy check - /// and the `MINTABLE` capability gate /// (this is the bootstrap mint, not a - /// normal mint operation). + /// normal mint operation; the policy + /// may not be configured at creation + /// time). /// @param initialSupplyRecipient Address that receives `initialSupply`. /// Ignored when `initialSupply == 0`. /// @param transferPolicyId Initial value of `transferPolicyId`. /// Must reference an existing policy in /// the policy registry. /// @param supplyCap Initial value of `supplyCap`. Use - /// `type(uint256).max` for no cap. + /// `type(uint256).max` for no cap. To + /// make the token permanently fixed-supply, + /// set this equal to `initialSupply` and + /// leave the `CAP_MUTABLE` capability + /// unset. + /// @param minimumRedeemable Initial value of `minimumRedeemable`. + /// Use `0` to allow any non-zero amount + /// (the typical setting for tokens + /// without a redemption product). Mutable + /// post-creation via `setMinimumRedeemable`. /// @param contractURI Initial ERC-7572 contract URI. /// @param salt Caller-chosen salt for deterministic /// address derivation. @@ -64,20 +74,21 @@ interface ITokenFactory { string symbol; uint8 decimals; address admin; - uint48 defaultAdminDelay; uint256 capabilities; uint256 initialSupply; address initialSupplyRecipient; uint64 transferPolicyId; uint256 supplyCap; + uint256 minimumRedeemable; string contractURI; bytes32 salt; } /// @notice Creation parameters for a Stablecoin-variant token. /// @param currency Immutable currency identifier (e.g. - /// "USD", "EUR", "XAU"). See `IStablecoin.currency` - /// for the convention. + /// "USD", "EUR", "XAU"). See + /// `IStablecoin.currency` for the + /// convention. /// @dev All other fields have the same semantics as the Default /// params struct. struct CreateStablecoinParams { @@ -85,25 +96,18 @@ interface ITokenFactory { string symbol; uint8 decimals; address admin; - uint48 defaultAdminDelay; uint256 capabilities; uint256 initialSupply; address initialSupplyRecipient; uint64 transferPolicyId; uint256 supplyCap; + uint256 minimumRedeemable; string contractURI; string currency; bytes32 salt; } /// @notice Creation parameters for a Security-variant token. - /// @param redeemPolicyId Initial value of `redeemPolicyId`. - /// Typically a simple WHITELIST policy - /// whose admin is the brokerage-onboarding - /// operator. Defaulting to policy ID 0 - /// (always-reject) is the safe choice - /// if no allowlist is ready at creation. - /// @param minimumRedeemable Initial minimum redeem amount. /// @param shareRatioNumerator Initial share-ratio numerator. Must /// be non-zero. Use `1` for 1:1 unless /// the issuer wants headroom for @@ -119,22 +123,26 @@ interface ITokenFactory { /// issuance goes through `create` (rate-limited compliant /// path) or `adminMint` (cold-path batch with announcement /// coupling) after creation. The supply cap is set at - /// creation; `transferPolicyId` and `redeemPolicyId` must - /// reference existing policies. + /// creation; `transferPolicyId` must reference an existing + /// compound policy in the registry whose redeemer slot + /// encodes the brokerage allowlist (typically a + /// Coinbase-managed whitelist of KYC'd, brokerage-connected + /// accounts). + /// + /// All other fields have the same semantics as the Default + /// params struct. struct CreateSecurityTokenParams { string name; string symbol; uint8 decimals; address admin; - uint48 defaultAdminDelay; uint256 capabilities; uint64 transferPolicyId; - uint64 redeemPolicyId; + uint256 supplyCap; uint256 minimumRedeemable; uint48 shareRatioNumerator; uint48 shareRatioDenominator; string[2][] securityIdentifiers; - uint256 supplyCap; string contractURI; bytes32 salt; } @@ -143,12 +151,13 @@ interface ITokenFactory { ERRORS //////////////////////////////////////////////////////////////*/ - /// @notice A token already exists at the deterministic address derived - /// from `(variant, msg.sender, salt)`. Caller must use a - /// different salt. + /// @notice A token already exists at the deterministic address + /// derived from `(variant, msg.sender, salt)`. Caller must + /// use a different salt. error TokenAlreadyExists(address token); - /// @notice The provided policy ID does not exist in the policy registry. + /// @notice The provided policy ID does not exist in the policy + /// registry. error InvalidPolicyId(uint64 policyId); /// @notice The provided share-ratio numerator or denominator is zero. @@ -165,9 +174,9 @@ interface ITokenFactory { /// supply, or is otherwise invalid. error InvalidSupplyCap(); - /// @notice A security identifier `type` was the empty string. Identifier - /// types must be non-empty (typical values: "isin", "cusip", - /// "figi", "sedol"). + /// @notice A security identifier `type` was the empty string. + /// Identifier types must be non-empty (typical values: + /// "isin", "cusip", "figi", "sedol"). error EmptyIdentifierType(); /*////////////////////////////////////////////////////////////// @@ -222,36 +231,42 @@ interface ITokenFactory { /// @notice Creates a Default-variant token at a deterministic address /// derived from `(DEFAULT, msg.sender, params.salt)`. Mints /// `params.initialSupply` to `params.initialSupplyRecipient` - /// atomically (bypasses the policy check and `MINTABLE` gate; - /// this is the bootstrap mint). + /// atomically. The bootstrap mint bypasses the policy check + /// (the policy may not yet authorize the recipient at + /// creation time); subsequent mints go through the normal + /// policy hook. /// @return token The address of the newly created token. function createDefault(CreateDefaultTokenParams calldata params) external returns (address token); /// @notice Creates a Stablecoin-variant token at a deterministic /// address derived from `(STABLECOIN, msg.sender, params.salt)`. /// Mints `params.initialSupply` to - /// `params.initialSupplyRecipient` atomically. Sets the - /// immutable `currency` field. + /// `params.initialSupplyRecipient` atomically (same bootstrap + /// policy bypass as `createDefault`). Sets the immutable + /// `currency` field. function createStablecoin(CreateStablecoinParams calldata params) external returns (address token); - /// @notice Creates a Security-variant token at a deterministic address - /// derived from `(SECURITY, msg.sender, params.salt)`. NO - /// initial supply is minted; security tokens use `create` / - /// `adminMint` for issuance after deployment. + /// @notice Creates a Security-variant token at a deterministic + /// address derived from `(SECURITY, msg.sender, params.salt)`. + /// NO initial supply is minted; security tokens use `create` + /// (rate-limited compliant issuance) or `adminMint` + /// (cold-path batch with announcement coupling) for issuance + /// after deployment. function createSecurity(CreateSecurityTokenParams calldata params) external returns (address token); /*////////////////////////////////////////////////////////////// ADDRESS PREDICTION //////////////////////////////////////////////////////////////*/ - /// @notice Returns the deterministic address that `createDefault` would - /// assign for the given `(creator, salt)`. The address depends - /// only on the variant, creator, and salt — not on any of the - /// other creation parameters. Stable across all parameter - /// choices for a given `(creator, salt)`. + /// @notice Returns the deterministic address that `createDefault` + /// would assign for the given `(creator, salt)`. The address + /// depends only on the variant, creator, and salt; not on + /// any of the other creation parameters. Stable across all + /// parameter choices for a given `(creator, salt)`. function predictDefaultAddress(address creator, bytes32 salt) external view returns (address); - /// @notice Same as `predictDefaultAddress`, for the Stablecoin variant. + /// @notice Same as `predictDefaultAddress`, for the Stablecoin + /// variant. function predictStablecoinAddress(address creator, bytes32 salt) external view returns (address); /// @notice Same as `predictDefaultAddress`, for the Security variant. diff --git a/src/interfaces/PauseVectors.sol b/src/interfaces/PauseVectors.sol new file mode 100644 index 0000000..f167a75 --- /dev/null +++ b/src/interfaces/PauseVectors.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.20 <0.9.0; + +/// @title PauseVectors +/// @notice Bit positions for the granular pause bitfield used by `pause` +/// and `paused` on `IDefaultToken` and its variants. +/// +/// A token's `pause(uint256 vectors)` function accepts a bitmask +/// of vectors to pause; multiple calls are additive (each call +/// OR's its argument into the current paused state). `unpause()` +/// clears all paused vectors at once. `paused()` returns the +/// current bitmask, and `isPaused(vector)` returns whether a +/// specific vector is set. +/// +/// Bit positions are append-only across protocol versions and +/// shared across token variants. Default-token vectors live in +/// bits 0..15; security-variant additions would live in bits +/// 16..23; stablecoin-variant additions in bits 24..31. +library PauseVectors { + /*////////////////////////////////////////////////////////////// + Default-token vectors (0..15) + //////////////////////////////////////////////////////////////*/ + + /// @notice Pauses `mint` and `mintWithMemo`. Issuance is halted while + /// this bit is set. + uint256 internal constant MINT = 1 << 0; + + /// @notice Pauses `burn` and `burnWithMemo`. Holders cannot destroy + /// their own balance via `burn` while this bit is set. + uint256 internal constant BURN = 1 << 1; + + /// @notice Pauses `transfer`, `transferFrom`, and the `*WithMemo` + /// siblings. Holder-to-holder movement is halted while this + /// bit is set. Mint, burn, and redeem vectors are independent. + uint256 internal constant TRANSFER = 1 << 2; + + /// @notice Pauses `redeem` and `redeemWithMemo`. Holders cannot + /// redeem their balance for off-chain settlement while this + /// bit is set. Independent of the `BURN` vector even though + /// both operations destroy supply: redeem implies an + /// off-chain claim, burn does not. + uint256 internal constant REDEEM = 1 << 3; +}