Skip to content

test(precompiles): add golden tests for factory V1 (BOP-424)#4015

Merged
stephancill merged 1 commit into
mainfrom
stephancilliers/bop-424-factory-goldens
Jul 20, 2026
Merged

test(precompiles): add golden tests for factory V1 (BOP-424)#4015
stephancill merged 1 commit into
mainfrom
stephancilliers/bop-424-factory-goldens

Conversation

@stephancill

Copy link
Copy Markdown
Contributor

What

Adds a golden/snapshot suite pinning Factory V1 behavior of the B-20 precompile (crates/common/precompiles/tests/b20_factory_v1_golden.rs), driven through the real B20FactoryStorage dispatch entry. Covers all 4 ops and every validation guard:

  • createB20 — asset + stablecoin success (created token state/code + B20Created event), with initCalls (asset & stablecoin), zero-admin (skips role grant), and guards: TokenAlreadyExists, UnsupportedVersion, InvalidDecimals, MissingRequiredField, InvalidCurrency, InitCallFailed, typed-revert propagation from an init call, malformed params, not-activated, and NonPayable.
  • getB20Address — deterministic address derivation (asset + stablecoin).
  • isB20 / isB20Initialized — prefix + factory-initialized reads.

Each case asserts exact returned bytes (or typed revert), the created token's resulting state, emitted events, and a per-case keccak storage-hash snapshot scoped to the factory + created token (excludes activation scaffolding); plus per-op storage-access gas footprints and a compile-time op-coverage checklist over IB20FactoryCalls.

Why

Baseline for the frozen-manifest check (BOP-422/BOP-424). The pins were authored and blessed against the shipped v1.1.1 (pre-versioned) factory implementation, then confirmed to pass unchanged on the versioned (resolver-gated) structure — proving the SOLID/versioned migration (BOP-418) is behavior-preserving.

Testing

cargo test -p base-common-precompiles --features test-utils --test b20_factory_v1_golden   # 21 passed
cargo test -p base-common-precompiles --features test-utils                                # full crate green
cargo clippy -p base-common-precompiles --features test-utils --all-targets                # clean
cargo +nightly fmt -p base-common-precompiles -- --check                                   # clean

~96% line coverage of b20_factory/logic/v1.rs (remainder are defensive ? error edges + the unused non-observer create_b20 alt-API). Test-only: new integration test + one Cargo.toml [[test]] stanza; no source/logic changes.

@linear

linear Bot commented Jul 17, 2026

Copy link
Copy Markdown

BOP-424

@cb-heimdall

cb-heimdall commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

✅ Heimdall Review Status

Requirement Status More Info
Reviews 1/1
Denominator calculation
Show calculation
1 if user is bot 0
1 if user is external 0
2 if repo is sensitive 0
From .codeflow.yml 1
Additional review requirements
Show calculation
Max 0
0
From CODEOWNERS 0
Global minimum 0
Max 1
1
1 if commit is unverified 0
Sum 1

