Skip to content

feat(factory): add variantParams bytes field to B20Created event for stablecoin currency (BOP-158) - #102

Merged
amiecorso merged 5 commits into
mainfrom
amiecorso/bop-158-b20created-variantparams-bytes-field
May 29, 2026
Merged

amiecorso merged 5 commits into
mainfrom
amiecorso/bop-158-b20created-variantparams-bytes-field

Conversation

@amiecorso

Copy link
Copy Markdown
Collaborator

Context

Tracking ticket: BOP-158

Adds a bytes variantParams field to B20Created so stream-based indexers (e.g. Coindexer) can recover the immutable currency of a stablecoin without making mid-handler RPC calls. The payload format mirrors the existing createParams pattern: ABI-encoded variant-specific struct prefixed by a version byte.

Supersedes the closed #85 (Option B: separate StablecoinCreated/SecurityCreated companion events). Rust precompile counterpart: BOP-216.

Design

event B20Created(
    address indexed token,
    B20Variant indexed variant,
    string name,
    string symbol,
    uint8 decimals,
    bytes variantParams  // NEW
);

struct B20StablecoinEventParams {
    uint8 version;
    string currency;
}

Indexer contract: variant comes from the indexed field; if variantParams.length == 0, no extra immutable fields; if non-empty, first byte is the version, decoder selected by (variant, version). Same shape as B20CreateParams / B20StablecoinCreateParams / B20SecurityCreateParams.

Per-variant payload

Variant variantParams payload Rationale
DEFAULT empty ("") No extra immutable identity beyond name/symbol/decimals
STABLECOIN abi.encode(B20StablecoinEventParams { version: 1, currency }) currency is immutable, no setter, no other event emits it
SECURITY empty ("") isin is mutable via updateSecurityIdentifier (emits SecurityIdentifierUpdated); minimumRedeemable is mutable via updateMinimumRedeemable (emits MinimumRedeemableUpdated)

