Skip to content

fix(invariants): reject null vault authoriser unconditionally in migration invariant - #246

Closed
thedavidmeister wants to merge 3 commits into
mainfrom
fix/issue-245-null-authoriser-fail-open
Closed

fix(invariants): reject null vault authoriser unconditionally in migration invariant#246
thedavidmeister wants to merge 3 commits into
mainfrom
fix/issue-245-null-authoriser-fail-open

Conversation

@thedavidmeister

@thedavidmeister thedavidmeister commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

What

LibTokenInvariants.assertUniformAuthoriserMigration — and via it LibInvariants.assertAll, the production invariant bundle the cron enforces — failed open on a null (address(0)) vault authoriser. The V4 post pin (LibProdDeployV4.STOX_PROD_AUTHORISER_V4_CLONE) is still unhydrated address(0), so for a vault whose authorizer() is address(0) the window guard's boolean derivation reads:

  • actual != pre0 != V3 → true
  • actual != post0 != 0false
  • true && false → false → no revert → a bricked, ungated vault passes as "already migrated"

This PR rejects actual == address(0) unconditionally — before the migration window is consulted and independent of the deadline — with a new typed error ReceiptVaultNullAuthoriser(address vault) that surfaces the offending vault, declared alongside the sibling ReceiptVault* invariant errors.

Layering

The rejection lives in assertUniformAuthoriserMigration (the semantic authoriser leg), not in the generic LibMigrationInvariant.assertMigration: that helper is a reusable dual-state window primitive with bytes32/uint256 overloads serving migrations where zero can be a legitimate value on either side. Only the authoriser leg knows that a production vault is never legitimately ungated, so that is where "null is never acceptable" belongs. Legitimate window behaviour is untouched: real pre and real post both pass before the deadline, only post at/after it.

Touched files are the operator-tooling invariant layer (src/lib/LibTokenInvariants.sol) plus tests — no deployed vault source, no generated deploy libs, no bytecode change.

QA

  • Discriminating tests: testNullAuthoriserRevertsBeforeDeadline, testNullAuthoriserRevertsAfterDeadline (in new test/src/lib/LibTokenInvariantsMigration.t.sol, using the CURRENT production pins: pre = real V3 authoriser, post = STOX_PROD_AUTHORISER_V4_CLONE = address(0), deadline = V4_SWAP_DEADLINE) — each fails on base: with the zero check removed (base behaviour), both fail with "next call did not revert as expected", i.e. the fail-open passes-without-revert was reproduced and proven. Window-preservation tests testWindowAcceptsPreBeforeDeadline / testWindowAcceptsPostBeforeDeadline / testWindowAcceptsPostAfterDeadline / testWindowRejectsPreAfterDeadline (non-zero sentinels) pass on base and on the fix by design — they pin that the null rejection does not narrow legitimate window behaviour and kill over-correction mutants.
  • Mutations applied:
    • remove the zero check → testNullAuthoriserRevertsBeforeDeadline + testNullAuthoriserRevertsAfterDeadline fail (killed)
    • window-gate the check to block.timestamp < deadlinetestNullAuthoriserRevertsAfterDeadline fails (killed)
    • reverse-gate to block.timestamp >= deadlinetestNullAuthoriserRevertsBeforeDeadline fails (killed)
    • surface address(0) instead of the offending vault → both null tests fail on exact revert-data mismatch (killed)
  • Oracle: the issue's boolean derivation of the window guard (actual != pre && actual != post with post == 0 collapsing to accept actual == 0) plus migration semantics — a null authoriser is never a legitimate state on either side of the swap — derived independently of the implementation.
  • Category check: issue asks unconditional null-authoriser rejection with a specific typed error + zero-sentinel regression tests; both covered (zero-sentinel path exercised before AND after the deadline under the exact production pin configuration). The productionReceiptVaults() exhaustiveness guard is a related gap the issue lists, not the ask — explicitly out of scope here (separate engineering change).

Closes #245

Co-Authored-By: Claude noreply@anthropic.com

Summary by CodeRabbit

  • Bug Fixes

    • Added a dedicated revert error when a production receipt vault reports a null authorizer.
    • Null authorizers are now rejected unconditionally, with behavior consistent both before and after the migration deadline.
    • Continued to enforce migration window/deadline correctness for invalid authorizer transitions.
  • Tests

    • Added migration invariant tests covering null authorizer reverts before/after the deadline.
    • Added tests for accepted valid pre/post authorizer states and rejection when a “pre” vault is detected after the deadline.

