Skip to content

feat(b20-asset): ERC-8056 scaled multiplier (scheduled updates + conformance)#173

Open
robriks wants to merge 2 commits into
mainfrom
markusosterlund/bop-431-erc8056-scaled-multiplier
Open

feat(b20-asset): ERC-8056 scaled multiplier (scheduled updates + conformance)#173
robriks wants to merge 2 commits into
mainfrom
markusosterlund/bop-431-erc8056-scaled-multiplier

Conversation

@robriks

@robriks robriks commented Jul 21, 2026

Copy link
Copy Markdown

Summary

ERC-8056 ("Scaled UI Amount") conformance for the B20 Asset variant, plus on-chain scheduling of multiplier updates for corporate actions (stock splits / reinvested dividends).

  • Scheduled setUIMultiplier(new, effectiveAt) + cancelScheduledMultiplier; instant updateMultiplier retained as a failsafe ("useful tech debt").
  • ERC-8056 surface: uiMultiplier, newUIMultiplier/effectiveAt, Balances (balanceOfUI/totalSupplyUI), ERC-165; event widened to UIMultiplierUpdated, new MultiplierUpdateCancelled.
  • Packed PendingMultiplier at slot 4 of base.b20.asset (uint128 ceiling as guard against unlikely-but-possible eventual multiplier overflow after repeated splits)

BOP-431

@linear

linear Bot commented Jul 21, 2026

Copy link
Copy Markdown

BOP-431

@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown

Interface Coverage

✅ All interface functions have test coverage.

@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown

📊 Forge Coverage (src/lib/)

🟡 ≥95% across all metrics — some metrics below 99%.

File Lines Stmts Branches Funcs
🔴 B20FactoryLib.sol 95.40% 96.00% 100.00% 90.00%
🔴 test/lib/ForceFeeder.sol 0.00% 0.00% 100.00% 0.00%
🔴 test/lib/PrecompileProbe.sol 0.00% 0.00% 0.00% 0.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.96% 99.10% 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 96.41% 97.02% 98.51% 96.15%

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

@robriks robriks self-assigned this Jul 21, 2026
Re-adds test/unit/storage/MockB20AssetSlotHelpers.t.sol, the sole caller of
MockB20AssetStorage.pendingMultiplierValue / pendingEffectiveAt. It had been
dropped, leaving those packed-slot decoders uncovered and failing the
Interface Coverage CI gate on #173.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown

⚠️ Fork tests: 60 failed, 616 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_cancelScheduledMultiplier_revert_matured(uint256): custom error 0x93d32890; counterexample: calldata=0x7675e37600000000000000000000000000000000000000000000000000000000000031a6 args=[12710 [1.271e4]]
  • test_cancelScheduledMultiplier_revert_unauthorized(address): custom error 0x93d32890; counterexample: calldata=0x4617706800000000000000000000000000000000000000000000000000000000000008b9 args=[0x00000000000000000000000000000000000008B9]
  • test_cancelScheduledMultiplier_success_clearsPending(uint256,uint256): custom error 0x93d32890; counterexample: calldata=0x54f35f71b582ebae03f16fee49a6763f78df482fb11ae73f103ed0d330bbe556aa90a43f0000000000000000000000000000000000000000000000000000000000000050 args=[82099942313829880515582171778066873600068812804006977395700351106844409766975 [8.209e76], 80]
  • test_cancelScheduledMultiplier_success_emitsEvent(uint256,uint256): custom error 0x93d32890; counterexample: calldata=0xdb3ca1fd000000000000000000cd2f13f26632b98c800b5de9e1b6e9619cfb8468e4fe1a00000000000000000000000000000000000000000e05e5983ad08b3e2e9a0425 args=[19652713057430537011694114656976247463538563928224497178 [1.965e55], 4339918996664620809167963173 [4.339e27]]
  • test_cancelScheduledMultiplier_success_leavesCurrentUntouched(uint256): custom error 0x93d32890; counterexample: calldata=0x7f58ca6b000000000000000000000000000000000000000025695d56fc2f0b5a9aef72dc args=[11578323359123956327424946908 [1.157e28]]
  • test_erc8056Surface_success_aliasesResolve(uint256): custom error 0xa60bf13d; counterexample: calldata=0x91648ceefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd args=[115792089237316195423570985008687907853269984665640564039457584007913129639933 [1.157e77]]
  • test_multiplierEvent_success_widenedToUIMultiplierUpdated(uint256): UIMultiplierUpdated(old,new,effAt) must be emitted: -1 <= -1; counterexample: calldata=0x1b209288000000000000000000000000000000000000000000000000000000000000267b args=[9851]
  • test_newUIMultiplier_success_maturedMirrorsUiMultiplier(uint256): custom error 0x93d32890; counterexample: calldata=0x42f4772200000000000000000000000000000000000000000000000000000000000f4240 args=[1000000 [1e6]]
  • test_newUIMultiplier_success_reportsLivePending(uint256,uint256): custom error 0xa60bf13d; counterexample: calldata=0xf706898000000000000000000000000000000000000000000a345c8457b2489a9d4c13aa000000000000000000000000000000004a11fab25950dd749a88180dcac21fde args=[3158151139839246197408404394 [3.158e27], 98456225467334632854843904080558039006 [9.845e37]]
  • test_pendingSlot_success_decodesScheduledPending(uint256,uint256): custom error 0x93d32890; counterexample: calldata=0x24184370000000000000000000000000000000000000000000000000000000000000005f0000000000000000000000000000000000000000000000000000000000000040 args=[95, 64]
  • test_setUIMultiplier_revert_aboveUint128Ceiling(uint256): Error != expected error: custom error 0x93d32890 != InvalidMultiplier(); counterexample: calldata=0x2f72f86a0000000000000000000000000000000000000000000000000000000000000001 args=[1]
  • test_setUIMultiplier_revert_effectiveAtInPast(uint256): Error != expected error: custom error 0x93d32890 != EffectiveAtInPast(420183766 [4.201e8]); counterexample: calldata=0x66557d060000000000000000000000000000000004969680e5d43fe0bf5a05195647c0e0 args=[6098809085693898390087360988951593184 [6.098e36]]
  • test_setUIMultiplier_revert_effectiveAtTooFar(uint256): Error != expected error: custom error 0x93d32890 != EffectiveAtTooFar(115792089237316195423570985008687907853269984665640564039439137263839420099082 [1.157e77]); counterexample: calldata=0x58e942670000000000000000000000000000000000000000000000000000000000002a0a args=[10762 [1.076e4]]
  • test_setUIMultiplier_revert_scheduleOverlap(uint256,uint256): custom error 0x93d32890; counterexample: calldata=0x7daa316996d64dafe2c790596430196b982ad1da3221cb3b0f4e6e2df77f2e4f71a90036c78b71fee795ddd74aff64ea9b2474194c938c3196430e10bb5f01ed48434000 args=[68225568735694575856320728037824936588139205024441132732907719601679206252598 [6.822e76], 90256635379626638402994590225948139774634157376374926850808936957150668996608 [9.025e76]]
  • test_setUIMultiplier_revert_unauthorized(address,uint256): Error != expected error: custom error 0x93d32890 != AccessControlUnauthorizedAccount(0xE39eA17BF3d7F8edC97DeaFccF6e25E9cB21aA5D, 0x97667070c54ef182b0f5858b034beac1b6f3089aa2d3188bb1e8929f4fa9b929); counterexample: calldata=0x0c5956e1000000000000000000000000e39ea17bf3d7f8edc97deafccf6e25e9cb21aa5d0000000000000000000000000000000000000000000000000000000000000003 args=[0xE39eA17BF3d7F8edC97DeaFccF6e25E9cB21aA5D, 3]
  • test_setUIMultiplier_success_emitsEvent(uint256,uint256): log != expected log; counterexample: calldata=0x0ed4dff60000000000000000000000000000000000000000000318eaed6fce800fa0460900000000000000000000000000000000000000000adc8e5b1d837b5a9bf54d61 args=[3744447901666565861426697 [3.744e24], 3361486035349626381015534945 [3.361e27]]
  • test_setUIMultiplier_success_lazyFlipAtBoundary(uint256): custom error 0x93d32890; counterexample: calldata=0xc98da67c00b155050b3d83e2b9979aea46c18d8ea2b23e917e33eff2fc875359ab26e692 args=[313318714888725644924149844416607612979009363429644371136789189428330817170 [3.133e74]]
  • test_supportsInterface_success_unknownFalse(bytes4): custom error 0x01ffc9a7; counterexample: calldata=0xf84ff4060000000000000000000000000000000000000000000000000000000000000000 args=[0x00000000]
  • test_totalSupplyUI_success_scalesByMultiplier(uint256,uint256): custom error 0x9bea6429; counterexample: calldata=0x5c65438200000000000000000000000000000000000000000000000000000000000011640000000000000000000000000000000000000000000000000000000000000016 args=[4452, 22]
  • test_totalSupplyUI_success_zeroWhenNoSupply(uint256): custom error 0x9bea6429; counterexample: calldata=0x605620a900000000000000000000000000000000000000000001f9a1a0fce4889d0b82db args=[2387776600043053671744219 [2.387e24]]
  • test_updateMultiplier_revert_aboveUint128Ceiling(uint256): next call did not revert as expected; counterexample: calldata=0x2b852f5600000000000000000000000000000000000000000000000000000000000015be args=[5566]
  • test_updateMultiplier_success_emitsEvent(uint256): log != expected log; counterexample: calldata=0xfda1a8f9000000000000000000000000000000000000000000000000000000004bd27648 args=[1272084040 [1.272e9]]
    [FAIL: Error != expected error: custom error 0x93d32890 != InvalidMultiplier()] test_setUIMultiplier_revert_zeroMultiplier() (gas: 45906)
    [FAIL: Error != expected error: custom error 0xc4b1e3e7 != NoScheduledMultiplier()] test_cancelScheduledMultiplier_revert_noPending() (gas: 45755)
    [FAIL: custom error 0x01ffc9a7] test_supportsInterface_success_claimedIds() (gas: 5221)
    [FAIL: custom error 0x93d32890] test_b20AssetLayout_success_populatedSnapshotMatchesAllSlots() (gas: 71310)
    [FAIL: custom error 0x93d32890] test_reorder_success_cancelThenScheduleInOneBracket() (gas: 45458)
    [FAIL: custom error 0x93d32890] test_setUIMultiplier_success_materializesMaturedPending() (gas: 45484)
    [FAIL: custom error 0x93d32890] test_updateMultiplier_success_clearsLivePending() (gas: 45462)
    [FAIL: custom error 0x93d32890] test_updateMultiplier_success_clearsMaturedPendingNoCancelEvent() (gas: 45463)

/// @dev `uiMultiplier` aliases `multiplier`; `balanceOfUI` aliases `scaledBalanceOf`; the pending
/// surface, `totalSupplyUI`, and `supportsInterface` all resolve. These typed calls only
/// compile against the current interface, so their presence is the guard. Regression: BOP-431.
function test_erc8056Surface_success_aliasesResolve(uint256 amount) public {

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.

Hey question is regressions testing pass versions ?


/// @notice Emitted by `updateMultiplier`.
event MultiplierUpdated(uint256 multiplier);
/// @notice Base-local companion to ERC-8056's `UIMultiplierUpdated`. Fills the standard's

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 we can simplify this comment to. Old multiprler scheduled has been cancelled something along those lines

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