@stephancill
stephancill force-pushed the stephancilliers/bop-424-factory-goldens branch from 7318c75 to b67edca Compare July 17, 2026 16:53
Comment on lines +642 to +656
#[test]
fn golden_create_reverts_malformed_params() {
let mut s = fresh();
let (rev, _bytes) = call_factory(
&mut s,
CREATOR,
create_call(
IB20Factory::B20Variant::ASSET,
SALT,
Bytes::from(vec![0xaa_u8, 0xbb, 0xcc]),
vec![],
),
);
assert!(rev);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: For a golden test that pins exact behavior, discarding the revert bytes (_bytes) means the specific revert selector/payload isn't pinned. If the factory changes which error it returns for malformed params, this test would still pass. Same applies to golden_create_reverts_when_not_activated below. Consider asserting the exact revert bytes (or at minimum the error selector) to match the thoroughness of the other revert tests.

@stephancill
stephancill force-pushed the stephancilliers/bop-424-factory-goldens branch from b67edca to bd96a39 Compare July 17, 2026 21:00
@stephancill
stephancill enabled auto-merge July 17, 2026 21:06
@stephancill
stephancill requested a review from rayyan224 July 17, 2026 21:06
Comment on lines +684 to +688
StorageCtx::enter(&mut s, |ctx| {
B20FactoryStorage::new(ctx).dispatch(ctx, &calldata, BaseUpgrade::Beryl)
})
.expect("gas-footprint op must succeed");
(s.counter_sload(), s.counter_sstore(), s.counter_keccak256())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The gas() helper unwraps the outer Result but doesn't assert that the dispatch actually succeeded (i.e. !output.is_revert()). If a future refactor introduces a bug that makes one of these calls revert, this test would silently measure the revert-path gas footprint rather than failing.

Suggested change
StorageCtx::enter(&mut s, |ctx| {
B20FactoryStorage::new(ctx).dispatch(ctx, &calldata, BaseUpgrade::Beryl)
})
.expect("gas-footprint op must succeed");
(s.counter_sload(), s.counter_sstore(), s.counter_keccak256())
let out = StorageCtx::enter(&mut s, |ctx| {
B20FactoryStorage::new(ctx).dispatch(ctx, &calldata, BaseUpgrade::Beryl)
})
.expect("gas-footprint op must not fatally error");
assert!(!out.is_revert(), "gas-footprint op must not revert");

rayyan224
rayyan224 previously approved these changes Jul 17, 2026
@stephancill
stephancill added this pull request to the merge queue Jul 17, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Jul 17, 2026
@osr21

osr21 commented Jul 19, 2026

Copy link
Copy Markdown

Consumer-side ABI integration notes from BasePay

Hi team — commenting from the consumer/dApp side. We've integrated the B20 factory precompile into BasePay (https://github.com/osr21/basepay-dapp) and want to share the ABI we derived from the spec, which aligns with what these golden tests cover:

// B20 factory ABI used in BasePay (wagmi.ts)
export const B20_FACTORY_ABI = [
  {
    type: "function", name: "createB20",
    inputs: [
      { name: "variant",   type: "uint8"   }, // 0 = ASSET, 1 = STABLECOIN
      { name: "salt",      type: "bytes32" }, // deterministic address derivation
      { name: "params",    type: "bytes"   }, // ABI-encoded (name, symbol, admin, [currencyCode])
      { name: "initCalls", type: "bytes[]" }, // optional post-creation calls
    ],
    outputs: [{ name: "tokenAddress", type: "address" }],
    stateMutability: "nonpayable",
  },
  {
    type: "function", name: "getB20Address",
    inputs: [
      { name: "variant",  type: "uint8"   },
      { name: "deployer", type: "address" },
      { name: "salt",     type: "bytes32" },
    ],
    outputs: [{ name: "", type: "address" }],
    stateMutability: "view",
  },
] as const;

Error guards we're surfacing to users: TokenAlreadyExists (pre-flight check with getB20Address), UnsupportedVersion, InvalidDecimals.

One thing that would help dApp developers: a canonical npm-published ABI package (or typed viem contract definition) for the B20 factory and B20 token interface, so we're not hand-writing ABIs from the spec. Is that on the roadmap alongside these golden tests?

@stephancill
stephancill force-pushed the stephancilliers/bop-424-factory-goldens branch from bd96a39 to 3e2cc5d Compare July 20, 2026 13:28
@cb-heimdall
cb-heimdall dismissed rayyan224’s stale review July 20, 2026 13:28

Approved review 4726210827 from rayyan224 is now dismissed due to new commit. Re-request for approval.

Comment on lines +387 to +391
read_stablecoin(&mut s, token, |t| {
assert_eq!(t.name().unwrap(), SC_NAME);
assert_eq!(t.symbol().unwrap(), SC_SYMBOL);
assert!(t.has_role(B20TokenRole::DefaultAdmin.id(), ADMIN).unwrap());
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The asset golden (golden_create_asset) asserts the variant-specific field (decimals), but this stablecoin golden doesn't assert that currency was stored correctly. B20StablecoinStorage exposes currency() via StablecoinAccounting — adding an assertion here would close the gap and pin the currency write in the frozen manifest.

Suggested change
read_stablecoin(&mut s, token, |t| {
assert_eq!(t.name().unwrap(), SC_NAME);
assert_eq!(t.symbol().unwrap(), SC_SYMBOL);
assert!(t.has_role(B20TokenRole::DefaultAdmin.id(), ADMIN).unwrap());
});
read_stablecoin(&mut s, token, |t| {
assert_eq!(t.name().unwrap(), SC_NAME);
assert_eq!(t.symbol().unwrap(), SC_SYMBOL);
assert_eq!(StablecoinAccounting::currency(t).unwrap(), CURRENCY);
assert!(t.has_role(B20TokenRole::DefaultAdmin.id(), ADMIN).unwrap());
});

Comment on lines +736 to +741
let expected: &[(&str, (u64, u64, u64))] = &[
("create_asset", (3, 7, 1)),
("create_stablecoin", (3, 6, 1)),
("get_b20_address", (0, 0, 1)),
("is_b20", (0, 0, 0)),
];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: isB20Initialized is a distinct op in the coverage checklist (line 784) but its storage-access footprint isn't pinned here. It exercises a different code path from isB20 (storage read vs. pure address-prefix check). Consider adding it for completeness:

("is_b20_initialized", (1, 0, 0)),  // or whatever the actual counts are

@osr21

osr21 commented Jul 20, 2026

Copy link
Copy Markdown

Follow-up from BasePay integration (consumer-side review)

Good coverage overall — the compile-time coverage checklist (v1_op_coverage_checklist with no _ arm) is a clean pattern and the keccak storage-hash snapshot approach is solid for the frozen-manifest baseline. A few observations from integrating the B20 factory ABI into BasePay:


1. currency field not asserted in golden_create_stablecoin (reinforcing the automated review)

golden_create_asset asserts the variant-specific field (decimals):

assert_eq!(AssetAccounting::decimals(t).unwrap(), ASSET_DECIMALS);

golden_create_stablecoin asserts name, symbol, and the admin role — but not currency:

read_stablecoin(&mut s, token, |t| {
    assert_eq!(t.name().unwrap(), SC_NAME);
    assert_eq!(t.symbol().unwrap(), SC_SYMBOL);
    assert!(t.has_role(B20TokenRole::DefaultAdmin.id(), ADMIN).unwrap());
    // gap: currency write is pinned in the storage hash but not verified explicitly
});

From the consumer side this matters: BasePay's multi-currency grid reads the on-chain currency() accessor to display the ISO code and route to the correct fiat display. If that write were ever silently broken (e.g. by a storage-layout change), the storage-hash pin would catch it at the hash level, but a direct assertion makes the invariant legible in the test output and documents the expected accessor value.

Suggested addition:

read_stablecoin(&mut s, token, |t| {
    assert_eq!(t.name().unwrap(), SC_NAME);
    assert_eq!(t.symbol().unwrap(), SC_SYMBOL);
    assert_eq!(t.decimals().unwrap(), 6_u8); // STABLECOIN always 6 — worth pinning explicitly
    assert_eq!(StablecoinAccounting::currency(t).unwrap(), CURRENCY);
    assert!(t.has_role(B20TokenRole::DefaultAdmin.id(), ADMIN).unwrap());
});

2. InvalidCurrency validation scope — what exactly is rejected?

golden_create_reverts_invalid_currency covers the lowercase case ("usd"). To build correct client-side input sanitization on the BasePay /b20 create-token page, I need to know the exact validation rules:

  • Is validation strictly uppercase-only, or is there a regex/length constraint?
  • Is it ISO 4217 list-membership (i.e. only known codes accepted) or just /^[A-Z]{3,4}$/?
  • Are 4-character codes ("USDC", "EURC") valid currency codes, or is it strictly 3 characters?

A test for at least one of the boundary cases (e.g. a 4-char code, or an all-uppercase-but-unknown code like "XYZ") would make the validation contract explicit at the spec level.


3. Gas asymmetry: create_stablecoin is 1 sstore cheaper than create_asset (6 vs 7)

From golden_gas_footprints:

("create_asset",      (3, 7, 1)),
("create_stablecoin", (3, 6, 1)),

I assume create_asset stores decimals as a separate slot (since it's configurable), while STABLECOIN fixes decimals at 6 and doesn't store it — accounting for the -1 sstore. Is that right? BasePay shows estimated gas for each operation, so confirming the intent here helps document the layout for external tooling.


Tested against: BasePay on Base Mainnet, factory ABI derived from v1.1.1 spec. All ABI interactions (createB20 / getB20Address / isB20 / transferWithMemo) working as expected.

rayyan224
rayyan224 previously approved these changes Jul 20, 2026
@stephancill
stephancill force-pushed the stephancilliers/bop-424-factory-goldens branch from 3e2cc5d to 8954b3f Compare July 20, 2026 13:52
@cb-heimdall
cb-heimdall dismissed rayyan224’s stale review July 20, 2026 13:52

Approved review 4735543548 from rayyan224 is now dismissed due to new commit. Re-request for approval.

@rayyan224
rayyan224 enabled auto-merge July 20, 2026 14:16
@rayyan224
rayyan224 self-requested a review July 20, 2026 14:16
@rayyan224
rayyan224 added this pull request to the merge queue Jul 20, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Jul 20, 2026
Pins Factory V1 behavior of the B-20 precompile: token creation flows
(createB20 asset + stablecoin, with/without initCalls, zero-admin), deterministic
address derivation (getB20Address), prefix/initialized reads (isB20,
isB20Initialized), and every validation guard (TokenAlreadyExists,
UnsupportedVersion, InvalidDecimals, MissingRequiredField, InvalidCurrency,
InitCallFailed, typed-revert propagation, malformed params, not-activated,
NonPayable). Each case asserts returned bytes/typed reverts, the created token's
state + code, emitted events (B20Created + init events), and a per-case keccak
storage-hash snapshot scoped to the factory + created token; plus per-op gas
footprints and a compile-time op-coverage checklist.

Authored and blessed against the shipped v1.1.1 (pre-versioned) factory, then
confirmed identical pins pass on the versioned (resolver-gated) structure -
proving the SOLID/versioned migration (BOP-418) is behavior-preserving. ~96%
line coverage of b20_factory/logic/v1.rs (remainder are defensive error edges).

Test-only: new integration test + one Cargo.toml [[test]] stanza; no
source/logic changes.

Co-authored-by: OpenCode <opencode-noreply@coinbase.com>
@stephancill
stephancill force-pushed the stephancilliers/bop-424-factory-goldens branch from 8954b3f to 1707d4a Compare July 20, 2026 14:36
@github-actions

Copy link
Copy Markdown
Contributor

Review Summary

Test-only PR adding 806 lines of golden/snapshot tests for Factory V1 behavior. No production code changes.

No new issues found. All prior inline review comments have been addressed in the current revision:

  • gas() helper now asserts !out.is_revert() (line 699)
  • golden_create_stablecoin now asserts currency (line 391)
  • golden_gas_footprints now includes is_b20_initialized (lines 747-752, 760)

The code is well-structured: the compile-time exhaustive-match coverage checklist (v1_op_coverage_checklist) is a solid technique for ensuring new ABI ops get pinned, the BLESS_GOLDEN workflow is clearly documented, and the hash_state scoping to factory+token addresses (excluding activation scaffolding) keeps the pins stable.

LGTM — no blocking concerns.

@github-actions

Copy link
Copy Markdown
Contributor

✅ base-std fork tests: all 616 passed

base/base is fully in sync with the base-std spec.

Dependency Ref Commit
base-std main 4658f1b7
base-anvil dccbdfd0b37d364cc50e0e15f1686ab029543c6f dccbdfd0

@stephancill
stephancill enabled auto-merge July 20, 2026 14:48
@stephancill
stephancill added this pull request to the merge queue Jul 20, 2026
Merged via the queue into main with commit d07bdb2 Jul 20, 2026
24 checks passed
@stephancill
stephancill deleted the stephancilliers/bop-424-factory-goldens branch July 20, 2026 15:23
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.

4 participants