Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,58 @@

## V4 (rain.vats 0.1.6)

### StoxReceiptVault

- **Fix (audit H01): keep OZ's `_totalSupply` in step with rebased balances.**
`migrateAccount` rewrites `_balances` directly via `LibERC20Storage`, so
before this change OZ's own `_totalSupply` accumulator was never adjusted for
a stock split and drifted below the true balance sum. OZ subtracts from that
accumulator **unchecked** on burn and adds to it **checked** on mint, so the
first burn exceeding the stale value wrapped the slot to ~`2**256` and every
subsequent mint reverted with `Panic(0x11)` — permanently capping issuance,
with no external symptom because `totalSupply()`, `balanceOf()` and all events
stayed mutually consistent. The slot has no write path other than mint/burn,
so recovery would have required a beacon implementation upgrade.
`migrateAccount` now applies the balance delta to the raw slot as well, via
the new `LibERC20Storage.setUnderlyingTotalSupply`. Reported supply is
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
unchanged: `totalSupply()` remains `LibTotalSupply.effectiveTotalSupply()` and
the per-cursor pot accounting is untouched. Reported by Protofire as H01 in
the `st0x.deploy 5.0` report (July 2026, audited at `ed767bf2`).

- **`optimizer_runs` lowered 5000 → 2000 to fit the H01 fix under EIP-170.**
`StoxReceiptVault` had only a 6-byte runtime margin (24,570 of 24,576) at 5000
runs, and the smallest correct form of the H01 fix costs ~148 bytes. Measured
with the fix applied: `runs=5000` → 24,718 (over by 142); `runs=3000` →
24,456; `runs=2000` → 24,037 (**539 spare**); `runs=1000` → 22,936. Two
in-vault alternatives were measured and rejected: hoisting the supply sync to
a single call site in `_update` costs _more_ than the duplicated inline
(24,809), and `unchecked` arithmetic recovers only 18 bytes (24,700).

This changes the compiled bytecode — and therefore the deterministic Zoltu
address — of **every** contract, not just the vault. Consequences:

- Nothing deployed moves. Every per-token contract is a `BeaconProxy`, so
token addresses, beacon addresses and roles are all unaffected. Shipping H01
means deploying a new `StoxReceiptVault` implementation and pointing the
beacon at it — which the fix requires regardless of this change.
- The `testDeployAddress*` assertions for `StoxReceipt`,
`StoxWrappedTokenVault`, `StoxWrappedTokenVaultBeacon` and both authorizers
are re-pointed from the frozen `_0_1_1` pins to `_CANDIDATE`, matching what
`StoxReceiptVault` already did. `0_1_1` stays as the frozen historical
record of what is live on Base; `testFrozenRedeploy*` keeps proving those
snapshots redeploy reproducibly, independent of the current optimizer
setting, and the on-chain fork codehash tests still compare live code to the
unchanged `0_1_1` pins.
- The property given up is "a fresh build of current source reproduces the
_previously released_ artifacts". Verifying those against the repo now means
using the frozen snapshot rather than a fresh build. Deploying the stack to
a **new chain** must likewise use the frozen creation code to keep addresses
identical to Base.
- Runtime gas rises across all contracts. `.gas-snapshot` is stale as a
result; it is not gate-checked by CI (`rainix-sol-test` is just
`forge test -vvv`) and should be regenerated in CI, where the fork-test RPC
secrets are available.

### New contracts

- **ST0xOrchestrator**: Singleton mint/burn proxy for the whole ST0x
Expand Down
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ optimizer = true
# changes and dependency-tree bytecode shifts — re-check StoxReceiptVault's
# `forge build --sizes` runtime margin on any such change. via_ir is OFF (tried
# in #144, made the vault larger for this inheritance shape).
optimizer_runs = 5000
optimizer_runs = 2000
Comment thread
coderabbitai[bot] marked this conversation as resolved.

evm_version = "cancun"

Expand Down
18 changes: 18 additions & 0 deletions src/concrete/StoxReceiptVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,24 @@ contract StoxReceiptVault is OffchainAssetReceiptVault {
// Skip the SSTORE when the rasterized balance is unchanged.
if (newBalance != storedBalance) {
LibERC20Storage.setUnderlyingBalance(account, newBalance);

// Rebasing rewrites `_balances` behind OZ's back, so apply the same
// delta to OZ's own `_totalSupply` accumulator and preserve its
// `_totalSupply == Σ _balances` invariant.
//
// The raw slot is not the reported supply — `totalSupply()` above
// is the rebase-aware `LibTotalSupply.effectiveTotalSupply()`, and
// the pot accounting in `onAccountMigrated` below is untouched by
// this write. But OZ's `_update` still subtracts from the raw slot
// **unchecked** on burn and adds to it **checked** on mint. Left
// stale, it drifts below the true balance sum, wraps to ~2**256 on
// the first burn that exceeds it, and from then on every mint
// reverts with `Panic(0x11)` — silently, because `totalSupply()`,
// `balanceOf()` and all events keep agreeing with each other. The
// slot has no write path other than mint/burn, so recovering from
// that state would need a beacon implementation upgrade.
//
LibERC20Storage.applyBalanceDeltaToTotalSupply(storedBalance, newBalance);
}
emit AccountMigrated(account, currentCursor, newCursor, storedBalance, newBalance);

Expand Down
8 changes: 4 additions & 4 deletions src/generated/candidate/ST0xOrchestrator.pointers.sol

Large diffs are not rendered by default.

Loading
Loading