diff --git a/FACTORY_DESIGN_NOTES.md b/FACTORY_DESIGN_NOTES.md new file mode 100644 index 0000000..9178446 --- /dev/null +++ b/FACTORY_DESIGN_NOTES.md @@ -0,0 +1,177 @@ +# ITokenFactory Design Notes + +Decisions and open questions for the B-20 token factory precompile. Pair +this with `src/interfaces/ITokenFactory.sol` for the full picture. + +## Why no address prefix for B-20 tokens + +Earlier drafts proposed reserving short address prefixes (e.g. `0xB200` +for Default / Stablecoin and `0xB201` for Security) so that +`variantOf(token)` could decode the variant from the address shape with +no storage read. **We rejected this in favor of a chain-level +registry** because of backward compatibility on Base. + +### The math + +- Base has roughly tens of millions of deployed contracts. +- A 16-bit prefix (e.g. `0xB200`) reserves `2^144` of the `2^160` + address space, but that prefix is shared by `2^144` addresses, of + which a uniform distribution of existing contracts puts roughly + `10M / 65k ≈ 150-500` already-deployed Base contracts at addresses + beginning with `0xB200`. Same again for `0xB201`. +- These are real, live contracts (DEX pools, NFT contracts, user + wallets, anything). +- Reserving the prefix would either break those contracts (the chain + starts dispatching their addresses to the B-20 precompile) or + require a one-off scan + grandfathering at the hardfork. +- Longer prefixes (e.g. 64-bit) make collisions astronomically + unlikely but still require chain-level coordination on which prefix + bytes to reserve AND restrictions on user CREATE / CREATE2 + deployments into the reserved range. +- The simpler answer is to drop the prefix entirely. + +### What we use instead + +- B-20 tokens have arbitrary-looking addresses (CREATE2-style + deterministic derivation from `(variant, creator, salt)` with no + fixed prefix). +- The chain maintains an internal registry of "addresses that are B-20 + tokens", populated by this factory at creation time. +- Dispatch logic on every call: if the target address is in the + registry → route to the B-20 precompile; else → normal EVM dispatch. +- `variantOf(token)` reads from that registry (one storage slot). + +### What we give up + +- `variantOf(token)` becomes a storage read instead of a pure address + decode. Acceptable: it isn't called on every transfer, only on + introspection. +- B-20 tokens have no visual marker in their address. Wallets and + indexers that want to identify B-20 tokens consult the registry + (or call `isB20(token)` on the factory). + +### Sequencer optimization story is preserved + +The sequencer can prefetch / cache the registry aggressively. The +"is this address a B-20 token?" check is still O(1) and entirely +predictable. We just use a registry lookup instead of an address-shape +decode. + +## Resolved decisions + +### Address derivation algorithm + +Implementation detail; not specified at the interface level. Standard +CREATE2-style hash of `(variant derivation domain, creator, salt)` +producing a 20-byte address. **There is no fancy prefix scheme** (per +the address-prefix discussion above; we can't reserve prefixes on +Base). The Rust precompile implementation picks the exact derivation +function; what's locked at the interface level is determinism (`predict*Address` +returns the address `create*` would assign) and the variant-domain +separation (Default / Stablecoin share a domain; Security uses a +different domain). + +### Capability bit validation at creation + +Factory validates the capability bitfield at creation and reverts with +`InvalidCapabilities` if any bit is set that isn't valid for the chosen +variant. For Default and Stablecoin tokens, only `PAUSABLE` and +`CAP_MUTABLE` are currently valid. For Security tokens, the additional +security-specific bits in `Capabilities` (16..23 range) are also valid. + +### `transferPolicyId` default at creation + +Required at creation but no implicit default. Recommended values: + +- **Pass `1` (always-allow)** for tokens that don't need compliance + gating. This is the typical default for memecoins and similar. +- **Pass `0` (always-reject)** to start the token in a soft-paused + state where transfers fail until the admin sets a real policy via + `changeTransferPolicyId`. Useful when the policy isn't yet + configured at creation. +- **Pass any existing custom policy ID** to apply that policy from + creation. + +The factory does not auto-default to `1` because we want the choice +to be explicit; passing `0` accidentally is a foot-gun (the token +would be silently unusable), so callers should know what they're +doing. + +## Other decisions baked into the current draft + +- **Three creation methods** (one per variant), not the unified + `createToken(...variant...)` from the PRD. Diverges from PRD wording + in favor of typed per-variant params; happy to revert if the team + prefers the unified shape. (See open question #4 below.) +- **`TokenVariant` enum has no `STABLECOIN` value.** Stablecoin and + Default share the same address derivation and the same `DEFAULT` + variant marker. Sub-case is detected via `isStablecoin(token)` (which + checks `currency() != ""`). (See open question #3 below.) +- **Default and Stablecoin share address space.** Same `(creator, salt)` + pair maps to one token of either variant; calling `createDefault` + precludes `createStablecoin` at the same slot and vice versa. +- **Security uses a different derivation domain.** Security tokens at + the same `(creator, salt)` get a different address from Default / + Stablecoin, so no collision risk across variants. +- **Per-token custom decimals** (and immutable after creation, per + IDefaultToken). +- **`admin == address(0)` explicitly allowed** for the "demonstrate no + owner" case from the PRD. Tokens with no admin: no role grants, no + policy changes, no pauses. The last-admin-renounce guard in + `IDefaultToken.renounceRole` does not apply because there was never + an admin to renounce. +- **Initial supply bootstrap mint** for Default and Stablecoin (atomic + with creation). Security tokens have no `initialSupply` (use + `create` / `adminMint` after creation). +- **Permissionless creation.** Anyone deploys; no factory admin. +- **No wrapper coupling.** Factory creates the token only. Wrappers + (Bridge's TIP20Controller, CCS's beacon proxy, etc.) deploy + separately as normal EVM contracts. + +## Open questions to take to the team + +### 1. Bootstrap mint policy bypass vs apply + +Current draft bypasses the transfer policy check on the initial mint at +creation (the policy referenced by `transferPolicyId` may legitimately +not authorize the recipient yet at creation). Argument for applying: +**fail-fast sanity check on setup**. If the issuer mints to an address +that the policy then can't authorize for transfers, that supply is +stuck and the token has to be redeployed. Catching this at creation is +nice. Argument against: **mint and transfer policies can differ**. +Compound policies have separate `senderPolicyId` and +`mintRecipientPolicyId` slots, so an address might legitimately be +authorized to receive mints but not to send transfers (or vice versa). +The bootstrap mint check should probably be against the +`mintRecipientPolicyId` slot specifically, not the full transfer check. + +Lean: apply the `mintRecipientPolicyId` check at bootstrap. Keep open +pending team discussion on whether the friction is worth it. + +### 2. Two-step "renounce last admin" pattern + +Not in v1. The last-admin guard in `IDefaultToken.renounceRole` +prevents the LAST admin from renouncing, which means tokens cannot +evolve from admin-controlled to admin-less mid-life. Real use case: +**a token might need active admin oversight for an initial setup +period (mintable while bootstrapping, configuring policies, etc.) and +then need to renounce admin control once the setup is done**. + +Lean: we probably need this. Mechanism would be a delay-protected +two-step renounce on `IDefaultToken` (something like +`beginRenounceLastAdmin` → wait → `acceptRenounceLastAdmin`). Worth a +separate design discussion before adding. + +### 3. Drop `STABLECOIN` from the enum entirely vs keep it + +Current draft drops it because Default and Stablecoin share both the +address derivation and the variant marker. Stablecoin is detected +via `isStablecoin(token)` (one external call into the token's +`currency()` accessor). Worth team confirmation that this collapse is +desirable. + +### 4. Three separate functions vs one unified `createToken(variant, ...)` + +PRD wording implies unified. Current draft uses separate per-variant +functions for clearer typing and per-variant params structs. Worth +team confirmation. diff --git a/src/interfaces/ITokenFactory.sol b/src/interfaces/ITokenFactory.sol index fa31f95..5ee32b9 100644 --- a/src/interfaces/ITokenFactory.sol +++ b/src/interfaces/ITokenFactory.sol @@ -2,71 +2,134 @@ 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. +/// @notice Singleton factory precompile for creating B-20 tokens. Exposes +/// three creation methods, one per variant the caller wants: +/// `createDefault`, `createStablecoin`, `createSecurity`. Each +/// returns the address of the newly created token, deployed at +/// a deterministic location derived from the caller's salt. /// -/// @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. +/// @dev **Variant identification.** B-20 tokens have arbitrary-looking +/// addresses (no reserved prefix). The chain maintains an +/// internal registry of "addresses that are B-20 tokens", +/// populated by this factory at creation time. Dispatch logic +/// on every call: if the target address is in the registry, +/// route to the B-20 precompile; otherwise dispatch to normal +/// EVM. `variantOf(token)` reads from that registry. The +/// rationale for this design (rather than reserving an address +/// prefix like 0xB200 / 0xB201) is in `FACTORY_DESIGN_NOTES.md`: +/// short version is that Base has millions of pre-existing +/// contracts spread across the entire 160-bit address space, +/// and reserving any short prefix would either break existing +/// contracts or require extensive scanning + grandfathering. /// -/// 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. +/// **Variants and address space.** Default and Stablecoin share +/// the same address derivation (a Stablecoin is structurally a +/// Default token with `currency` set). For a given +/// `(creator, salt)` pair, exactly ONE of `createDefault` or +/// `createStablecoin` can succeed; the other reverts with +/// `TokenAlreadyExists`. Security tokens use a different +/// derivation domain so their addresses never collide with +/// Default / Stablecoin tokens for the same `(creator, salt)`. +/// +/// **The `TokenVariant` enum** has only `NONE`, `DEFAULT`, +/// `SECURITY`. There is no `STABLECOIN` value because Stablecoin +/// is detected by the immutable `currency()` field returning a +/// non-empty string, not by a separate variant marker. Use +/// `isStablecoin(token)` to detect the stablecoin sub-case. +/// +/// **Address determinism.** Each `predict*Address` view returns +/// the deterministic address that the matching `create*` call +/// would assign for a given `(creator, salt)`. Addresses depend +/// on the variant derivation domain, the creator, and the salt +/// only, so callers can compute the address off-chain or +/// pre-fund it before deployment. `predictDefaultAddress` and +/// `predictStablecoinAddress` return the SAME address for the +/// same `(creator, salt)` pair (shared derivation). +/// +/// **Permissionless.** Anyone may create a token of any variant. +/// The factory has no admin and no per-call gating beyond the +/// standard creator-pays-gas flow. +/// +/// **Each token is independent.** The factory creates the token +/// and bootstrap-mints initial supply (for Default and +/// Stablecoin) atomically, then the token is fully self- +/// governing. Issuers who need wrappers / controllers (Bridge's +/// TIP20Controller pattern, CCS's beacon proxy) deploy those +/// separately as normal EVM contracts. 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. + /// @notice Variant of a B-20 token. `NONE` indicates the address + /// is not a B-20 token registered with this factory. + /// @dev Stablecoin is NOT a separate variant value: Stablecoin + /// and Default tokens share the `DEFAULT` enum value AND + /// the same address derivation, distinguished only by + /// whether `currency()` returns a non-empty string. Use + /// the `isStablecoin(token)` convenience view to identify + /// the stablecoin sub-case. enum TokenVariant { NONE, DEFAULT, - STABLECOIN, SECURITY } /// @notice Creation parameters for a Default-variant token. - /// @param name ERC-20 token name. Mutable post-creation - /// via `setName` (admin-only). - /// @param symbol ERC-20 token symbol. Mutable - /// post-creation via `setSymbol` - /// (admin-only). - /// @param decimals ERC-20 token decimals (issuer choice). - /// Immutable after creation. + /// @param name ERC-20 token name. Mutable post- + /// creation via `setName`. + /// @param symbol ERC-20 token symbol. Mutable post- + /// creation via `setSymbol`. + /// @param decimals Number of decimal places. Immutable. + /// Per-token custom; the factory does + /// not enforce a fixed value. /// @param admin Initial holder of `DEFAULT_ADMIN_ROLE`. + /// Pass `address(0)` to create an + /// ADMIN-LESS token (credibly neutral, + /// no future role grants possible, no + /// policy or pause changes possible). + /// This is the "demonstrate no owner" + /// case from the PRD; the + /// last-admin-renounce guard does not + /// apply because there was never an + /// admin to renounce. /// @param capabilities Immutable capability bitfield. See - /// `Capabilities` for the bit definitions. - /// @param initialSupply Amount minted atomically at creation. - /// Bypasses the transfer-policy check - /// (this is the bootstrap mint, not a - /// normal mint operation; the policy - /// may not be configured at creation - /// time). - /// @param initialSupplyRecipient Address that receives `initialSupply`. - /// Ignored when `initialSupply == 0`. + /// `Capabilities` for the bit + /// definitions. The factory validates + /// that only bits valid for this + /// variant are set; reverts with + /// `InvalidCapabilities` otherwise. + /// For Default tokens, only `PAUSABLE` + /// and `CAP_MUTABLE` are valid. /// @param transferPolicyId Initial value of `transferPolicyId`. - /// Must reference an existing policy in - /// the policy registry. + /// Required field; caller MUST pass a + /// value. The recommended default for + /// tokens without compliance needs is + /// policy ID `1` (always-allow). Pass + /// policy ID `0` (always-reject) to + /// start in a soft-paused state until + /// compliance is configured. /// @param supplyCap Initial value of `supplyCap`. Use - /// `type(uint256).max` for no cap. To - /// make the token permanently fixed-supply, - /// set this equal to `initialSupply` and - /// leave the `CAP_MUTABLE` capability - /// unset. + /// `type(uint256).max` for no cap. + /// To make the token permanently + /// fixed-supply, set this equal to + /// `initialSupply` and leave + /// `CAP_MUTABLE` unset. + /// @param initialSupply Amount minted atomically at + /// creation. Set to `0` for no + /// bootstrap mint. + /// OPEN DESIGN QUESTION: bootstrap + /// mints currently bypass the policy + /// check. See FACTORY_DESIGN_NOTES.md. + /// @param initialSupplyRecipient Address that receives `initialSupply`. + /// Ignored when `initialSupply == 0`. /// @param minimumRedeemable Initial value of `minimumRedeemable`. - /// Use `0` to allow any non-zero amount - /// (the typical setting for tokens - /// without a redemption product). Mutable - /// post-creation via `setMinimumRedeemable`. + /// Use `0` to allow any non-zero + /// redeem amount (typical for tokens + /// without a redemption product). + /// Mutable post-creation. /// @param contractURI Initial ERC-7572 contract URI. + /// Mutable post-creation by admin. /// @param salt Caller-chosen salt for deterministic /// address derivation. struct CreateDefaultTokenParams { @@ -75,32 +138,40 @@ interface ITokenFactory { uint8 decimals; address admin; uint256 capabilities; - uint256 initialSupply; - address initialSupplyRecipient; uint64 transferPolicyId; uint256 supplyCap; + uint256 initialSupply; + address initialSupplyRecipient; uint256 minimumRedeemable; string contractURI; bytes32 salt; } /// @notice Creation parameters for a Stablecoin-variant token. - /// @param currency Immutable currency identifier (e.g. - /// "USD", "EUR", "XAU"). See + /// @param currency Immutable currency identifier + /// (e.g. "USD", "EUR", "XAU"). See /// `IStablecoin.currency` for the - /// convention. + /// convention. MUST be non-empty; + /// passing an empty string defeats + /// the point of using + /// `createStablecoin` over + /// `createDefault`. /// @dev All other fields have the same semantics as the Default - /// params struct. + /// params struct. Stablecoin tokens share the same address + /// derivation as Default tokens; for a given + /// `(creator, salt)` pair, exactly ONE of `createDefault` + /// or `createStablecoin` can succeed (the other reverts + /// with `TokenAlreadyExists`). struct CreateStablecoinParams { string name; string symbol; uint8 decimals; address admin; uint256 capabilities; - uint256 initialSupply; - address initialSupplyRecipient; uint64 transferPolicyId; uint256 supplyCap; + uint256 initialSupply; + address initialSupplyRecipient; uint256 minimumRedeemable; string contractURI; string currency; @@ -108,10 +179,10 @@ interface ITokenFactory { } /// @notice Creation parameters for a Security-variant token. - /// @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 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. @@ -125,12 +196,21 @@ interface ITokenFactory { /// coupling) after creation. The supply cap is set at /// creation; `transferPolicyId` must reference an existing /// compound policy in the registry whose redeemer slot - /// encodes the brokerage allowlist (typically a - /// Coinbase-managed whitelist of KYC'd, brokerage-connected + /// encodes the brokerage allowlist (typically a Coinbase- + /// managed whitelist of KYC'd, brokerage-connected /// accounts). /// + /// Security tokens use a different address derivation + /// domain from Default and Stablecoin tokens; addresses + /// created via `createSecurity` will never collide with + /// addresses created via `createDefault` or + /// `createStablecoin`, even for the same `(creator, salt)` + /// pair. + /// /// All other fields have the same semantics as the Default - /// params struct. + /// params struct. Stablecoin-and-Default fields like + /// `initialSupplyRecipient` are absent here because Security + /// has no bootstrap mint. struct CreateSecurityTokenParams { string name; string symbol; @@ -152,38 +232,57 @@ interface ITokenFactory { //////////////////////////////////////////////////////////////*/ /// @notice A token already exists at the deterministic address - /// derived from `(variant, msg.sender, salt)`. Caller must - /// use a different salt. + /// derived from the caller's salt. 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. + /// @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). + /// @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. + /// (NOTE: `admin == address(0)` is explicitly ALLOWED for + /// creating admin-less tokens; this error fires for other + /// positional zero-address checks like + /// `initialSupplyRecipient` when `initialSupply > 0`.) error ZeroAddress(); /// @notice The provided supply cap is below the configured initial /// supply, or is otherwise invalid. error InvalidSupplyCap(); + /// @notice The provided capability bitfield contains bits that are + /// not valid for this variant. The factory validates that: + /// (a) only currently-defined bits are set (no reserved / + /// future bits), and (b) only variant-appropriate bits are + /// set (e.g. `SECURITY_*` bits only on Security tokens). + error InvalidCapabilities(uint256 capabilities); + /// @notice A security identifier `type` was the empty string. /// Identifier types must be non-empty (typical values: /// "isin", "cusip", "figi", "sedol"). error EmptyIdentifierType(); + /// @notice `createStablecoin` was called with an empty `currency` + /// string. Use `createDefault` for tokens without a + /// currency identifier. + error EmptyCurrency(); + /*////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////*/ - /// @notice Emitted when a Default-variant token is created. + /// @notice Emitted when a Default-variant token is created (via + /// `createDefault`; tokens created via `createStablecoin` + /// emit `StablecoinCreated` instead). event DefaultTokenCreated( address indexed token, address indexed creator, @@ -196,7 +295,8 @@ interface ITokenFactory { bytes32 salt ); - /// @notice Emitted when a Stablecoin-variant token is created. + /// @notice Emitted when a Stablecoin-variant token is created (via + /// `createStablecoin`). event StablecoinCreated( address indexed token, address indexed creator, @@ -228,30 +328,53 @@ interface ITokenFactory { 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. The bootstrap mint bypasses the policy check - /// (the policy may not yet authorize the recipient at - /// creation time); subsequent mints go through the normal - /// policy hook. + /// @notice Creates a Default-variant token at the deterministic + /// address derived from `(DEFAULT derivation, msg.sender, params.salt)`. + /// Registers the new address with the chain's B-20 token + /// registry. If `params.initialSupply > 0`, mints that + /// amount to `params.initialSupplyRecipient` atomically as + /// a bootstrap operation. + /// @dev The bootstrap mint BYPASSES the transfer policy check + /// (the policy referenced by `params.transferPolicyId` may + /// legitimately not authorize the recipient yet at creation + /// time). Subsequent mints go through the normal policy + /// hook. **Open design question** (see + /// FACTORY_DESIGN_NOTES.md): should the bootstrap mint + /// instead require the policy to authorize the recipient? + /// + /// If `params.admin == address(0)`, the token is created + /// with NO admin (the "demonstrate no owner" case from the + /// PRD): no role grants, policy changes, or pauses are + /// possible after creation. The last-admin-renounce guard + /// in `IDefaultToken.renounceRole` does not apply because + /// there was never an admin to renounce. /// @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 (same bootstrap - /// policy bypass as `createDefault`). Sets the immutable - /// `currency` field. + /// @notice Creates a Stablecoin-variant token at the deterministic + /// address derived from `(DEFAULT derivation, msg.sender, params.salt)`. + /// Identical to `createDefault` except that + /// `IStablecoin.currency()` returns `params.currency` + /// (immutable). The address space is shared with + /// `createDefault`, so each `(creator, salt)` pair maps to + /// exactly one token (Default OR Stablecoin, not both). + /// @dev Reverts with `EmptyCurrency` if `params.currency` is + /// empty (use `createDefault` for tokens without a + /// currency identifier). 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` + /// @notice Creates a Security-variant token at the deterministic + /// address derived from `(SECURITY derivation, msg.sender, params.salt)`. + /// No bootstrap mint; security tokens use `create` /// (rate-limited compliant issuance) or `adminMint` - /// (cold-path batch with announcement coupling) for issuance - /// after deployment. + /// (cold-path batch with announcement coupling) for + /// issuance after deployment. + /// @dev Security tokens use a different address derivation + /// domain from Default and Stablecoin tokens; addresses + /// created via `createSecurity` will never collide with + /// addresses created via `createDefault` or + /// `createStablecoin`, even for the same `(creator, salt)` + /// pair. function createSecurity(CreateSecurityTokenParams calldata params) external returns (address token); /*////////////////////////////////////////////////////////////// @@ -259,28 +382,52 @@ interface ITokenFactory { //////////////////////////////////////////////////////////////*/ /// @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)`. + /// would assign for the given `(creator, salt)`. Depends + /// only on the variant derivation domain, creator, and + /// salt; not on any of the other creation parameters. + /// @dev Returns the SAME address as `predictStablecoinAddress` + /// for the same `(creator, salt)` pair, because Default + /// and Stablecoin share the address derivation. Callers + /// who want both a Default and a Stablecoin token must + /// use different salts. function predictDefaultAddress(address creator, bytes32 salt) external view returns (address); - /// @notice Same as `predictDefaultAddress`, for the Stablecoin - /// variant. + /// @notice Returns the deterministic address that `createStablecoin` + /// would assign. Same as `predictDefaultAddress` for the + /// same `(creator, salt)` pair. function predictStablecoinAddress(address creator, bytes32 salt) external view returns (address); - /// @notice Same as `predictDefaultAddress`, for the Security variant. + /// @notice Returns the deterministic address that `createSecurity` + /// would assign. Distinct from `predictDefaultAddress` / + /// `predictStablecoinAddress` because Security uses a + /// different derivation domain. 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. + /// @notice Returns the variant of `token`. Returns `NONE` if + /// `token` is not in the chain's B-20 token registry + /// (i.e. was not created by this factory). + /// @dev Reads from the chain-level B-20 registry (one storage + /// slot read). Returns `DEFAULT` for any token created + /// via `createDefault` OR `createStablecoin` (they share + /// the variant marker). To distinguish the stablecoin + /// sub-case, use `isStablecoin(token)`. function variantOf(address token) external view returns (TokenVariant); /// @notice Convenience: `variantOf(token) != NONE`. function isB20(address token) external view returns (bool); + + /// @notice Convenience: returns true if `token` is a B-20 token + /// AND its `currency()` accessor returns a non-empty + /// string, indicating it was created via + /// `createStablecoin` rather than `createDefault`. + /// Returns false for non-B-20 addresses, for Security + /// tokens (no `currency` accessor), and for Default + /// tokens with empty `currency`. + /// @dev Reads from the token's storage (one external call) in + /// addition to the registry lookup. + function isStablecoin(address token) external view returns (bool); }