Forward-compatible: any variant can promote from empty to non-empty in a future version (just bump the variant's *_EVENT_PARAMS_VERSION and decode by (variant, version)).

Why Option A over Option B (PR #85)

Concern Option A (this PR) Option B (#85)
Indexer-friendly ✅ one event per creation ✅ but two events per creation
Variant-specific payload ✅ typed via bytes + version ✅ separate event per variant
Forward-compat for new immutable fields ✅ bump version, add struct field ❌ new event per change
Mirrors existing createParams pattern ✅ same shape ❌ asymmetric
Selector count growth as variants are added ✅ stays 1 ❌ grows linearly with variants

Changes

File What
src/interfaces/IB20Factory.sol Add bytes variantParams to B20Created; add struct B20StablecoinEventParams
src/lib/B20FactoryLib.sol Add B20_STABLECOIN_EVENT_PARAMS_VERSION = 1 constant
test/lib/mocks/MockB20Factory.sol Build variantParams per variant before emit B20Created
test/unit/B20Factory/createToken.t.sol Update 2 existing event-match tests for the 6-arg signature; add test_createB20_success_emitsB20Created_stablecoin (expectEmit-level pin) and test_createB20_success_b20CreatedVariantParams_stablecoin_decodes (recorded-logs decode round-trip)

Verification

  • forge fmt --check clean on touched files
  • forge build clean (only pre-existing warnings on unrelated files)
  • Mock-only unit suite: 583 / 0 / 0 (stable across 3 consecutive runs; was 581 baseline + 2 new tests = 583)
  • B20Factory suite: 55 / 0 / 0 (was 53 + 2 new = 55)
  • All 4 B20Created emission tests pass:
    [PASS] test_createB20_success_emitsB20Created(address,bytes32) (runs: 256)
    [PASS] test_createB20_success_emitsB20Created_security(address,bytes32) (runs: 256)
    [PASS] test_createB20_success_emitsB20Created_stablecoin(address,bytes32) (runs: 256)
    [PASS] test_createB20_success_b20CreatedVariantParams_stablecoin_decodes(address,bytes32,uint256) (runs: 256)
    

Diff stats

4 files changed, 170 insertions, 6 deletions.

Notes for review

  • Adds a bytes memory variantParams local at the bottom of createB20. Stayed under Solc's stack-too-deep limit; no inlining gymnastics needed (the local is allocated after _writeBaseStorage so several other locals are no longer live by the time it's introduced).
  • The decode-level test (test_createB20_success_b20CreatedVariantParams_stablecoin_decodes) uses a deterministic seed → 3-letter uppercase generator (_make3LetterUppercase) rather than vm.assume(_isValidFiatCode(...)) over a raw string. Avoids foundry's fuzz rejection-rate ceiling that the assume-filter approach hit on the first run.
  • _computeAddress and _writeBaseStorage signatures are unchanged.

Related

  • Tracking: BOP-158
  • Rust counterpart: BOP-216
  • Parent decision: CHAIN-4510
  • Superseded approach (Option B, companion events): #85 (closed)

…stablecoin currency (BOP-158)

Add a `bytes variantParams` field to the `B20Created` event carrying
variant-specific immutable identity data that isn't already covered by the
fixed event fields. The bytes payload leads with a version byte and is
ABI-encoded per variant — same shape as the existing `createParams` pattern
— so stream-based indexers (e.g. Coindexer) that can't make mid-handler
RPC calls can decode by `(variant, leading version byte)` and recover
immutable fields without reading storage.

Payload per variant:
- DEFAULT: empty (no extra immutable fields beyond what's already in the event)
- STABLECOIN: abi.encode(B20StablecoinEventParams { version, currency })
- SECURITY: empty (its `isin` and `minimumRedeemable` are mutable and
  surfaced via their own SecurityIdentifierUpdated /
  MinimumRedeemableUpdated events)

Forward-compatible: any variant can promote from empty to non-empty in a
future version without re-issuing a new event type.

Adds:
- `struct B20StablecoinEventParams { uint8 version; string currency; }` in
  IB20Factory.sol
- `uint8 B20_STABLECOIN_EVENT_PARAMS_VERSION = 1` constant in B20FactoryLib.sol
- New emission logic in MockB20Factory.createB20 that builds variantParams
  per variant
- Updates to the two existing B20Created event-match tests (DEFAULT,
  SECURITY) for the new 6-arg signature
- Two new STABLECOIN-focused tests: expectEmit-level pin of the encoded
  payload, and a recorded-logs decode test that round-trips the currency
  through B20StablecoinEventParams and verifies the version constant

Supersedes Option B (separate StablecoinCreated/SecurityCreated companion
events) — see closed PR #85. The Rust precompile counterpart is tracked
at BOP-216.
@linear

linear Bot commented May 29, 2026

Copy link
Copy Markdown

BOP-158

…yLib

Pull the abi.encode of B20StablecoinEventParams out of MockB20Factory and
into a B20FactoryLib.encodeStablecoinEventParams helper, matching the
existing encodeXCreateParams pattern (encodeDefaultCreateParams /
encodeStablecoinCreateParams / encodeSecurityCreateParams). The struct
itself stays in IB20Factory.sol alongside the create-params structs.

Adds the matching encoder unit test file
(test/unit/B20FactoryLib/encodeStablecoinEventParams.t.sol) with the
two-test shape used by every existing encoder:
- round-trips-through-decode (pins version constant + currency)
- matches-hand-encoded-struct (pins byte-for-byte encoding shape)
@amiecorso
amiecorso requested a review from stevieraykatz May 29, 2026 19:13
Comment thread src/interfaces/IB20Factory.sol Outdated
/// `B20StablecoinCreateParams.currency`. Surfaced here
/// because `currency` has no setter and no other
/// event ever emits it; indexers that cannot make
/// mid-handler view calls (e.g. Coindexer) would

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
/// mid-handler view calls (e.g. Coindexer) would
/// mid-handler view calls would

Comment thread src/interfaces/IB20Factory.sol Outdated
/// @param name ERC-20 token name.
/// @param symbol ERC-20 token symbol.
/// @param decimals ERC-20 decimals (fixed per variant).
/// @param variantParams ABI-encoded variant-specific immutable identity

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Naming convention should match between variantParams and the underlying encoded data, i.e. `B20StablecoinEventParams.

…riantParams → eventParams)

Two cleanups from Stevie's review on PR #102:

1. Strip three Coindexer references from OSS-facing NatSpec/comments
   (src/interfaces/IB20Factory.sol, test/lib/mocks/MockB20Factory.sol,
   test/unit/B20Factory/createToken.t.sol). Per the leak-prevention rule
   in AGENTS.md, internal Coinbase tool names should not appear in
   anything under projects/<oss>/. The remaining text — "stream-based
   indexers" / "indexers that cannot make mid-handler view calls" —
   conveys the same meaning without naming a specific tool.

2. Rename the B20Created event field variantParams → eventParams to
   align with the B20*EventParams struct family stem
   (B20StablecoinEventParams, encodeStablecoinEventParams,
   B20_STABLECOIN_EVENT_PARAMS_VERSION). The struct family already uses
   the *EventParams stem to signal event-time payload (vs. *CreateParams
   for create-time payload); the field now shares that stem.

Wire-level B20Created selector changes because the field rename changes
the canonical ABI signature (bytes variantParams → bytes eventParams).
Will need to ship in lockstep with the Rust counterpart (BOP-216).
Comment thread src/interfaces/IB20Factory.sol Outdated
/// with `currency`. Indexers decode by
/// `(variant, leading version byte)`.
event B20Created(
address indexed token, B20Variant indexed variant, string name, string symbol, uint8 decimals, bytes eventParams

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.

nit:

Suggested change
address indexed token, B20Variant indexed variant, string name, string symbol, uint8 decimals, bytes eventParams
address indexed token, B20Variant indexed variant, string name, string symbol, uint8 decimals, bytes variantParams

@github-actions

github-actions Bot commented May 29, 2026 •

Copy link
Copy Markdown

⚠️ Fork tests: 14 failed, 586 passed

These failures indicate divergences where base/base needs to catch up to the base-std spec. This check is advisory and does not block merging.

Failing tests
  • test_createB20_revert_missingIsin(address,bytes32): Error != expected error: custom error 0xff5f85a2 != MissingRequiredField("isin"); counterexample: calldata=0x108bedc0000000000000000000000000a10c3ee921e5af74079669ad9b72059a814175f0961879715a9e2570c5c9d237dbabb2e073994875fc37be3a2d3c26d6da8fe7a3 args=[0xA10C3eE921E5Af74079669AD9b72059A814175F0, 0x961879715a9e2570c5c9d237dbabb2e073994875fc37be3a2d3c26d6da8fe7a3]
  • test_createB20_success_b20CreatedVariantParams_stablecoin_decodes(address,bytes32,uint256): STABLECOIN variantEventParams must be non-empty: 0 <= 0; counterexample: calldata=0xd98dd665000000000000000000000000000000000000000000000000000000000000262f63616c6c6572206d757374206e6f7420686f6c642061646d696e00000000000000000000000000000000000000000000000000000000000000000000000030a2 args=[0x000000000000000000000000000000000000262f, 0x63616c6c6572206d757374206e6f7420686f6c642061646d696e000000000000, 12450 [1.245e4]]
  • test_createB20_success_emitsB20Created(address,bytes32): log != expected log; counterexample: calldata=0xc85a70450000000000000000000000008d8acc6353c6d93958fe959a2020638d3ef969539ae90e046efe464e53f13a06c4fb03014245c7b3764a419ca3d5974b55c98a3a args=[0x8d8AcC6353c6D93958fE959a2020638d3EF96953, 0x9ae90e046efe464e53f13a06c4fb03014245c7b3764a419ca3d5974b55c98a3a]
  • test_createB20_success_emitsB20CreatedBeforeInitCallEvents(address,bytes32): B20Created must be present in the log: -1 <= -1; counterexample: calldata=0xe87c62f00000000000000000000000005796cae5aab6281a2e02bea646338afd3d3254c0372219a35a0e6e5c84c7606bec24bca4977cc86d4e0c68ed4525595c08f5a0c9 args=[0x5796CaE5Aab6281A2e02BEa646338aFD3d3254c0, 0x372219a35a0e6e5c84c7606bec24bca4977cc86d4e0c68ed4525595c08f5a0c9]
  • test_createB20_success_emitsB20Created_security(address,bytes32): log != expected log; counterexample: calldata=0xf32acc57000000000000000000000000754098ddbeaf37c59dd4f9796d4e8ba9f5408c32e884fcb614c5106514fae1970450a62cba4b8f2cfb1ac1f611c4feabccc07cfe args=[0x754098DdBEaF37C59dd4F9796d4E8Ba9F5408c32, 0xe884fcb614c5106514fae1970450a62cba4b8f2cfb1ac1f611c4feabccc07cfe]
  • test_createB20_success_emitsB20Created_stablecoin(address,bytes32): log != expected log; counterexample: calldata=0xa0352f05000000000000000000000000cf580174349556439abe4adbff1afa9622c71dd534772d20e75eb56f0cde72d58aae43a755ad78ed29d98ffcea5c659366de45c3 args=[0xcf580174349556439AbE4aDbff1aFa9622c71DD5, 0x34772d20e75eb56f0cde72d58aae43a755ad78ed29d98ffcea5c659366de45c3]
    [FAIL: vm.store: cannot use precompile 0x8453000000000000000000000000000000000002 as an argument] test_pendingPolicyAdmin_success_zeroForBuiltinsEvenWithStoragePoison() (gas: 5654)

amiecorso added 2 commits May 29, 2026 14:29
…y review)

Roll back the variantParams → eventParams rename from 64ec9b9 per Conner's
review comment on PR #102. Stevie's earlier ask was to align the event
field with the underlying encoded data; that's satisfied either direction
(eventParams matching the *EventParams struct family, or variantParams
emphasizing the per-variant payload nature). Conner reviewed three minutes
after the rename push and explicitly suggested variantParams, so the field
returns to its original name.

The B20StablecoinEventParams struct, the encodeStablecoinEventParams
helper, and the B20_STABLECOIN_EVENT_PARAMS_VERSION constant all keep
their *EventParams names (they are about event-time payload type,
distinct from the variantParams field name which is about per-variant
event-payload bytes carriage).

Coindexer strip from 64ec9b9 stays.
Compromise between the two reviewer threads on PR #102:

- Conner asked for variantParams (emphasizes per-variant nature)
- Stevie asked for the field to share a stem with the encoded data,
  which lives in the B20*EventParams struct family

variantEventParams captures both: "variant" prefix preserves Conner's
per-variant semantics; "EventParams" suffix matches the struct /
encoder / version-constant family already in place
(B20StablecoinEventParams, encodeStablecoinEventParams,
B20_STABLECOIN_EVENT_PARAMS_VERSION).

Side effect: the longer identifier pushes the event signature past
forge fmt's 120-char limit, so it reflows to one-param-per-line.
@amiecorso
amiecorso merged commit 3850b51 into main May 29, 2026
5 checks passed
@amiecorso
amiecorso deleted the amiecorso/bop-158-b20created-variantparams-bytes-field branch May 29, 2026 22:06
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.

3 participants