…ation invariant

assertUniformAuthoriserMigration passed a vault whose authorizer() is
address(0): the unhydrated STOX_PROD_AUTHORISER_V4_CLONE pin is still
address(0), so a null authoriser satisfied actual == post and read as
already-migrated before the deadline (and after it). A bricked, ungated
vault — exactly what the cron bundle exists to catch — kept cron green.

The null rejection lives in the authoriser leg (the semantic layer that
knows an authoriser must never be null), not in the generic
LibMigrationInvariant helper, whose bytes32/uint256 overloads serve
migrations where zero can be a legitimate state. It fires before the
window check and independent of the deadline, surfacing the offending
vault via the typed ReceiptVaultNullAuthoriser error.

Closes #245

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@thedavidmeister thedavidmeister self-assigned this Jul 10, 2026
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

assertUniformAuthoriserMigration now rejects zero-address vault authorisers unconditionally with a typed error. A harness shim and Forge tests cover null-authoriser rejection, valid migration states, and deadline behavior.

Changes

Null authoriser invariant

Layer / File(s) Summary
Add unconditional null-authoriser validation
src/lib/LibTokenInvariants.sol
Adds ReceiptVaultNullAuthoriser, documents the rule, and checks each vault’s authoriser before migration validation.
Exercise migration boundary behavior
test/src/lib/LibTokenInvariantsHarness.sol, test/src/lib/LibTokenInvariantsMigration.t.sol
Adds an external harness wrapper and mocked-vault tests for null authorisers, pre/post configurations, and deadline boundaries.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Poem

I’m a bunny guarding the vault,
Zero keys shall never halt.
Pre and post may pass the gate,
Null ones meet a typed-error fate.
Deadline tests go hop-hop-hop!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: rejecting null vault authorisers in the migration invariant.
Linked Issues check ✅ Passed The PR matches #245 by unconditionally rejecting address(0) authorisers, adding a typed error, and covering the regression with tests.
Out of Scope Changes check ✅ Passed The harness shim and new tests are supporting changes for the fix and no unrelated scope appears to have been added.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-245-null-authoriser-fail-open

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@thedavidmeister thedavidmeister added the ai:ready AI vetter: passes review, ready for human decision label Jul 10, 2026
@thedavidmeister

Copy link
Copy Markdown
Contributor Author

🤖 ai:vetter
Reviewed 4e2ba7e: ready — closes #245 — fail-open verified against source (post pin STOX_PROD_AUTHORISER_V4_CLONE is address(0), assertMigration accepts actual==post before deadline), unconditional ReceiptVaultNullAuthoriser guard at the correct layer (authoriser leg, generic bytes32/uint256 window helper untouched), both null tests discriminate under real prod pins, window behavior pinned by 4 sentinel tests, mutation claims all check out; note issue's related productionReceiptVaults() exhaustiveness gap stays open as separate work
cost 478 — invariant fail-open fix, prod-pin and window semantics review

@thedavidmeister

Copy link
Copy Markdown
Contributor Author

Design decision: keep the caller-level null-authoriser check (this PR), not a zero-post rejection in the generic assertMigration.

Considered moving the guard into LibMigrationInvariant.assertMigration by rejecting a zero post. It would fix the fail-open at the source and would NOT affect the codehash-migration layer (its post is a real pinned codehash; only its pre is bytes32(0), for an undeployed impl). But rejecting zero post was declined because post == address(0) is a deliberate live state of the authoriser leg:

  • LibProdDeployV4.STOX_PROD_AUTHORISER_V4_CLONE is unhydrated (address(0)), and LibInvariants.assertAll composes the authoriser migration with that zero post on purpose.
  • Before V4_SWAP_DEADLINE the window passes on the V3 pre side — the invariant is cron-covered from merge, before the clone is even deployed.
  • After the deadline the unsatisfiable must-be-post (== 0) branch is the forcing function that red-lines an un-run swap.

Rejecting a zero post would force the authoriser leg to be gated on hydration (if (post != 0)) and would lose that deadline forcing-function. So the null-authoriser rejection stays where the domain knowledge lives — in assertUniformAuthoriserMigration, before it delegates — with its own typed ReceiptVaultNullAuthoriser error. assertMigration stays domain-agnostic (zero is a legitimate pre in the codehash layer). Closing the design question; keeping this fix as-is.

claude added 2 commits July 22, 2026 14:58
Merging main brings LibProdDeployV4 from src/lib to src/generated, which this
branch's new test still imported from the old path — a moved file breaks the
build without ever showing as a merge conflict, so the PR read mergeable while
being uncompilable.

