Skip to content

fix(BOP-274): port Rust parity fixes into Solidity mocks #140

Open
stevieraykatz wants to merge 4 commits into
mainfrom
stevekatzman/bop-274-port-rust-parity-fixes-into-solidity-mocks
Open

fix(BOP-274): port Rust parity fixes into Solidity mocks #140
stevieraykatz wants to merge 4 commits into
mainfrom
stevekatzman/bop-274-port-rust-parity-fixes-into-solidity-mocks

Conversation

@stevieraykatz
Copy link
Copy Markdown
Member

Summary

Brings three Solidity-mock behaviors back into parity with the Rust precompile after Roger flagged the divergences:

  • Mint receiver policy_mint no longer skips MINT_RECEIVER_POLICY when called from the factory bootstrap window. Rust enforces the policy unconditionally; the policy itself defaults to ALWAYS_ALLOW_ID for an unconfigured slot, so existing bootstrap flows are unaffected.
  • Burn-blocked target checkburnBlocked no longer skips the "target is blocked" check during bootstrap. Same shape as the mint fix: unconditional enforcement, default-allow when unconfigured.
  • isAnnouncementActive() view — adds the missing view to IB20Asset so inner-call contracts can detect they're executing inside an announce(...) bracket. Backed by an EIP-1153 transient bool flipped at the open and close of the bracket; transient storage auto-clears at tx end, so a revert inside the bracket cannot leave the flag stuck true.

Linear: BOP-274. One commit per subtask.

Test plan

  • forge build clean
  • forge test — 610 tests pass (3 new in test/unit/B20Asset/announcement/isAnnouncementActive.t.sol, no regressions)
  • Rust-side parity remains the source of truth — no Rust changes here

Rust precompile enforces MINT_RECEIVER_POLICY unconditionally on every
mint, including factory-originated mints in the bootstrap window. The
Solidity mock previously bypassed the check whenever `_isPrivileged()`
held, which let initCalls mint to non-authorized accounts. Drop the
bypass so the mock matches Rust semantics. An unconfigured slot still
reads as ALWAYS_ALLOW_ID, so default-deploy bootstrap flows are
unaffected.
…rap (BOP-274)

Rust precompile enforces the "target account must be blocked under
TRANSFER_SENDER_POLICY" guard unconditionally on burnBlocked. The
Solidity mock previously bypassed the check whenever `_isPrivileged()`
held. Drop the bypass so factory-originated burnBlocked calls during the
bootstrap window also revert AccountNotBlocked unless the target is
actually blocked, matching Rust.
…OP-274)

The Rust precompile exposes an is_announcement_active runtime flag that
flips true for the lifetime of an announce(...) bracket and false at all
other times. The Solidity mock did not surface the same view, leaving
inner-call contracts no way to detect they were executing inside a
disclosed corp action.

Add the IB20Asset.isAnnouncementActive() interface entry and back it in
MockB20Asset with an EIP-1153 transient bool set at the top of announce
and cleared at the bottom. Transient storage auto-clears at tx end, so a
revert anywhere in the bracket cannot leave the flag stuck true.
@linear
Copy link
Copy Markdown

linear Bot commented Jun 3, 2026

BOP-274

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 3, 2026

Interface Coverage

✅ All interface functions have test coverage.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 3, 2026

📊 Forge Coverage (src/lib/)

🟢 ≥99% across all metrics.

File Lines Stmts Branches Funcs
🟢 B20FactoryLib.sol 100.00% 100.00% 100.00% 100.00%
🟢 MockActivationRegistry.sol 100.00% 100.00% 100.00% 100.00%
🟢 MockActivationRegistryStorage.sol 100.00% 100.00% 100.00% 100.00%
🟢 MockB20.sol 100.00% 100.00% 100.00% 100.00%
🟢 MockB20Asset.sol 100.00% 100.00% 100.00% 100.00%
🟡 MockB20Factory.sol 98.95% 99.08% 100.00% 100.00%
🟢 MockB20Stablecoin.sol 100.00% 100.00% 100.00% 100.00%
🟢 MockB20Storage.sol 100.00% 100.00% 100.00% 100.00%
🟢 MockPolicyRegistry.sol 100.00% 100.00% 100.00% 100.00%
🟢 MockPolicyRegistryStorage.sol 100.00% 100.00% 100.00% 100.00%
Total 99.86% 99.88% 100.00% 100.00%

Full report: download artifact. To browse locally: make coverage (runs forge coverage + genhtml + opens the HTML report).

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 3, 2026

⚠️ Fork tests: 6 failed, 602 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_isAnnouncementActive_success_falseAfterAnnounce(string): custom error 0xf152925d; counterexample: calldata=0xac049b710000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002643314120603fc2a52046c3b83026513fc392e0b3aee18fbdc2abe1a9a8c2b7f09fa9a75a2e3b0000000000000000000000000000000000000000000000000000 args=["C1A ?¥ Fø0&Q?Ò೮ᏽ«\u{1a68}·🩧Z.;"]`
    [FAIL: custom error 0xf152925d] test_isAnnouncementActive_success_falseAfterRevertedAnnounce() (gas: 73173)
    [FAIL: custom error 0xf152925d] test_isAnnouncementActive_success_falseBeforeAnnounce() (gas: 5193)

@stevieraykatz stevieraykatz changed the title port Rust parity fixes into Solidity mocks (BOP-274) fix(BOP-274): port Rust parity fixes into Solidity mocks Jun 3, 2026
/// exposes the same value via a runtime context flag rather
/// than a storage slot, so there is no persistent layout the
/// Rust impl needs to mirror.
bool internal transient _announcementActive;
Copy link
Copy Markdown
Member Author

@stevieraykatz stevieraykatz Jun 3, 2026

Choose a reason for hiding this comment

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

the transient storage location is only available in solidity as of 0.8.28 but our pragma is open to 0.8.20 (not good). we could:

  1. bump the required version for the whole repo to 0.8.28, or
  2. use an assembly tstore pattern (supported as of 0.8.24)
  3. use a regular storage param

I'm partial to bumping the pragma to at least 0.8.24 and using tstorage (more closely reflects the rust revm memory impl), but I could also see us not bringing this feature to parity.

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.

I think bumping the pragma to support the most expressive/accurate mock implementation makes sense to me? If there were genuine compatibility concerns for other devs importing/working with this source code I'd hesitate more but given that these aren't production implementations I don't think it matters?

///
/// @dev Intended for inner-call contracts dispatched via `internalCalls` (or other
/// contracts they reach) to detect they are executing inside an announcement
/// bracket — useful for issuance, multiplier, and metadata flows whose policy
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.

I'm not sold on this

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.

Even if we wanted the state, it's not clear any external consumer would ever be able to use this given internal calls are internal so no external entity gets control flow when announcement is active

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.

But I'm also not sold the state is even useful given no code is actually using it seemingly?

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