Skip to content

feat(interfaces): first-pass design for the three B-20 token variants#1

Merged
amiecorso merged 8 commits into
masterfrom
amie/interface-design
May 13, 2026
Merged

feat(interfaces): first-pass design for the three B-20 token variants#1
amiecorso merged 8 commits into
masterfrom
amie/interface-design

Conversation

@amiecorso
Copy link
Copy Markdown
Collaborator

Summary

First-pass interface design for the three B-20 token variants: IDefaultToken (the inheritable base), IStablecoin, ISecurityToken, plus the Capabilities bitfield library that gates each variant's optional features.

Draft. Looking for feedback on the design choices documented in DESIGN_NOTES.md before iterating further.

Design highlights

  • Capabilities bitfield as the honest-signaling mechanism for permanently-disabled features (immutable per token, set at creation)
  • Default IS Core: Stablecoin and Security extend IDefaultToken directly, no separate ICoreToken
  • Compliance delegated to the policy engine; no internal blocklist
  • Mint/burn role split, two-step admin transfer with delay (CCS-style)
  • IStablecoin: per-minter rate limiting + ERC-3009 + currency identifier (informed by reading coinbase/custom-stablecoin)
  • ISecurityToken: announce/share-ratio/create/redeem/admin-batch with on-chain announcement coupling

Open for input

DESIGN_NOTES.md has a Design Rationale section explaining the why of the major decisions, plus a 9-item confirmation list at the bottom for the still-open questions. Most consequential of those: should we add a MEMOS_REQUIRED capability bit, and should adminBurn be allowed to affect any account (not just policy-blocked) given announcement coupling.

Not implemented yet

  • ITokenFactory.sol (one factory, three create methods, takes capability bitfield + initial supply per variant)
  • IPolicyRegistry.sol (TIP-403 + TIP-1015 adapted)
  • Reference Solidity implementations of all variants
  • StdPrecompiles equivalent with per-variant address prefixes

amiecorso added 8 commits May 12, 2026 17:08
IDefaultToken is the base interface every B-20 token implements (Default,
Stablecoin, Security all inherit). ERC-20 selector compat, plus memo
siblings, mint/burn, role-based access control, pause, EIP-2612 +
ERC-1271 permit, transfer-policy hookup, supply cap, and ERC-7572
contractURI.

Capabilities is a small library of immutable feature bits that gate the
optional surface of any B-20 token. Functions whose capability bit is
unset revert with FeatureDisabled, regardless of role state. This is the
honest-signaling mechanism: integrators read capabilities() once and
know exactly what is permanently possible on the token. Includes named
presets ALL, IMMUTABLE_MEMECOIN, and FIXED_SUPPLY.
IStablecoin extends IDefaultToken with a single addition: an immutable
currency() identifier (USD, EUR, BTC, etc.) for DEX/routing/wallet
categorization. Other stablecoin-specific extensions (reserve
attestation, master-minter, freeze, yield distribution) deferred
pending team alignment and CDP Custom Stablecoin reference access.

ISecurityToken extends IDefaultToken with the Tangor-derived security
surface: announcement coupling for metadata changes, share ratio for
split-safe accounting, compliant create() with per-caller rate limit,
user-side redeem() gated on a separate redeemPolicyId allowlist, batch
adminMint/adminBurn cold paths, multi-key security identifiers, name
and symbol updates with announcement coupling.

Capabilities extended with security-specific bits in the 16+ range
(SECURITY_CREATABLE, SECURITY_REDEEMABLE, SHARE_RATIO_MUTABLE,
SECURITY_METADATA_MUTABLE, SECURITY_ADMIN_BATCH) and a STANDARD_EQUITY
preset that captures the typical security-token configuration
(inherited mint/burn off, security paths on, BURN_BLOCKED on for
sanctions enforcement).

Open questions and unilateral assumptions documented in DESIGN_NOTES.md
(separate commit).
Catalogs every decision I made unilaterally vs. every question that
needs your input, organized by file and topic. Categories:
- ASSUMED: decisions made without explicit sign-off, flag any to revisit
- OPEN: questions awaiting your input
- VERIFY: ambiguities in source docs (user stories vs. wiki vs. Tangor)

Includes a top-9 list of bits to explicitly confirm before iterating
further.
…ablecoin

After reading coinbase/custom-stablecoin (CCS) end-to-end, the
following changes:

IDefaultToken:
- Split ISSUER_ROLE into MINT_ROLE and BURN_ROLE so issuance and
  destruction authority can be held separately. Same pattern as CCS;
  enables operational separation of concerns (treasury team mints,
  redemption team burns).
- Add two-step admin transfer with configurable delay (the OZ
  AccessControlDefaultAdminRules pattern, also used by CCS): adds
  defaultAdmin, pendingDefaultAdmin, defaultAdminDelay, plus the
  begin/cancel/accept/changeDelay/rollbackDelay flow. grantRole and
  revokeRole now revert when called for DEFAULT_ADMIN_ROLE; the only
  valid transfer path is the two-step flow with delay. Protects against
  typo-bricking the admin role and gives detection time on key
  compromise.

IStablecoin:
- Add per-minter rate limiting: configureMinter, removeMinterRateLimit,
  grantMinterRoleWithLimit (atomic combo to avoid first-mint race),
  currentMintLimit, mintRateLimitConfig. Held under MINT_RATE_LIMIT_ROLE
  separately from DEFAULT_ADMIN_ROLE.
