diff --git a/DESIGN_NOTES.md b/DESIGN_NOTES.md index d4f5662..f283a25 100644 --- a/DESIGN_NOTES.md +++ b/DESIGN_NOTES.md @@ -1,614 +1,275 @@ -# Design Notes & Open Questions - -Decisions, rationale, and open questions for the v1 interfaces in -`src/interfaces/`. Two sections: - -- **[Design Rationale](#design-rationale)**: settled decisions and the - reasoning behind them. Read this before reviewing the code if you want - the "why" before the "what." -- **[Open Questions](#open-questions)**: items still requiring input, - flagged inline so you can scan and respond. - ---- - -## Design Rationale - -### Cross-cutting decisions - -#### Capabilities bitfield (immutable feature flags) - -Every B-20 token exposes an immutable `capabilities()` bitfield, set at -creation by the factory. Each bit corresponds to one optional feature. -Functions whose bit is unset revert with `FeatureDisabled`, regardless -of role state. - -**Why this exists.** Role renunciation alone is not strong enough for -honest signaling. An issuer who claims "this token cannot be paused" -might have renounced `PAUSE_ROLE` today but kept `DEFAULT_ADMIN_ROLE`, -allowing them to grant `PAUSE_ROLE` to anyone tomorrow. An integrator -checking for non-pausability would have to recursively analyze the role -admin tree and the current admin holder to be sure. Capabilities collapse -this into one immutable read: if the bit is unset, the function reverts -forever, no exceptions. - -The bitfield is immutable per token (set once, never changed). New -features ship as new bits in higher positions; bits are append-only -across versions and never repurposed. - -**Bit ranges per variant** to avoid collisions across the variant ABIs: -- Default-token bits: `1 << 0` through `1 << 15` -- Security-token bits: `1 << 16` through `1 << 23` -- Stablecoin-token bits: `1 << 24` through `1 << 31` - -This leaves headroom in each range for v2 / v3 additions. - -#### Default IS Core (variant inheritance, no separate ICoreToken) - -`IStablecoin` and `ISecurityToken` both extend `IDefaultToken` directly -as siblings. There is no separate `ICoreToken` interface. The Default -token IS the canonical "ERC-20 + memos + roles + permits + policy + pause -+ URI + supply cap" surface that every variant inherits. - -**Why.** Predictable variant ABIs (every B-20 token has at least the -Default surface), no parallel interface to keep in sync, and the -practical observation that there is no realistic case where a B-20 token -would NOT want any of the Default features. Tokens that want to -permanently disable specific Default features use the capabilities -bitfield to opt out at creation. - -#### Single source of truth for compliance: external Policy Registry - -All B-20 tokens delegate transfer authorization to the policy engine -via `transferPolicyId`. There is no internal blocklist on the token -itself. Sanctions lists, KYC allowlists, jurisdiction restrictions, and -similar compliance rules all live in the policy registry as -whitelist/blacklist/compound policies. - -**Why.** Composability across tokens (one Coinbase-managed sanctions -blacklist policy serves every stablecoin AND every security AND every -default token that opts in), single auditable source for compliance -state, and no duplication of mechanism. CCS uses an internal blocklist; -we deliberately diverge to centralize this. - -The token-level `BURN_BLOCKED` capability bit is still per-token. It -controls whether the issuer can force-burn balance from policy-blocked -addresses (sanctions seizure flow). See "Freeze vs. seize" below. - -#### Memos as sibling functions, not optional parameters - -`transfer` / `transferWithMemo`, `mint` / `mintWithMemo`, `burn` / -`burnWithMemo` are paired. The non-memo variants are byte-for-byte -ERC-20 compatible. The `WithMemo` variants are B-20 extensions. - -**Why not a single function with optional `bytes32 memo`?** ERC-20 -selector compatibility. Existing wallets, indexers, and contracts that -call `transfer(address, uint256)` need to keep working without -modification. Adding an unused `bytes32` parameter changes the selector. -The sibling pair pattern preserves the ERC-20 selector for the -non-memo'd path and offers the memo'd alternative under a different -selector. - -The non-memo'd Transfer event is ALSO emitted on memo'd transfers (so -ERC-20 indexers see all token movement) along with the additional -`TransferWithMemo` event for indexers that want the memo. Same pattern -for mint/burn. - -#### No third-party dependencies - -The repo does not import OpenZeppelin, Tempo, Solady, or any other -third-party library. Reference implementations are written from scratch. -The reasoning, captured in the README: it's too easy to absorb someone -else's interface decisions wholesale instead of reaching our own -opinions. We can read prior art freely; we just don't link it. - -This means the reference implementations will reimplement things like -EIP-712, ECDSA, ERC-1271 dispatch, and OZ-style RBAC by hand. Treat -those as illustrative, not gas-optimal. - -### Roles & Admin model - -#### MINT_ROLE and BURN_ROLE are separate - -`IDefaultToken` exposes `MINT_ROLE` and `BURN_ROLE` as distinct role -identifiers. Originally combined as `ISSUER_ROLE` (TIP-20 convention), -we split them after reading CDP Custom Stablecoin (CCS), which has -them separate. - -**Why.** Operational separation of concerns: a treasury team might be -authorized to mint (issuance) without being able to burn (redemption is -a different process), and vice versa. Compromise of one role does not -compromise the other. The split costs essentially nothing on the -interface surface and gives genuine ops authority granularity. Tokens -that want unified mint+burn authority just grant both roles to the -same address. - -`burnBlocked` remains under `BURN_BLOCKED_ROLE` (separate from -`BURN_ROLE`). The two operations have very different blast radii: `burn` -destroys the caller's own balance; `burnBlocked` destroys someone else's -balance. - -#### PAUSE_ROLE and UNPAUSE_ROLE are separate - -Same pattern as TIP-20. Pause authority can be delegated to a 24/7 ops -team for emergency response without granting unpause authority. Unpause -is typically a more deliberate action requiring senior sign-off. - -#### Two-step admin transfer with delay - -`DEFAULT_ADMIN_ROLE` is the single most powerful role on a token. It -controls all other role assignments. An accidental transfer to a wrong -address (typo, key error, contract that can't accept) permanently -bricks all admin operations. To prevent this, we adopt the OZ -`AccessControlDefaultAdminRulesUpgradeable` pattern, also used by CCS. - -The mechanism: -- Admin transfer is a TWO-step process. The current admin calls - `beginDefaultAdminTransfer(newAdmin)`, scheduling the transfer. -- The new admin must call `acceptDefaultAdminTransfer()` after a - configurable `defaultAdminDelay` elapses. -- The current admin can `cancelDefaultAdminTransfer` at any time before - acceptance. -- `grantRole(DEFAULT_ADMIN_ROLE, ...)` and `revokeRole(DEFAULT_ADMIN_ROLE, ...)` - REVERT — the only valid transfer path is the two-step flow. - -The delay protects against key compromise. If an attacker steals the -admin key and immediately schedules a transfer to themselves, the -legitimate admin has `defaultAdminDelay` seconds to detect it and call -`cancelDefaultAdminTransfer`. There is also a `defaultAdminDelayIncreaseWait` -floor that prevents an admin from "instantly" extending the delay to -trap a rightful owner. - -`renounceRole(DEFAULT_ADMIN_ROLE)` is allowed but is itself scheduled -through the same mechanism (with `newAdmin == address(0)`). - -#### User-defined roles supported - -Beyond the named role identifiers, the generic `grantRole(bytes32, address)` -accepts any `bytes32` value. Issuers can compute their own role hashes -(`keccak256("MY_CUSTOM_ROLE")`) and use them for external integrations. -The token itself only checks the named roles internally; user-defined -roles have no built-in effect on token functions but can be consumed by -wrapper contracts. - -### Stablecoin-specific design - -#### Per-minter rate limiting (`STABLECOIN_MINT_RATE_LIMITED`) - -The single most distinctive feature CCS has over a vanilla ERC-20. -Each address holding `MINT_ROLE` has an independent rate-limit -configuration: a maximum capacity that replenishes linearly over a -configurable interval. `mint` calls consume from the caller's -remaining capacity. - -**Why this matters for stablecoins specifically:** -- **Risk management.** If a minter key is compromised, the blast radius - is bounded by their configured rate limit, not the entire supply cap. -- **Multi-party governance.** Different minters can have different - quotas reflecting different operational responsibilities (e.g. CDP - team has $X/day; treasury has $Y/day). -- **Operational compliance.** Per-team minting budgets enforce - business-process boundaries on chain. - -Default tokens typically have one issuer or none, and don't need this. -That's why the bit lives in the stablecoin range, not the default range. - -`grantMinterRoleWithLimit` is an atomic combo: grants `MINT_ROLE` and -configures the rate limit in one transaction. Avoids the race where a -freshly-granted minter has the role but no limit configured and reverts -on first mint attempt. Pattern from CCS. - -`MINT_RATE_LIMIT_ROLE` is held separately from `DEFAULT_ADMIN_ROLE` so -the authority that GRANTS minter access can be distinct from the -authority that TUNES per-minter quotas. - -#### ERC-3009 Transfer With Authorization (`STABLECOIN_AUTHORIZATIONS`) - -Gasless and front-run-resistant transfers. The user signs an EIP-712 -authorization off-chain; anyone (or specifically the recipient) submits -it on-chain. USDC has had this for years; it is essentially the price -of admission for stablecoins that want to be used in payment apps. - -**Distinct from EIP-2612 permit:** -- Permit sets allowances; ERC-3009 directly executes transfers. -- Permit uses sequential nonces; ERC-3009 uses random 32-byte nonces - so multiple authorizations can be in flight concurrently. -- Permit has no time-window beyond a deadline; ERC-3009 has both - `validAfter` and `validBefore` for scheduled-payment use cases. -- ERC-3009's `receiveWithAuthorization` is front-run-resistant: only - `to` can submit. Useful when the payer signs for a specific recipient - and wants no relayer to be able to redirect. -- `cancelAuthorization` lets the signer void an unused authorization - preemptively. Permit has no equivalent. - -Both are useful, complementary primitives. We expose both. - -#### Currency identifier - -`currency()` is an immutable string set at creation, identifying the -reference asset the stablecoin tracks (USD, EUR, BTC, etc.). Useful for -DEX routing, fee categorization, and wallet display. - -Convention follows ISO-4217 codes for fiat / commodity references and -asset symbols for non-ISO references. See the function's docstring for -the full convention. - -#### Freeze vs. seize philosophy - -CCS does NOT have a force-burn function. The strongest action against a -malicious holder is `blocklist`, which freezes the address's balance -without destroying it. This is the "freeze, never seize" philosophy. - -Tangor / Coinbase Tokenized Securities DOES have force-burn (called -`burnBlocked` in our interface). Sanctions enforcement requires the -ability to actually destroy the balance, not just freeze it. - -We support both via the `BURN_BLOCKED` capability bit. The -`STANDARD_STABLECOIN` preset OMITS `BURN_BLOCKED` to default to the CCS -philosophy. Issuers who want force-burn capability OR `BURN_BLOCKED` in -at creation. The `STANDARD_EQUITY` preset INCLUDES `BURN_BLOCKED` to -default to the Tangor philosophy. - -### Security-specific design - -#### Three issuance paths: `create`, `adminMint`, and inherited `mint` - -Securities have legally meaningful semantics around supply changes that -do not map cleanly to ERC-20's `mint`. We expose three issuance paths: - -- **`create(address to, uint256 amount)`**: the standard compliance- - friendly issuance path. Single-recipient, rate-limited per caller, - policy-checked. Distinct from `mint` because the legal definition of - "creation" of a security is operationally distinct from arbitrary - supply changes. -- **`adminMint(announcementId, recipients[], amounts[])`**: cold-path - batch mint with announcement coupling. Used for unusual or emergency - issuance (stock dividend distribution, recapitalization, error - correction). -- **Inherited `mint(address, uint256)`**: typically DISABLED on - security tokens via setting `MINTABLE = false` in capabilities. - Issuers use `create` and `adminMint` instead. - -The `STANDARD_EQUITY` preset reflects this: `MINTABLE` and `BURNABLE` -are off; `SECURITY_CREATABLE` and `SECURITY_ADMIN_BATCH` are on. - -#### User redemption (`redeem`) - -A holder calls `redeem(amount)` to destroy their tokens in exchange -for off-chain settlement to their brokerage account. This is distinct -from `burn` because it's user-initiated AND it triggers an off-chain -commitment from the issuer. - -Gated on a separate `redeemPolicyId` (see below). - -#### Policy engine scope: TIP-403 + TIP-1015 parity, no callback or richer guards in v1 - -We considered four levels of policy sophistication for v1: - -1. **Pure set membership** (TIP-403): WHITELIST, BLACKLIST. -2. **+ Compound policies** (TIP-1015): asymmetric sender / recipient / - mint-recipient slots referencing simple policies. -3. **+ Callback policies**: a fourth policy type that defers the - authorization decision to a designated contract via `staticcall`. - Solves time-, oracle-, lockup-, jurisdiction-, attestation-based - rules without bloating the precompile. -4. **+ Modular guards / hooks**: the Modular ERC20 vision. Per-operation - guard arrays, custom storage per guard, etc. - -We ship **Levels 1 + 2 only** in v1. - -The case for adding callback (Level 3) was real (richer rules without -chain bloat, small interface delta). But the forward-compat argument is -weak: even if we reserved the `CALLBACK` enum value now, the actual -implementation requires a hardfork — same as just adding it later. -Enum extensions are backward-compatible (existing values keep their -meanings), so consumers don't break when callback is added in a future -hardfork. Conclusion: defer to a future hardfork if real demand -emerges. - -The user-stories doc explicitly lists three types (allowlist, blocklist, -compound). Conner has consistently steered toward "fork Tempo cleanly." -Our v1 matches that exactly. - -**Rules that v1 DOES NOT support and would need future work:** -- Per-tx amount limits (callback signature lacks the amount) -- Counterparty-dependent rules ("X can only send to Y") -- Anything depending on per-transfer context - -For these, issuers wrap the precompile in a Solidity contract that does -the rich check before/after calling through to the registry. Standard -pattern; no chain change needed. - -#### Brokerage allowlist via separate `redeemPolicyId` - -Each security token holds two policy IDs: -- `transferPolicyId`: gates transfers and mints. Typically a compound - policy (e.g. KYC'd recipients, sanctions-blacklisted senders). -- `redeemPolicyId`: gates `redeem` callers. Typically a simple - whitelist of brokerage-verified accounts. Coinbase manages this list - by being the policy admin in the registry. - -**Why separate IDs?** Transfer-eligibility and redeem-eligibility are -different sets in practice. Retail can hold and trade a tokenized -security without being able to redeem to brokerage; redemption requires -KYC + brokerage account connection that not all holders have. Putting -both behind the same policy would force every holder to be brokerage- -verified. - -#### Announcement coupling for metadata changes - -Every state-changing operation that affects security identity (share -ratio updates, name/symbol changes, identifier updates, admin -mint/burn) must be paired with an `Announcement(id)` event emitted -earlier in the same transaction. The token enforces this via transient -storage at the implementation level. - -**Why on-chain enforcement, not just off-chain audit policy?** Strong -audit-trail invariant: it is impossible for a security token to change -identity without simultaneously emitting an announcement. Indexers, -exchanges, and wallets can rely on the chain itself to guarantee this. -The cost is a small transient-storage write per call; the benefit is -that audit reconstruction is mechanical rather than requiring trust in -the issuer's operational discipline. - -Per the user-stories doc, the announcement URI itself is event-only -(not stored on-chain). Indexers must scan event logs to retrieve URIs -for a given announcement. - -#### Share ratio for split-safe accounting - -A security token's underlying ERC-20 balance is the "raw" balance. -Holders' "share" count is `balance * denominator / numerator`. Stock -splits and reverse splits change the ratio; raw balances NEVER change. - -**Why.** A naive stock-split implementation that mints additional -tokens to every holder would break every smart contract that holds -the token (lending pools, AMMs, bridges, vaults) because those -contracts only know their deposit amount, not the post-split share -count. The ratio approach keeps raw balances stable so every smart -contract holder remains correct without modification; only the -displayed share count changes. - -Wallets and integrators call `sharesOf(account)` instead of -`balanceOf(account)` for display purposes. - ---- - -## Open Questions - -Items below need your input. Status legend: -- 🟡 **OPEN**: needs decision -- ✅ **RESOLVED**: confirmed and reflected in current code; kept for context -- 🔴 **VERIFY**: ambiguity in source docs; resolved one way but worth confirming - -### IDefaultToken - -#### 🟡 OPEN: Should there be a `MEMOS_REQUIRED` capability bit? - -Use case: a stablecoin issuer wants every transfer / mint / burn to -carry a non-zero memo for off-chain audit trail. With the bit set, the -non-memo'd `transfer` / `mint` / `burn` revert with `FeatureDisabled`. - -Cost: one extra capability bit, one extra runtime check on each -non-memo path. - -My lean: **add it**. Bit is cheap; it would be painful to add later. -Suggested bit position: `1 << 8`. Not added in current draft. - -#### ✅ RESOLVED: `renounceRole` exempt from `ADMIN_MUTABLE` - -Even on a token with `ADMIN_MUTABLE` off, role holders can voluntarily -renounce. For `DEFAULT_ADMIN_ROLE`, renunciation is scheduled through -the same two-step delay mechanism (with `newAdmin == address(0)`). - -#### ✅ RESOLVED: Default `transferPolicyId = 1` (always-allow) - -Default tokens default to always-allow at creation; security tokens -default to always-reject (paranoid) per their own surface (factory -parameter). Reflected in design intent; not yet enforced by interface -since defaults live in the impl/factory. - -#### 🔴 VERIFY: Pause does NOT block mints/burns/admin actions - -User stories doc is explicit; I went with that. Tangor's `pausedBurn` -function (which bypasses pause for admin burns) suggests they had a -"pause blocks everything" model and needed an escape hatch. Worth -confirming with Conner that the user-stories interpretation is the -intended one. - -### Capabilities - -#### 🟡 OPEN: Are `STANDARD_EQUITY`, `STANDARD_STABLECOIN`, `FIXED_SUPPLY` the right preset names / contents? - -Preset values: -- `STANDARD_STABLECOIN` includes per-minter rate limiting and ERC-3009; - OMITS `BURN_BLOCKED` (CCS-style freeze philosophy). -- `STANDARD_EQUITY` includes `BURN_BLOCKED` (Tangor-style sanctions - enforcement) and the security-specific bits. -- `FIXED_SUPPLY` is for default tokens with one-shot issuance. - -Worth verifying these match what real issuers (CCS, Tangor, Coinbase -Wrapped Assets) would actually want. - -### IStablecoin - -#### ✅ RESOLVED: Per-minter rate limiting added - -Reflects CCS pattern. `MINT_RATE_LIMIT_ROLE` configures, `MINT_ROLE` -mints. `grantMinterRoleWithLimit` atomic helper avoids first-mint -race. - -#### ✅ RESOLVED: ERC-3009 added - -Full surface (transfer, receive, cancel) with both ECDSA and ERC-1271 -sig variants. - -#### 🟡 OPEN: Reserve attestation accessor? - -Could add `reserveURI() returns (string)` for proof-of-reserves data, -or rely on contractURI's off-chain JSON. Not added in current draft. - -#### 🟡 OPEN: Yield distribution / rebase? - -For yield-bearing stablecoins like Base USD's planned design. Mechanics -are complex (rebase storage, snapshot timing, indexer compatibility). -Defer to dedicated design pass; not added. - -### ISecurityToken - -#### ✅ RESOLVED: `redeemPolicyId` separate from `transferPolicyId` - -Per the architectural recommendation. Brokerage allowlist managed via -the policy registry (Coinbase as policy admin). - -#### ✅ RESOLVED: Per-caller create rate limit configured via `DEFAULT_ADMIN_ROLE` - -Adopted the Tangor pattern (admin authority configures issuer quotas). -Could split out as `RATE_LIMIT_ADMIN_ROLE` later if needed. - -#### 🔴 VERIFY: Announcement URI is event-only, not stored on-chain - -User stories doc says event-only; wiki spec has on-chain getter. I -went with user stories. Indexers must scan logs to retrieve URIs. - -#### 🟡 OPEN: Should `Announcement` event index `id` for filterability? - -Currently `caller` is indexed; `id` is not. Indexers filtering by raw -string `id` would benefit from a separate `bytes32 indexed idHash` -field. Not added; flag if wanted. - -#### 🟡 OPEN: `adminMint` / `adminBurn` should accept `totalAmount` parameter for sum validation? - -Tangor's batch operations validate `totalAmount` matches the sum of -allocations. I omitted from current draft. Adds defense-in-depth -against caller-side off-by-one bugs at the cost of one extra parameter. - -#### 🟡 OPEN: `adminBurn` can affect any account (not just policy-blocked) given announcement coupling - -Powerful primitive: anyone with `BURN_BLOCKED_ROLE` + a posted -announcement can destroy any holder's balance. Use cases for -non-blocked accounts: liquidations, reverse tender settlements, -accounting corrections. Worth explicit confirmation. - -#### 🟡 OPEN: `share ratio` initial value at creation - -Tangor uses `1_000_000_000 / 1_000_000_000` (large 1:1, fractional -headroom). Wiki spec uses `1 / 1`. Factory/impl decision; not in -interface. My lean: 1:1. - -#### 🟡 OPEN: `pausedBurn` separate function vs. `adminBurn` always bypassing pause? - -Tangor has a separate `pausedBurn`. Current design: `adminBurn` always -bypasses pause. Simpler but less explicit about the "this is intended -to operate during pause" semantic. - ---- - -## What's NOT done yet - -1. **Reference Solidity implementations** of all three token variants - plus the factory and registry (`DefaultToken.sol`, `Stablecoin.sol`, - `SecurityToken.sol`, `TokenFactory.sol`, `PolicyRegistry.sol`). - Will be the biggest files in the repo. - -2. **`StdPrecompiles.sol`** equivalent — constants for the policy - registry, factory, and per-variant token address prefixes (TBD - addresses). - ---- - -## Notes on ITokenFactory unilateral choices - -A few non-obvious things I picked while drafting that you should react -to: - -### Per-variant deterministic address scheme - -`(variant, creator, salt) → address`. Variant is encoded in the address -prefix so `variantOf(token)` is a pure address-shape decode, no SLOAD. -Implies we reserve three address prefix ranges (one per variant) at -the chain config level. Specific prefix bytes are TBD; the interface -just promises determinism + variant-recoverability. - -### Initial supply mints bypass policy and capability checks - -For Default and Stablecoin, `initialSupply` is minted to -`initialSupplyRecipient` atomically at creation. This bypasses BOTH -the policy check (the recipient does not need to satisfy -`isAuthorizedMintRecipient` on `transferPolicyId`) AND the `MINTABLE` -capability check (the bootstrap mint works even on a token where -`MINTABLE = false`). - -Rationale: the policy and capability checks govern ongoing operation; -the initial mint is a one-time bootstrap configured by the creator -who is taking responsibility for the initial allocation. This makes -"fixed-supply meme coin" easy to express (set `MINTABLE = false`, -mint 1B at creation, done) without requiring temporary capability -gymnastics. - -If you'd rather have the initial mint go through the same checks as -runtime mints, easy to flip. Worth thinking about. - -### Security tokens have NO `initialSupply` parameter - -Security tokens use `create` (rate-limited) and `adminMint` (cold-path -batch with announcement coupling) for issuance. Bootstrap flow is: - -1. Factory creates the security token with no supply. -2. Admin configures the create() rate limit for one or more issuers - via `configureCreateRateLimit`. -3. Issuers call `create()` to mint to allocation recipients. - -Or the admin can use `adminMint` for a one-shot batch mint with an -announcement. Either path works; both produce more audit-trail than a -silent bootstrap mint. - -### `defaultAdminDelay` configurable at creation - -Each token's two-step admin transfer delay is set per-token at -creation via `defaultAdminDelay`. Different tokens can have different -delays based on their security posture (a stablecoin might want hours; -a memecoin might want zero). Admin can change later via -`changeDefaultAdminDelay` if the token's `ADMIN_MUTABLE` capability -is on. - -### `predict*Address` does not depend on params other than `(creator, salt)` - -The predicted address is stable across changes to name / symbol / -admin / capabilities / etc. — only `(variant, creator, salt)` -contributes. This lets callers compute the address before deciding -all the params, and lets pre-funding flows work without committing -to params upfront. - -### Factory is permissionless - -Anyone can call any create method. There is no `DEPLOYER_ROLE` or -similar; the factory itself has no admin. Each created token has its -own independent admin and is fully self-governing thereafter. - ---- - -## Summary of bits I want explicit confirmation on - -After your "yes to all" on the CCS-derived additions, the remaining -items needing your input: - -1. `MEMOS_REQUIRED` capability bit — add now or defer? (My lean: add) -2. Preset contents (`STANDARD_STABLECOIN`, `STANDARD_EQUITY`, - `FIXED_SUPPLY`) — confirm reasonable defaults? -3. Reserve attestation accessor on IStablecoin — add or defer to - off-chain JSON? -4. Indexed `bytes32 idHash` on `Announcement` event — add for - filterability? -5. `adminMint` / `adminBurn` `totalAmount` parameter for sum - validation — add? -6. `adminBurn` semantics — confirm it can affect any account, gated by - `BURN_BLOCKED_ROLE` + announcement coupling, NOT restricted to - policy-blocked addresses? -7. `share ratio` default at creation — 1:1 or 1e9:1e9? -8. `pausedBurn` as a separate function vs. `adminBurn` always - bypassing pause? -9. The pause-doesn't-block-mints/burns interpretation (per user - stories) — confirm? - -Once you weigh in, I'll iterate the interfaces, then write -`ITokenFactory` + `IPolicyRegistry`, then start on reference impls. +# Design Notes + +The interfaces in `src/interfaces/` enshrine the minimum protocol-level +surface needed for a Base-native ERC-20: balance state, raw transfer +mechanics, a policy hook for compliance, a pause flag, and a binding +to an EVM wrapper contract that owns all governance and product +behavior. Everything else is wrapper-level. + +## The architectural unit: precompile + wrapper + +A B-20 token is a **pair**: + +- A **precompile** (`IB20`) that owns the balance state, the raw + transfer mechanics, the policy hook, and the pause flag. +- An **EVM wrapper contract** that owns all authorization, all role + logic, and all product-specific behavior. + +The pair is created atomically by the factory, and the binding is +permanent: the precompile knows its wrapper address (`wrapper()`), +and only that wrapper can call the precompile's privileged operations. + +The model is the OZ `_internal` / `public` split, but enforced at the +protocol level rather than by Solidity inheritance: + +- **Precompile-public** functions (callable by anyone): the standard + ERC-20 surface (`transfer`, `transferFrom`, `approve`, plus + metadata and balance reads) and read-only state accessors + (`wrapper`, `paused`, `transferPolicyId`). +- **Precompile-internal** functions (callable only by the wrapper): + `mint`, `burn`, `wrapperTransfer`, `setPaused`, + `setTransferPolicyId`. Revert with `OnlyWrapper` for any other + caller. + +The wrapper is a normal EVM contract. Issuers design it to fit their +product: + +- A **stablecoin wrapper** adds RBAC, per-minter rate limits, EIP-2612 + permit, ERC-3009 transfer-with-authorization, paying-agent + integrations. +- A **security token wrapper** adds corporate actions, share-ratio + accounting, brokerage redeem flows, announcement-coupled metadata + changes. +- A **minimal wrapper** adds nothing and exposes a thin `mint` / + `burn` API gated by `Ownable`. + +### Wrapper upgradability + +The wrapper address is immutable on the precompile, set at creation. +If the wrapper bytecode needs to change post-creation (bug fix, new +feature), the wrapper itself uses a proxy pattern (transparent proxy, +beacon proxy, etc.). The proxy is the immutable wrapper from the +precompile's perspective; the implementation behind the proxy is +upgradable per the proxy's own governance rules. + +This pushes all upgrade complexity into EVM, where it's well- +understood. The precompile carries zero state related to upgrade +authority. + +## Design principles + +### Minimum enshrinement + +Anything in a precompile is permanent until a hardfork. The bar for +inclusion is whether the feature must live at the protocol level +because either (a) it cannot be expressed in EVM at all, or (b) +protocol-level behavior depends on it (most importantly, gas +payment). + +Specifically included: + +1. **ERC-20 surface** (balances, transfers, approvals, metadata). + Required for ecosystem compatibility and so the chain can read + token state during gas payment. +2. **Mint and burn**, callable only by the bound wrapper. Required + because the precompile owns supply state. +3. **Policy hook** (`transferPolicyId`). Required so compliance + enforcement applies at the protocol level, including during gas + payment. +4. **Pause flag**. Required so the same protocol-level halt applies + to gas payment as to user transfers. +5. **`wrapperTransfer`**. Required to support wrapper-mediated + authorization flows (permit, ERC-3009) without forcing every + user to maintain a precompile-level allowance. +6. **Wrapper binding**. Required to authorize `mint` / `burn` / + `setPaused` / `setTransferPolicyId` / `wrapperTransfer`. One + immutable address answers all "who can call this?" questions for + privileged operations. + +Specifically excluded (lives in wrappers): + +- Role-based access control of any flavor (admin, issuer, pauser, + minter, RBAC, OZ AccessControl). +- Two-step admin or issuer rotation. +- Configurable transfer delays. +- EIP-2612 permit, ERC-3009, transfer memos. +- Per-minter rate limiting. +- Supply caps. +- Contract URI, currency identifier, asset type, share ratio, + corporate actions, dividend distribution, brokerage redeem flows. + +### Policies are the single source of truth for compliance + +Every balance-mutating operation that moves value passes through the +policy registry referenced by `transferPolicyId`. The hook fires on: + +- **Transfers** (`transfer`, `transferFrom`, `wrapperTransfer`): + `isAuthorizedSender(transferPolicyId, from)` and + `isAuthorizedRecipient(transferPolicyId, to)`. +- **Mints**: `isAuthorizedMintRecipient(transferPolicyId, to)`. The + caller authority check is the wrapper check, not a policy check. +- **Burns**: no policy check. Burns reduce supply and are + deliberately available for compliance enforcement (sanctions + seizure). Authority is the wrapper check. + +A single shared policy can serve every B-20 token on the chain +(sanctions list, KYC allowlist, jurisdiction restrictions, etc.). To +update sanctions across all B-20 tokens, you update one policy in one +place. + +### Gas payment is a transfer + +When a B-20 token is configured as a gas asset, fee debits flow +through the same `transferPolicyId` check as user-initiated +transfers. The chain does NOT special-case fee payment to bypass the +policy. A sanctioned account that cannot transfer also cannot pay +gas in the token. A paused token cannot pay gas at all. This is the +intended behavior and is the primary justification for enshrining +both the policy hook and the pause flag alongside the balance state. + +The corollary: tokens that want to be gas assets must register a +policy that doesn't accidentally lock out legitimate users from fee +payment. Wrapper / issuer responsibility, not a protocol concern. + +### `wrapperTransfer` rationale + +`transferFrom` requires the caller to have an allowance from `from`. +For wrapper-mediated flows like permit and ERC-3009, the user signs +authorization off-chain and the wrapper executes the transfer; there +is no on-chain allowance to consume. + +`wrapperTransfer(from, to, amount)` is the precompile's escape hatch +for this case. Wrapper-only. Skips the allowance check. Subject to +the same policy and pause checks as `transfer`. The wrapper is +trusted (because the precompile only allows it as the caller) to +have verified user authorization off-chain. + +Without this, the wrapper would have to maintain its own per-account +shadow allowance state, or require every user to first +`approve(wrapper, type(uint256).max)`. Both are awkward and gas- +expensive compared to a single privileged call. + +### No third-party dependencies + +Reference implementations are written from scratch. We can read +OpenZeppelin, Tempo, CCS, Tangor, and other prior art for +inspiration; we don't import. The reasoning: it's too easy to absorb +someone else's interface decisions wholesale instead of reaching our +own opinions. Revisit when the interfaces stabilize. + +## What lives in EVM wrappers + +Almost everything that isn't on the `IB20` surface. Concrete +examples, each previously considered for enshrinement and explicitly +moved out: + +- **Authorization for privileged operations**: who can call `mint`, + `burn`, `setPaused`, `setTransferPolicyId`. The wrapper exposes its + own public `mint(...)` / `pause()` / etc. with whatever auth model + it wants (single owner, multi-sig, RBAC, timelock, governance + contract) and delegates to the precompile. +- **Two-step admin / issuer rotation with delays**: standard OZ + `AccessControlDefaultAdminRules` pattern in the wrapper. The + precompile knows nothing about it. +- **EIP-2612 permit / ERC-3009 transfer with authorization**: gasless + flows. Wrapper verifies the signature and calls + `precompile.wrapperTransfer(from, to, amount)`. +- **Memos / transfer metadata**: wrapper exposes + `transferWithMemo(...)` that emits a memo event before delegating + to the precompile. +- **Per-minter rate limiting**: wrapper tracks per-caller quotas in + its own storage and enforces them on its public `mint(...)`. +- **Corporate actions, share ratio, brokerage redeem**: wrapper + exposes typed action functions and composes the precompile + primitives. +- **Currency identifier, contract URI, asset type, reserve + attestation URI, etc.**: wrapper exposes the views. + +Pattern: the precompile owns balance and compliance primitives; +wrappers compose those primitives into product-specific behavior. + +## Policy registry + +`IPolicyRegistry` is a singleton precompile at a fixed address. Three +policy types in v1: WHITELIST, BLACKLIST, and COMPOUND. COMPOUND +policies reference three constituent simple policies (sender, +recipient, mint-recipient slots) for asymmetric rules. + +Policy IDs `0` (always-reject) and `1` (always-allow) are built-in; +custom IDs start at `2` and are assigned monotonically. Anyone may +create policies; the creator picks the policy admin (typically +themselves or a multi-sig). + +Adapted from Tempo TIP-403 + TIP-1015. Three deliberate omissions: + +- **No virtual-address rejection (TIP-1022)**: incompatible with our + hard requirement that B-20 tokens coexist with the existing Base + ERC-20 ecosystem and addresses. +- **No receive policies (TIP-1028)**: no concept of escrow on Base. +- **No callback / richer guard policies**: extending the enum later + is backward-compatible, and reserving the value now buys nothing + because the actual implementation requires a hardfork either way. + Defer. + +For rules outside the WHITELIST / BLACKLIST / COMPOUND vocabulary +(per-tx amount limits, time-windowed access, oracle-driven gating, +attestation-based eligibility), wrappers compose: do the rich check +first, then call `precompile.transferFrom` or `wrapperTransfer`. + +## Pause semantics + +Pause is a boolean read by the precompile on every balance-mutating +path. While paused: + +- `transfer`, `transferFrom`, `mint`, `burn`, `wrapperTransfer` + revert with `ContractPaused`. +- `approve`, `setPaused`, `setTransferPolicyId` remain available so + the wrapper can prepare state changes while the token is halted. + +The precompile knows nothing about pause authorization; `setPaused` +is wrapper-only. The wrapper exposes whatever pause API it wants +(simple admin pause, multi-sig pause, time-locked pause, separate +pause and unpause roles, etc.). + +`setPaused` is idempotent: calling with the current value is a no-op +and emits no event. + +## What's not in this draft + +- **Token factory.** The factory's job is to atomically deploy the + precompile + wrapper pair, bind them, and register the binding. + Open whether this is itself a precompile or some other chain- + config flow. +- **Reference wrappers.** Likely follow once the precompile interface + stabilizes: + - `MinimalWrapper.sol`: single-owner, exposes raw `mint` / `burn` / + `pause` / `setTransferPolicyId` with `Ownable`-style auth. + Suitable for memecoins, fixed-supply tokens, simple cases. + - `StablecoinWrapper.sol`: RBAC + per-minter rate limits + EIP-2612 + permit + ERC-3009. Suitable for CCS-style stablecoins. + - `SecurityTokenWrapper.sol`: corporate actions + share ratio + + brokerage redeem + announcement coupling. Suitable for tokenized + equities. +- **Reference Solidity implementation of `IB20`**. + +## Open questions + +1. **Wrapper rotation**: should the precompile expose any path for + the bound wrapper to designate a successor (one-step or two-step + wrapper swap)? Default position: no, wrappers are immutable; use + a proxy pattern internally if upgrades are needed. Argument for + adding: simpler for issuers who don't want to deal with proxies. + Argument against: re-introduces governance state on the + precompile. +2. **Initial supply at creation**: should the factory accept an + `initialSupply` parameter that bootstraps the token before any + wrapper-mediated mint? Bypasses the policy check (no policy may + be configured yet). Useful for fixed-supply tokens. +3. **Burn-during-pause**: currently burn is wrapper-only and blocked + by pause. Should burn bypass pause so wrappers can execute + compliance burns even during a halt? Or stays as-is? +4. **`setTransferPolicyId` validation**: the precompile reverts with + `InvalidPolicyId` if the policy doesn't exist in the registry. + This requires a registry call on every set. Cheap, but worth + confirming we want the validation rather than trusting the + wrapper to set valid IDs. diff --git a/src/interfaces/Capabilities.sol b/src/interfaces/Capabilities.sol deleted file mode 100644 index 4cb7184..0000000 --- a/src/interfaces/Capabilities.sol +++ /dev/null @@ -1,150 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.20 <0.9.0; - -/// @title Capabilities -/// @notice Bit flags identifying optional features on a Base-native token (B-20). -/// A token's `capabilities()` value is set at creation by the factory -/// and is permanent. Functions whose capability bit is unset revert -/// with `FeatureDisabled`, regardless of role state. Functions whose -/// bit IS set are subject to the normal role-based access control on -/// 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. -library Capabilities { - /*////////////////////////////////////////////////////////////// - Default token bits (0..15) - //////////////////////////////////////////////////////////////*/ - - /// @notice `pause()` and `unpause()` are callable. - 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; - - /*////////////////////////////////////////////////////////////// - Security-token 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). - 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). - 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). - uint256 internal constant SHARE_RATIO_MUTABLE = 1 << 18; - - /// @notice On a Security token, `updateName` / `updateSymbol` / - /// `updateSecurityIdentifier` are callable. When unset, the - /// identifying metadata set at creation is permanent. - uint256 internal constant SECURITY_METADATA_MUTABLE = 1 << 19; - - /// @notice On a Security token, `adminMint()` and `adminBurn()` are - /// callable. When unset, the cold-path batch operations are - /// permanently disabled. - uint256 internal constant SECURITY_ADMIN_BATCH = 1 << 20; - - /*////////////////////////////////////////////////////////////// - Stablecoin-token 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; - - /*////////////////////////////////////////////////////////////// - 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 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. - 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 - /// governance. - uint256 internal constant FIXED_SUPPLY = PAUSABLE | ADMIN_MUTABLE | POLICY_MUTABLE | URI_MUTABLE; - - /// @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; -} diff --git a/src/interfaces/IB20.sol b/src/interfaces/IB20.sol new file mode 100644 index 0000000..fbcef56 --- /dev/null +++ b/src/interfaces/IB20.sol @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.20 <0.9.0; + +/// @title IB20 +/// @notice The protocol-enshrined precompile half of a Base-native ERC-20. +/// A B-20 token is an architectural pair: this precompile (which +/// owns balance state, raw transfer mechanics, the policy hook, +/// and the pause flag) and an EVM wrapper contract bound to it at +/// creation. The precompile is opinion-free; all governance, role +/// logic, and product-specific behavior live in the wrapper. +/// +/// @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 and event signatures. +/// +/// **Wrapper binding.** Each precompile is bound to exactly one +/// wrapper EVM contract at creation, retrievable via `wrapper()`. +/// The binding is immutable. The precompile's privileged +/// operations (`mint`, `burn`, `wrapperTransfer`, `setPaused`, +/// `setTransferPolicyId`) revert with `OnlyWrapper` for any other +/// caller. Wrapper upgradability, when needed, is achieved via +/// standard EVM proxy patterns inside the wrapper itself. +/// +/// **Policy hook.** Every balance-mutating operation that moves +/// value (`transfer`, `transferFrom`, `mint`, `wrapperTransfer`) +/// passes through the policy registry referenced by +/// `transferPolicyId`. Burns are NOT policy-checked: they reduce +/// supply rather than move value to a recipient and are +/// deliberately available for compliance enforcement (sanctions +/// seizure). +/// +/// **Gas payment is a transfer.** When this token is configured +/// as a gas asset, fee debits go through the same +/// `transferPolicyId` check as user-initiated transfers. A +/// sanctioned account that cannot transfer also cannot pay gas +/// in this token. A paused token cannot pay gas at all. +interface IB20 { + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + /// @notice The caller is not the bound wrapper. The privileged + /// operations on this precompile (mint, burn, wrapperTransfer, + /// setPaused, setTransferPolicyId) are callable only by the + /// wrapper EVM contract bound at creation. + error OnlyWrapper(); + + /// @notice The token is paused. Transfers, mints, burns, and + /// wrapperTransfers are all blocked while paused. `approve` + /// and the wrapper-only state setters remain available. + error ContractPaused(); + + /// @notice The owner does not have enough balance for the requested + /// transfer or burn. + error InsufficientBalance(uint256 currentBalance, uint256 requestedAmount); + + /// @notice The spender does not have enough allowance for the + /// requested `transferFrom`. + error InsufficientAllowance(uint256 currentAllowance, uint256 requestedAmount); + + /// @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 InvalidPolicyId(uint64 policyId); + + /// @notice A required address argument was the zero address. + error InvalidRecipient(); + + /// @notice The amount argument was zero where a non-zero value is + /// required. + error InvalidAmount(); + + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + /// @notice ERC-20 standard transfer event. Emitted on every + /// successful transfer (including `transferFrom` and + /// `wrapperTransfer`), mint (`from = address(0)`), and burn + /// (`to = address(0)`). + event Transfer(address indexed from, address indexed to, uint256 amount); + + /// @notice ERC-20 standard approval event. + event Approval(address indexed owner, address indexed spender, uint256 amount); + + /// @notice The token was paused. + event Paused(); + + /// @notice The token was unpaused. + event Unpaused(); + + /// @notice The transfer policy was changed. `oldPolicyId` and + /// `newPolicyId` are the policy IDs immediately before and + /// after the change. + event TransferPolicyUpdated(uint64 oldPolicyId, uint64 newPolicyId); + + /*////////////////////////////////////////////////////////////// + ERC-20 + //////////////////////////////////////////////////////////////*/ + + /// @notice Token name. Immutable, set at creation. + function name() external view returns (string memory); + + /// @notice Token symbol. Immutable, set at creation. + function symbol() external view returns (string memory); + + /// @notice Number of decimal places. Immutable, set at creation. + function decimals() external view returns (uint8); + + /// @notice Total supply of the token. + 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` if paused, `PolicyForbids` if the active + /// transfer policy denies the transfer, `InsufficientBalance` + /// if `msg.sender` does not have enough balance, and + /// `InvalidRecipient` if `to == address(0)`. + /// @dev The same policy check applies to fee payment when this + /// token is configured as a gas asset. + 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` if `msg.sender` does not have + /// enough allowance from `from`. + 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. + function approve(address spender, uint256 amount) external returns (bool); + + /*////////////////////////////////////////////////////////////// + STATE READS + //////////////////////////////////////////////////////////////*/ + + /// @notice The EVM wrapper contract bound to this precompile at + /// creation. Immutable. Holds exclusive authority to call + /// this precompile's privileged operations (`mint`, `burn`, + /// `wrapperTransfer`, `setPaused`, `setTransferPolicyId`). + /// If the wrapper needs to be upgraded post-creation, it + /// must use a proxy pattern internally; the precompile sees + /// this address forever. + function wrapper() external view returns (address); + + /// @notice The policy ID currently gating this token's transfers, + /// mints, and wrapperTransfers. Resolved via the canonical + /// policy registry precompile. ID `0` always rejects + /// (functional soft-pause via policy); ID `1` always allows. + function transferPolicyId() external view returns (uint64); + + /// @notice Whether the token is currently paused. While paused, + /// `transfer`, `transferFrom`, `mint`, `burn`, and + /// `wrapperTransfer` revert with `ContractPaused`. `approve` + /// and the wrapper-only state setters remain available so + /// the wrapper can prepare state changes while the token is + /// halted. + function paused() external view returns (bool); + + /*////////////////////////////////////////////////////////////// + WRAPPER-ONLY OPERATIONS + //////////////////////////////////////////////////////////////*/ + + /// @notice Mints `amount` to `to`. Caller MUST be the bound + /// `wrapper`. Subject to the transfer policy: + /// `isAuthorizedMintRecipient(to)` must hold. Blocked by + /// pause. Emits `Transfer(address(0), to, amount)`. + function mint(address to, uint256 amount) external; + + /// @notice Burns `amount` from `from`. Caller MUST be the bound + /// `wrapper`. NOT subject to the transfer policy: burns + /// reduce supply rather than move value to a recipient and + /// are deliberately available for compliance enforcement + /// (e.g. seizure of sanctioned balances). Blocked by pause. + /// Emits `Transfer(from, address(0), amount)`. + function burn(address from, uint256 amount) external; + + /// @notice Transfers `amount` from `from` to `to` WITHOUT consulting + /// the precompile-level allowance. Caller MUST be the bound + /// `wrapper`. Subject to the transfer policy and to pause, + /// identical to `transfer` and `transferFrom` in those + /// respects. Emits `Transfer(from, to, amount)`. + /// @dev The escape hatch for wrapper-mediated authorization + /// flows: EIP-2612 permit, ERC-3009 transfer with + /// authorization, signature-based payment intents, etc. + /// The wrapper verifies user authorization off-chain + /// (typically via signature recovery) and is trusted to + /// only call this when the user has actually authorized the + /// transfer. The precompile-level allowance from `from` is + /// irrelevant; the wrapper IS the authorization. + function wrapperTransfer(address from, address to, uint256 amount) external returns (bool); + + /// @notice Sets the paused state. Caller MUST be the bound + /// `wrapper`. Idempotent: calling with the current value is + /// a no-op and does not emit an event. Calling with a new + /// value emits `Paused()` or `Unpaused()` accordingly. + function setPaused(bool isPaused) external; + + /// @notice Sets a new transfer policy. Caller MUST be the bound + /// `wrapper`. The policy MUST exist in the policy registry + /// (or be one of the built-in IDs `0` or `1`). Takes effect + /// immediately for the next transfer or mint. + function setTransferPolicyId(uint64 newPolicyId) external; +} diff --git a/src/interfaces/IDefaultToken.sol b/src/interfaces/IDefaultToken.sol deleted file mode 100644 index a03ab13..0000000 --- a/src/interfaces/IDefaultToken.sol +++ /dev/null @@ -1,413 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.20 <0.9.0; - -/// @title IDefaultToken -/// @notice The base Solidity surface every Base-native token (B-20) implements. -/// 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. -/// -/// 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. -interface IDefaultToken { - /*////////////////////////////////////////////////////////////// - ERRORS - //////////////////////////////////////////////////////////////*/ - - error Unauthorized(); - error ContractPaused(); - error AlreadyPaused(); - error NotPaused(); - error InsufficientAllowance(); - error InsufficientBalance(uint256 currentBalance, uint256 requestedAmount); - error InvalidAmount(); - error InvalidRecipient(); - error InvalidSupplyCap(); - error SupplyCapExceeded(); - error PolicyForbids(); - error ProtectedAddress(); - error PermitExpired(); - error InvalidSignature(); - error FeatureDisabled(uint256 capability); - error EnforcedDefaultAdminRules(); - error InvalidDefaultAdmin(address account); - error EnforcedDefaultAdminDelay(uint48 schedule); - error NoPendingDefaultAdmin(); - - /*////////////////////////////////////////////////////////////// - EVENTS - //////////////////////////////////////////////////////////////*/ - - 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); - - event PauseStateUpdated(address indexed updater, bool isPaused); - - event TransferPolicyUpdated(address indexed updater, uint64 indexed newPolicyId); - - event SupplyCapUpdated(address indexed updater, uint256 newSupplyCap); - - event ContractURIUpdated(); - - event DefaultAdminTransferScheduled(address indexed newAdmin, uint48 acceptSchedule); - event DefaultAdminTransferCanceled(); - event DefaultAdminDelayChangeScheduled(uint48 newDelay, uint48 effectSchedule); - event DefaultAdminDelayChangeCanceled(); - - /*////////////////////////////////////////////////////////////// - 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`. - 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). - function MINT_ROLE() external view returns (bytes32); - - /// @notice Required to call `burn` / `burnWithMemo`. See MINT_ROLE for - /// rationale on the split. - 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. - function PAUSE_ROLE() external view returns (bytes32); - - /// @notice Required to call `unpause`. See PAUSE_ROLE for rationale on the split. - 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. - function capabilities() external view returns (uint256); - - /// @notice Convenience views for individual capability bits. Each returns - /// `(capabilities() & Capabilities.X) != 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); - function isCapMutable() external view returns (bool); - function isURIMutable() external view returns (bool); - - /*////////////////////////////////////////////////////////////// - ERC-20 - //////////////////////////////////////////////////////////////*/ - - function name() external view returns (string memory); - - function symbol() external view returns (string memory); - - function decimals() external view returns (uint8); - - function totalSupply() external view returns (uint256); - - function balanceOf(address account) external view returns (uint256); - - function allowance(address owner, address spender) external view returns (uint256); - - function transfer(address to, uint256 amount) external returns (bool); - - function transferFrom(address from, address to, uint256 amount) external returns (bool); - - function approve(address spender, uint256 amount) external returns (bool); - - /*////////////////////////////////////////////////////////////// - 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. - function transferWithMemo(address to, uint256 amount, bytes32 memo) external returns (bool); - - /// @notice Same as `transferFrom`, with a memo. See `transferWithMemo`. - function transferFromWithMemo(address from, address to, uint256 amount, bytes32 memo) external returns (bool); - - /*////////////////////////////////////////////////////////////// - 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)`. - function mint(address to, uint256 amount) external; - - /// @notice Same as `mint`, with a 32-byte memo. Emits `MintWithMemo` in - /// addition to `Mint` and `Transfer`. - 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)`. - function burn(uint256 amount) external; - - /// @notice Same as `burn`, with a 32-byte memo. Emits `BurnWithMemo` in - /// addition to `Burn` and `Transfer`. - 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; - - /*////////////////////////////////////////////////////////////// - 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. - 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`. - 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. - 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`. - 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. - 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. - 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`. - 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 - /// transfer or mint. - function changeTransferPolicyId(uint64 newPolicyId) external; - - /*////////////////////////////////////////////////////////////// - SUPPLY CAP - //////////////////////////////////////////////////////////////*/ - - /// @notice The maximum total supply enforced on `mint`. A value of - /// `type(uint256).max` indicates no cap (the default). - 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). - function setSupplyCap(uint256 newSupplyCap) external; - - /*////////////////////////////////////////////////////////////// - PERMIT (EIP-2612 + ERC-1271) - //////////////////////////////////////////////////////////////*/ - - /// @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`. - 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. - 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) - external; - - /*////////////////////////////////////////////////////////////// - CONTRACT URI (ERC-7572) - //////////////////////////////////////////////////////////////*/ - - /// @notice An offchain URI pointing at contract-level metadata for this - /// token (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. - function setContractURI(string calldata newURI) external; -} diff --git a/src/interfaces/ISecurityToken.sol b/src/interfaces/ISecurityToken.sol deleted file mode 100644 index 6b54cd8..0000000 --- a/src/interfaces/ISecurityToken.sol +++ /dev/null @@ -1,280 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.20 <0.9.0; - -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. -/// -/// 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. -interface ISecurityToken is IDefaultToken { - /*////////////////////////////////////////////////////////////// - ERRORS - //////////////////////////////////////////////////////////////*/ - - error AnnouncementRequired(string id); - error AnnouncementIdAlreadyUsed(string id); - error InvalidShareRatio(); - error CreateRateLimitExceeded(address caller); - error RedeemNotAuthorized(address caller); - error RedeemBelowMinimum(uint256 amount, uint256 minimum); - error InvalidIdentifierType(); - - /*////////////////////////////////////////////////////////////// - EVENTS - //////////////////////////////////////////////////////////////*/ - - /// @notice A holder-impacting announcement. Posted before any - /// 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. - event ShareRatioUpdated( - address indexed caller, - string announcementId, - uint48 oldNumerator, - uint48 oldDenominator, - uint48 newNumerator, - 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. - event SecurityIdentifierUpdated( - address indexed caller, string announcementId, string identifierType, string value - ); - - /// @notice Supply created via the compliant issuance path. - event Created(address indexed to, uint256 amount); - - /// @notice Supply created via the cold-path admin batch. - event AdminMinted(address indexed caller, string announcementId, uint256 totalAmount); - - /// @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); - event CreateRateLimitConfigured(address indexed caller, uint256 maxAmount, uint256 interval); - - /*////////////////////////////////////////////////////////////// - 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. - function ANNOUNCE_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. - /// @dev Requires `ANNOUNCE_ROLE`. Reverts with - /// `AnnouncementIdAlreadyUsed` on `id` reuse. - function announcement(string calldata id, string calldata description, string calldata uri) external; - - /// @notice Whether the given announcement ID has been consumed. - function isAnnouncementIdUsed(string calldata id) external view returns (bool); - - /*////////////////////////////////////////////////////////////// - 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. - 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 - /// `balance * denominator / numerator`. - function toShares(uint256 balance) external view returns (uint256); - - /// @notice Convenience: `toShares(balanceOf(account))`. - function sharesOf(address account) external view returns (uint256); - - /// @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. - 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. - function create(address to, uint256 amount) external; - - /// @notice The remaining create allowance for `caller` under their - /// current rate-limit configuration. - function createAllowance(address caller) external view returns (uint256); - - /// @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. - 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. - 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, ...)` - /// emitted earlier in the same transaction with the same - /// `announcementId`. Reverts atomically if any single account - /// lacks sufficient balance. - 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. - 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. - 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`. - function updateSecurityIdentifier( - string calldata announcementId, - string calldata identifierType, - string calldata value, - bool remove - ) external; - - /*////////////////////////////////////////////////////////////// - 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. - 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. - function updateSymbol(string calldata announcementId, string calldata newSymbol) external; -} diff --git a/src/interfaces/IStablecoin.sol b/src/interfaces/IStablecoin.sol deleted file mode 100644 index c71d12d..0000000 --- a/src/interfaces/IStablecoin.sol +++ /dev/null @@ -1,223 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.20 <0.9.0; - -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: -/// -/// 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 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. -/// -/// 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. -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. - /// @dev Two stablecoins tracking the same asset return the same - /// identifier. Conventions: - /// - ISO-4217 codes for fiat / commodity references: "USD", - /// "EUR", "JPY", "XAU" (gold), "XAG" (silver). - /// - Symbol for non-ISO references: "BTC", "ETH" (for tokens - /// tracking the price of those assets). - /// - The token's own symbol if it tracks no external reference - /// (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 deleted file mode 100644 index e516ef8..0000000 --- a/src/interfaces/ITokenFactory.sol +++ /dev/null @@ -1,271 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.20 <0.9.0; - -/// @title ITokenFactory -/// @notice Singleton factory for creating B-20 tokens of any variant. -/// A single precompile at a fixed address exposes three creation -/// methods (`createDefault`, `createStablecoin`, `createSecurity`). -/// Creation is permissionless: anyone may create a token of any -/// variant, and the creator picks the initial admin. -/// -/// @dev Each token is deployed at a deterministic address derived from -/// `(variant, creator, salt)`. The variant is encoded in the -/// address prefix, so the variant of any address is recoverable -/// via `variantOf` without a storage lookup. Address prediction -/// functions (`predict*Address`) let callers compute the address -/// off-chain or pre-fund the address before deployment. -/// -/// The factory is a precompile and has no admin or governance. -/// Each created token has its own independent admin and operates -/// per the inherited `IDefaultToken` (and variant) surface. -interface ITokenFactory { - /*////////////////////////////////////////////////////////////// - TYPES - //////////////////////////////////////////////////////////////*/ - - /// @notice Variant of a B-20 token. Recoverable from the token's - /// address prefix; `NONE` indicates the address is not a B-20 - /// token created by this factory. - enum TokenVariant { - NONE, - DEFAULT, - STABLECOIN, - SECURITY - } - - /// @notice Creation parameters for a Default-variant token. - /// @param name ERC-20 token name. - /// @param symbol ERC-20 token symbol. - /// @param decimals ERC-20 token decimals (issuer choice). - /// @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 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). - /// @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. - /// @param contractURI Initial ERC-7572 contract URI. - /// @param salt Caller-chosen salt for deterministic - /// address derivation. - struct CreateDefaultTokenParams { - string name; - string symbol; - uint8 decimals; - address admin; - uint48 defaultAdminDelay; - uint256 capabilities; - uint256 initialSupply; - address initialSupplyRecipient; - uint64 transferPolicyId; - uint256 supplyCap; - 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. - /// @dev All other fields have the same semantics as the Default - /// params struct. - struct CreateStablecoinParams { - string name; - string symbol; - uint8 decimals; - address admin; - uint48 defaultAdminDelay; - uint256 capabilities; - uint256 initialSupply; - address initialSupplyRecipient; - uint64 transferPolicyId; - uint256 supplyCap; - 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 - /// fractional ratio updates. - /// @param shareRatioDenominator Initial share-ratio denominator. - /// Must be non-zero. - /// @param securityIdentifiers Initial `[type, value]` pairs (e.g. - /// `[["isin", "US..."], ["cusip", "..."]]`). - /// May be empty; identifiers can be - /// added later via - /// `updateSecurityIdentifier`. - /// @dev Security tokens have NO `initialSupply` parameter. All - /// 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. - struct CreateSecurityTokenParams { - string name; - string symbol; - uint8 decimals; - address admin; - uint48 defaultAdminDelay; - uint256 capabilities; - uint64 transferPolicyId; - uint64 redeemPolicyId; - uint256 minimumRedeemable; - uint48 shareRatioNumerator; - uint48 shareRatioDenominator; - string[2][] securityIdentifiers; - uint256 supplyCap; - string contractURI; - bytes32 salt; - } - - /*////////////////////////////////////////////////////////////// - ERRORS - //////////////////////////////////////////////////////////////*/ - - /// @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. - error InvalidPolicyId(uint64 policyId); - - /// @notice The provided share-ratio numerator or denominator is zero. - error InvalidShareRatio(); - - /// @notice The provided decimals value is outside the allowed range - /// (implementation-defined; typically 0..18 inclusive). - error InvalidDecimals(uint8 decimals); - - /// @notice A required address argument was the zero address. - error ZeroAddress(); - - /// @notice The provided supply cap is below the configured initial - /// 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"). - error EmptyIdentifierType(); - - /*////////////////////////////////////////////////////////////// - EVENTS - //////////////////////////////////////////////////////////////*/ - - /// @notice Emitted when a Default-variant token is created. - event DefaultTokenCreated( - address indexed token, - address indexed creator, - address indexed admin, - string name, - string symbol, - uint8 decimals, - uint256 capabilities, - uint256 initialSupply, - bytes32 salt - ); - - /// @notice Emitted when a Stablecoin-variant token is created. - event StablecoinCreated( - address indexed token, - address indexed creator, - address indexed admin, - string name, - string symbol, - uint8 decimals, - string currency, - uint256 capabilities, - uint256 initialSupply, - bytes32 salt - ); - - /// @notice Emitted when a Security-variant token is created. - event SecurityTokenCreated( - address indexed token, - address indexed creator, - address indexed admin, - string name, - string symbol, - uint8 decimals, - uint256 capabilities, - uint48 shareRatioNumerator, - uint48 shareRatioDenominator, - bytes32 salt - ); - - /*////////////////////////////////////////////////////////////// - CREATION METHODS - //////////////////////////////////////////////////////////////*/ - - /// @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). - /// @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. - 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. - 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)`. - function predictDefaultAddress(address creator, bytes32 salt) external view returns (address); - - /// @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. - function predictSecurityAddress(address creator, bytes32 salt) external view returns (address); - - /*////////////////////////////////////////////////////////////// - VARIANT INTROSPECTION - //////////////////////////////////////////////////////////////*/ - - /// @notice Returns the variant of `token`. Returns `NONE` if `token` - /// is not a B-20 token created by this factory. Recovered - /// from the address prefix; no storage read. - function variantOf(address token) external view returns (TokenVariant); - - /// @notice Convenience: `variantOf(token) != NONE`. - function isB20(address token) external view returns (bool); -}