Its header also described the V4 clone pin as unhydrated address(0), which is
what made the original fail-open reachable: a null authoriser collided with the
zero pin and read as already migrated. The pin carries a real address now, so
that collision is gone and a null already reverts — but as MigrationStateDrift,
which names the wrong fault. Rejecting null unconditionally keeps the specific
error correct whatever the pin holds, including a future chain's added
unhydrated.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/src/lib/LibTokenInvariantsMigration.t.sol`:
- Around line 70-75: The null-case NatSpec in
test/src/lib/LibTokenInvariantsMigration.t.sol lines 70-75 must describe the
zero-post collision as a historical regression scenario rather than current
production-pin behavior; update the comments around the null-authorizer test
accordingly. In lines 87-92, remove the assertion that all-zero values satisfy
the current live post pin, while leaving the test logic unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 167ada61-4a30-41b8-8438-5481b121bde3

📥 Commits

Reviewing files that changed from the base of the PR and between 4e2ba7e and c2f4f09.

📒 Files selected for processing (2)
  • src/lib/LibTokenInvariants.sol
  • test/src/lib/LibTokenInvariantsMigration.t.sol

Comment on lines +70 to +75
/// @notice A vault whose `authorizer()` is `address(0)` trips
/// `ReceiptVaultNullAuthoriser` before the deadline, under the
/// production pins whose `post` is still `address(0)`. Pins the
/// fail-closed reading of a null authoriser: without the unconditional
/// rejection, `actual == post` (`0 == 0`) would accept the bricked
/// vault as "already migrated" and the bundle would pass.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct stale V4-pin assertions in the null-case NatSpec.

The supplied PR context says LibProdDeployV4.STOX_PROD_AUTHORISER_V4_CLONE is now non-zero. These comments incorrectly claim the live-pin calls use post == address(0) and would otherwise pass.

  • test/src/lib/LibTokenInvariantsMigration.t.sol#L70-L75: describe the zero-post collision as the historical regression scenario, not the current production-pin state.
  • test/src/lib/LibTokenInvariantsMigration.t.sol#L87-L92: remove the claim that all-zero values would satisfy the current live post pin.
📍 Affects 1 file
  • test/src/lib/LibTokenInvariantsMigration.t.sol#L70-L75 (this comment)
  • test/src/lib/LibTokenInvariantsMigration.t.sol#L87-L92
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/src/lib/LibTokenInvariantsMigration.t.sol` around lines 70 - 75, The
null-case NatSpec in test/src/lib/LibTokenInvariantsMigration.t.sol lines 70-75
must describe the zero-post collision as a historical regression scenario rather
than current production-pin behavior; update the comments around the
null-authorizer test accordingly. In lines 87-92, remove the assertion that
all-zero values satisfy the current live post pin, while leaving the test logic
unchanged.

@thedavidmeister

Copy link
Copy Markdown
Contributor Author

Closing: the defect this PR fixes no longer exists.

The premise was that STOX_PROD_AUTHORISER_V4_CLONE was unhydrated address(0), so the migration window guard failed open — 0 != V3 true, 0 != 0 false, and a bricked ungated vault passed as "already migrated". On main that pin is now hydrated:

src/generated/LibProdDeployV4.sol:141
address constant STOX_PROD_AUTHORISER_V4_CLONE = address(0x315b16faa6eE413faBCa877d3851B3818369f0cD);

With a non-zero post pin, 0 != 0x315b… is true and a null authoriser already reverts. The bug was real when this was opened on 2026-07-10; hydration closed it.

Worth recording why the green CI here was not evidence the change still worked as described: testNullAuthoriserRevertsBeforeDeadline / testNullAuthoriserRevertsAfterDeadline were built on post = STOX_PROD_AUTHORISER_V4_CLONE = address(0). That equality no longer holds, so base reverts too and the tests stopped discriminating. They pass — they just no longer prove anything about base. Green meant "passes", not "would fail without the fix".

The residual concern is not the null check itself but that it is now only incidentally true: it holds because the pin happens to be non-zero, and this repo sat in the fail-open state for weeks. A future migration that introduces an unhydrated post pin reinstates the same hole. That is a pin-drift hazard rather than a live bug, and it belongs on #245 rather than in a PR whose justification has expired.

Not deleting the branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai:ready AI vetter: passes review, ready for human decision

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fail-open: production invariant bundle accepts a null (address(0)) vault authoriser

2 participants