- Add ERC-3009 transfer-with-authorization: transferWithAuthorization,
  receiveWithAuthorization (front-run-resistant; only payee submits),
  cancelAuthorization. Both ECDSA (canonical) and bytes (EOA + ERC-1271)
  variants. Distinct from EIP-2612 permit (random nonces, time windows,
  direct transfers vs. allowances).
- Currency identifier kept from prior draft.

Capabilities:
- New stablecoin bits in 24..31 range: STABLECOIN_MINT_RATE_LIMITED
  and STABLECOIN_AUTHORIZATIONS.
- New STANDARD_STABLECOIN preset that includes rate limiting +
  authorizations, OMITS BURN_BLOCKED to default to the CCS 'freeze,
  never seize' philosophy. Issuers wanting force-burn for sanctions
  enforcement OR in BURN_BLOCKED at creation.
…decisions

Restructure DESIGN_NOTES.md to lead with a Design Rationale section
that captures the reasoning behind the major settled decisions, so
future contributors do not have to re-derive them:

Cross-cutting:
- Capabilities bitfield as honest-signaling mechanism
- Default IS Core (variant inheritance, no separate ICoreToken)
- Single source of truth for compliance (external Policy Registry vs
  internal blocklist), with note on Tangor sanctions vs CCS freeze
- Memos as ERC-20-compatible sibling functions (vs optional parameter)
- No third-party deps constraint and its implications

Roles & Admin:
- Why MINT_ROLE and BURN_ROLE are separate
- Why PAUSE_ROLE and UNPAUSE_ROLE are separate
- Why two-step admin transfer with delay (typo-bricking protection,
  key-compromise detection window)
- User-defined role support

Stablecoin-specific:
- Why per-minter rate limiting (risk management, multi-party governance)
- Why ERC-3009 (USDC parity for payment apps; complementary to permit)
- Currency identifier convention
- Freeze vs seize philosophy (CCS vs Tangor) expressed via BURN_BLOCKED

Security-specific:
- Three issuance paths (create / adminMint / inherited mint)
- redeemPolicyId separate from transferPolicyId
- Why on-chain announcement coupling (vs off-chain audit policy)
- Share ratio for split-safe accounting (DeFi composability)

Open questions reorganized to mark resolved items as RESOLVED with
context, surface the remaining 9 items in a confirmation summary at
the bottom.
Three policy types in v1: WHITELIST, BLACKLIST, COMPOUND. Built-in
IDs 0 (always-reject) and 1 (always-allow) reserved as well-known
protocol constants (no view-function getters; documented in the
contract notice).

Surface mirrors Tempo TIP-403 + TIP-1015 with three deliberate
omissions: no virtual-address rejection (no TIP-1022 on Base), no
receive policies (no TIP-1028 escrow), no callback / richer-guard
policies (deferred; can be added in a future hardfork as a backward-
compatible enum extension).

Authorization queries split per role (isAuthorizedSender /
isAuthorizedRecipient / isAuthorizedMintRecipient) so compound
policies can carry asymmetric rules; isAuthorized retained as the
sender-AND-recipient composite for callers that want a single check.

Compound policies are structurally immutable (constituent IDs cannot
change after creation); to rotate the configuration, create a new
compound policy and re-point the consuming token's transferPolicyId.
Constituents may be simple policies or built-ins (0, 1) but never
other compound policies (PolicyNotSimple).

DESIGN_NOTES updated with the rationale for shipping just Levels 1+2
(not Level 3 callback policies or Level 4 modular guards) in v1, and
which rule classes that decision leaves unsupported chain-side
(per-tx amount limits, counterparty-dependent rules).
Singleton factory at a fixed precompile address. Three permissionless
create methods (createDefault, createStablecoin, createSecurity), each
accepting a variant-specific params struct.

Tokens are deployed at deterministic addresses derived from
(variant, creator, salt). Variant is encoded in the address prefix so
the variant of any token is recoverable via variantOf without an
SLOAD; address prediction functions (predict*Address) let callers
compute the address from (creator, salt) alone, independent of the
other creation params.

Default and Stablecoin variants accept an initialSupply minted
atomically at creation; security tokens have no initialSupply and
bootstrap via create() / adminMint() after deployment. The bootstrap
mint deliberately bypasses both the policy check and the MINTABLE
capability gate (one-shot creator-managed allocation; rationale
captured in DESIGN_NOTES).

Each token gets a per-token defaultAdminDelay configured at creation
so different security postures (stablecoin vs memecoin) can have
appropriately different delays from day one.

Factory is permissionless and has no admin; each created token
self-governs via its own roles.
Comment on lines +30 to +31
NONE,
DEFAULT,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was assuming default and none are effectively the same, what's the case for separating you see?

/// @param contractURI Initial ERC-7572 contract URI.
/// @param salt Caller-chosen salt for deterministic
/// address derivation.
struct CreateDefaultTokenParams {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is too big, needs to be smaller

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe a pattern we can consider is some way for factory smart contracts that wrap our factory precompile can also configure some of these things through the setter directly? Would help reduce our scope of creation params

function setRoleAdmin(bytes32 role, bytes32 newAdminRole) external;

/*//////////////////////////////////////////////////////////////
TWO-STEP DEFAULT ADMIN
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feeling resistance on including this although whenever we do admin patterns, I always advocate for 2-step patterns...

@amiecorso amiecorso marked this pull request as ready for review May 13, 2026 19:30
@amiecorso amiecorso merged commit 562db90 into master May 13, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants