Skip to content

fix(#451): UpdateBaseUnitMints requires the OLD vault to be empty - #462

Merged
dcccrypto merged 1 commit into
mainfrom
fix/451-basemint-old-vault-empty
Sep 4, 2026
Merged

fix(#451): UpdateBaseUnitMints requires the OLD vault to be empty#462
dcccrypto merged 1 commit into
mainfrom
fix/451-basemint-old-vault-empty

Conversation

@dcccrypto

Copy link
Copy Markdown
Owner

Decision A — add the check at the next deploy and coordinate the SDK bump.

Ports upstream's require_empty_vault_token_account guard, which our port dropped.

Why the existing check isn't enough

The handler already refuses when vault / c_tot / insurance are non-zero. Those are the engine's counters. They can all read zero while the SPL token account still holds a balance — tokens that arrived outside the accounted paths, or a residue the counters no longer track.

Switching the mint then orphans those atoms: the vault authority still owns them, but no instruction references that mint any more, so nothing can move them out. Not a theft — a one-way door.

What's checked, and when

Only the mints that actually change, with account indices conditional on that, mirroring upstream exactly. Switching only the secondary passes one extra account, not two; a no-op re-assert of the same mints passes none, so the common case stays cheap.

Our require_empty_vault_token_account reads the amount rather than testing a return value, because our verify_vault_token_account returns () where upstream's returns the balance. Same obligation — and it keeps our stricter canonical-address pin (F-VAULT-FRAG), which upstream's variant doesn't carry.

EngineLockActive matches upstream's error here and the header-counter check in the same handler, so a caller sees one code for "the market is not empty enough to do this".

ABI change: additional accounts, conditionally. Rides the next deploy window alongside #412 — one coordinated SDK bump covers both.

Test helper

configure_base_unit_mints now derives the old-vault accounts from the market's current config and appends exactly what the handler will ask for. Eight callers pass different before/after mint combinations, and hand-computing which needs one account, two, or none is the sort of bookkeeping that goes quietly wrong — so the helper mirrors the handler's own branch structure. Every vault it builds is empty, i.e. the passing case.

Verified

check result
new test asserts both halves: a one-atom vault blocks the switch and leaves the config byte-identical; the same call with an empty vault succeeds and actually applies. The rejection alone would pass against a handler that rejected for any reason.
negative control amount check removed → test fails at v16_wrapper.rs:21261; restored → passes
full CI suite 604 passed / 21 failed"OK: failing set matches the allowlist exactly"

Two tests surfaced the ABI change loudly rather than silently, which is the append behaving as intended.

Upstream

This is the upstream behaviour — ported, not invented. Upstream additionally takes an expected_authority_epoch and calls require_authority_epoch_view, which we don't; that's a separate divergence and deliberately not bundled here.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NgoNgagkvw7i5SSRC3FJ8D

Ports upstream's `require_empty_vault_token_account` guard, which our port
dropped.

WHY THE EXISTING CHECK IS NOT ENOUGH

The handler already refuses when `vault` / `c_tot` / `insurance` are non-zero.
Those are the ENGINE's counters. They can all read zero while the SPL token
account still holds a balance — tokens that arrived outside the accounted paths,
or a residue the counters no longer track.

Switching the mint then ORPHANS those atoms: the vault authority still owns them,
but no instruction references that mint any more, so nothing can move them out.
Not a theft, a one-way door.

WHAT IS CHECKED, AND WHEN

Only the mints that actually CHANGE, with the account indices conditional on that
— mirroring upstream exactly. A caller switching only the secondary passes one
extra account, not two; a no-op re-assert of the same mints passes none, so the
common case stays cheap.

Our `require_empty_vault_token_account` reads the amount rather than testing a
return value, because our `verify_vault_token_account` returns `()` where
upstream's returns the balance. Same obligation, and it keeps our STRICTER
canonical-address pin (F-VAULT-FRAG), which upstream's variant does not carry.

`EngineLockActive` matches upstream's error here and matches the header-counter
check in the same handler, so a caller sees one code for "the market is not empty
enough to do this".

ABI CHANGE: additional accounts, conditionally. Rides the next deploy window
alongside #412 — one coordinated SDK bump covers both.

TEST HELPER: `configure_base_unit_mints` now derives the old-vault accounts from
the market's CURRENT config and appends exactly what the handler will ask for.
Eight callers pass different before/after mint combinations, and hand-computing
which needs one account, two, or none is the sort of bookkeeping that goes quietly
wrong — so the helper mirrors the handler's own branch structure. Every vault it
builds is EMPTY, i.e. the passing case.

VERIFIED
  new test          `..._rejects_a_nonempty_old_vault` asserts BOTH halves: a
                    one-atom vault blocks the switch AND leaves the config
                    byte-identical, and the same call with an EMPTY vault
                    succeeds and actually applies. The rejection alone would pass
                    against a handler that rejected for any reason at all.
  NEGATIVE CONTROL  with the amount check removed, that test FAILS at
                    v16_wrapper.rs:21261; restored, it passes.
  full CI suite     603 passed / 21 failed — `scripts/ci-test.sh` reports
                    "OK: failing set matches the allowlist exactly".

Two tests surfaced the ABI change loudly rather than silently
(`..._changes_primary_and_rotates`, `..._rejects_mismatched_decimals`), which is
the append behaving as intended.

UPSTREAM: this IS the upstream behaviour — ported, not invented. Upstream
additionally takes an `expected_authority_epoch` and calls
`require_authority_epoch_view`, which we do not; that is a separate divergence and
is deliberately NOT bundled here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NgoNgagkvw7i5SSRC3FJ8D
@coderabbitai

coderabbitai Bot commented Sep 3, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: c4581205-b71b-482a-9917-b76961fb95d4


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.

@dcccrypto
dcccrypto merged commit a0f059c into main Sep 4, 2026
3 checks passed
@dcccrypto
dcccrypto deleted the fix/451-basemint-old-vault-empty branch September 4, 2026 00:11
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.

1 participant