Skip to content

fix(vault): keep OZ _totalSupply in step with rebased balances (audit H01) - #289

Merged
thedavidmeister merged 4 commits into
mainfrom
fix/audit-h01-raw-totalsupply
Aug 7, 2026
Merged

fix(vault): keep OZ _totalSupply in step with rebased balances (audit H01)#289
thedavidmeister merged 4 commits into
mainfrom
fix/audit-h01-raw-totalsupply

Conversation

@hardyjosh

@hardyjosh hardyjosh commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Fixes H01 from the Protofire st0x.deploy 5.0 report (July 2026, audited at
ed767bf2, tag sol-v0.1.14) — the only open finding in that report. Both
informationals (I01, I02) were already fixed at aba8db0 / 87ce4d1.

Two commits: the fix itself, then optimizer_runs 5000 → 2000 so it fits under
EIP-170. Full suite is green — 657 passing, and the only failures are fork tests
needing BASE_RPC_URL, which CI supplies. rainix-sol-static passes (slither: 0
results, forge fmt --check clean).

The bug

StoxReceiptVault keeps two records of supply:

  1. totalSupply() — the rebase-aware LibTotalSupply.effectiveTotalSupply()
  2. OZ's raw _totalSupply slot — only ever written by ERC20Upgradeable._update

A stock split rewrites balances but never the raw slot: migrateAccount
writes straight to storage via LibERC20Storage.setUnderlyingBalance, bypassing
_mint / _burn (no Transfer, no supply write). The two records drift.

The raw slot looks like dead weight, but OZ still uses it — burn subtracts
unchecked, mint adds checked. So once balances have been inflated by a
split, a burn of the inflated amount subtracts more than the raw slot holds, the
slot wraps to ~2**256, and from that point every mint reverts with
Panic(0x11)
.

The report's worked example, reproduced verbatim as a test: Alice mints 1000, a
2:1 split completes, Alice redeems 1002 — the raw slot goes 1000 - 1002 and
wraps to 2**256 - 2. The vault holds 998 shares and can never issue more than
1 more wei.

What makes it nasty is that nothing surfaces it. totalSupply(), balanceOf()
and every event keep returning correct, mutually consistent values. No funds are
lost and transfers/redemptions keep working — but new issuance is permanently
capped, and the slot has no write path other than mint/burn, so recovery would
require a beacon implementation upgrade.

Production status — latent, not yet triggered

Checked all 29 live receipt vaults on Base: totalSupply() equals the raw slot
on every one, so no split has ever been applied in production and nothing is
corrupted today. That also means this must land before the first stock split
is scheduled on any live vault
.

The fix

Preserve OZ's own _totalSupply == Σ _balances invariant instead of letting the
rebase break it — this is what the auditors recommended. migrateAccount now
applies the same delta to the raw slot that it applied to the balance, via a new
LibERC20Storage.applyBalanceDeltaToTotalSupply.

Reported supply is unchanged: totalSupply() is still
LibTotalSupply.effectiveTotalSupply() and the per-cursor pot accounting in
onAccountMigrated is untouched. The raw slot now tracks the sum of stored
balances, which is a different quantity from the rebase-aware supply during
partial migration — they converge once every holder has migrated. Every OZ code
path now operates on valid state, so burn's unchecked subtraction and mint's
checked addition are both safe.

Scope is confined to the ERC-20 side. StoxReceipt extends Receipt with no
ERC1155Supply, so there is no raw supply slot on the receipt side, and
StoxWrappedTokenVault never writes storage directly.

Tests

Written before the fix and confirmed failing against unfixed code — the raw slot
wrapped to exactly 2**256 - 2 and the follow-up mint panicked with 0x11,
matching the report.

New StoxReceiptVaultRawTotalSupplyTest (5 tests):

  • testH01RedeemAcrossSplitDoesNotWrapRawTotalSupply — the report's exact table
  • testH01MintAfterRedeemAcrossSplitStillSucceeds — the permanent-issuance-cap impact
  • testRawTotalSupplyFollowsReverseSplit — the shrinking direction
  • testRawTotalSupplyMatchesStoredBalancesDuringPartialMigration — invariant holds mid-migration
  • testFuzzRawTotalSupplyInvariantAcrossSplitAndBurn — arbitrary holdings/split/burn

Plus 3 new LibERC20Storage tests pinning the +2 slot offset in the write
direction (underlyingTotalSupply alone only pinned it for reads) and proving
the write does not disturb any _balances slot. 18 tests pass in total.

Sizing — resolved by lowering optimizer_runs

StoxReceiptVault had a 6-byte runtime margin before this change (24,570 of
24,576 at optimizer_runs = 5000). The smallest correct form of the fix costs
~148 bytes. Measured runtime sizes with the fix applied:

optimizer_runs Runtime size Margin
5000 (current) 24,718 -142
3000 24,456 120
2000 24,037 539
1000 22,936 1,640

Remedies attempted and measured

Adopted: optimizer_runs = 2000. The vault lands at 24,037 with 539 bytes
spare and every production contract is comfortably under the limit. Pointers
regenerate cleanly (5 passes to converge, 14 generated files).

Two in-vault alternatives were measured first and rejected:

  • Hoist the sync to a single call site — worse. Having migrateAccount
    return the balance pair so _update applies one combined delta costs more
    than it saves: 24,809, i.e. 233 over. The return-value plumbing outweighs the
    removed duplicate.
  • unchecked arithmetic — 18 bytes. Sound (two's-complement makes
    supply + new - old exact whenever the true result is representable, which it
    is by construction) but only reaches 24,700, still 124 over. Not kept: it does
    not solve the problem and the checked form is easier to audit.

So the floor for the fix is ~124–142 bytes against 6 available — the optimizer
was the only lever that clears it without a structural redesign.

What the optimizer change does and does not affect

It changes the compiled bytecode, and therefore the deterministic Zoltu address,
of every contract — not just the vault.

No user-facing address moves. Every per-token contract is a BeaconProxy
(confirmed on Base: beacon slot populated, implementation slot empty), with
beacons owned by the Safe at 0xe70d821f…. Token addresses, beacon addresses
and roles are all unaffected.

⚠️ But this is a coordinated multi-contract release, not a single vault
upgrade.
Three singletons must be deployed and pointed at in lockstep:

Contract Why Required by
StoxReceiptVault The H01 fix; vault beacon repointed the fix
StoxCorporateActionsFacet The vault bakes LibProdDeployCurrent.STOX_CORPORATE_ACTIONS_FACET into its fallback(); that address moved, so a vault built from this source delegatecalls into empty code until the facet is redeployed the optimizer change
StoxReceipt ST0xOrchestrator's vault-logic version lock compares live beacon implementations against LibProdDeployCurrent; the receipt's address moved even though its source did not, so the orchestrator halts mint/burn until a new receipt impl is deployed and its beacon repointed the optimizer change

The facet coupling was previously invisible: the facet's bytecode had been
unchanged since 0.1.1, so its candidate and 0_1_1 addresses coincided. Only
CI's fork tests surfaced it — worth knowing before the deploy is planned.

Pin bookkeeping. 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 against the unchanged
0_1_1 pins. No new release tag is cut here — that is a release-time action.

What is genuinely given up. A fresh build of current source no longer
reproduces the previously released artifacts; verifying those against the repo
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. StoxCrossChainParity compares implementation addresses
across chains by reading them on-chain, so existing parity is unaffected.

Gas. Runtime gas rises across all contracts at 2000 runs. .gas-snapshot is
consequently stale; 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.

Worth noting regardless: a 6-byte margin meant any vault change was blocked,
not just this one.

Fork-test fixtures

CI's fork tests caught two fixtures that silently depended on current source
reproducing the 0.1.1 bytecode — an assumption the optimizer change breaks.
Both are fixed at the root rather than re-pinned to new constants:

  • V3UpgradeShadowFork planted the corporate-actions facet at a hardcoded
    _0_1_1 address while planting a freshly-compiled vault that pins
    _CANDIDATE. It now plants the facet at
    LibProdDeployCurrent.STOX_CORPORATE_ACTIONS_FACET — the same source the
    vault reads — so the two cannot drift apart again.
  • 20260619-deploy-v4-authoriser-clone etched a freshly-compiled authoriser
    at the pinned address, with NatSpec asserting its codehash "matches the
    LibProdDeployV4 pin by construction". It now etches the frozen
    RUNTIME_CODE_0_1_1 snapshot, which is what the script's codehash guard
    actually checks against. The production guard is unchanged.

CI status

Greentest, static, legal and git-clean all pass.

Locally, with a Base fork against a public endpoint, 739 pass and the only
failures are RPC infrastructure (other chains' env vars, plus public-endpoint
rate limits) — zero assertion or revert mismatches. rainix-sol-static (slither
across 135 contracts, 0 results, plus forge fmt --check) and
rainix-sol-legal both pass locally too.

Summary by CodeRabbit

  • Bug Fixes

    • Improved account migration and corporate-action handling to keep displayed token supply and balances consistent.
    • Prevented supply calculation mismatches during stock splits, redemptions, minting, reverse splits, and partial migrations.
    • Added safeguards against supply underflow and overflow conditions.
  • Deployment & Reliability

    • Updated V4 deployment configuration and verification pins for coordinated releases.
    • Refreshed reproducibility documentation and historical release snapshots.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR synchronizes OpenZeppelin’s raw _totalSupply with account balances during StoxReceiptVault migration. It adds storage helpers and regression tests, changes optimizer runs from 5000 to 2000, updates deployment test pins, and records the deployment changes.

Changes

Raw total supply synchronization

Layer / File(s) Summary
Raw total-supply storage helpers
src/lib/LibERC20Storage.sol, test/src/lib/TestERC20.sol, test/src/lib/LibTotalSupplyHarness.sol
Adds a dedicated OpenZeppelin total-supply slot constant, direct read/write helpers, and checked balance-delta adjustment logic. Test harnesses use the library setter.
Vault migration total-supply sync
src/concrete/StoxReceiptVault.sol
During account migration, adjusts raw total supply after rewriting the account balance.
Raw total-supply regression coverage
test/src/concrete/TestStoxReceiptVault.sol, test/src/lib/LibERC20Storage.t.sol, test/src/concrete/StoxReceiptVault.rawTotalSupply.t.sol
Adds raw supply access and tests for direct writes, balance preservation, stock splits, redemptions, minting, reverse splits, partial migration, and fuzzed scenarios.
Build and deployment snapshot updates
foundry.toml, test/src/lib/LibProdDeployV4.t.sol, test/script/20260619-deploy-v4-authoriser-clone.t.sol, test/src/concrete/upgrade/V3UpgradeShadowFork.t.sol, CHANGELOG.md
Changes optimizer runs to 2000, updates candidate deployment pins, uses frozen runtime data and current facet addresses, and documents the changes.

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

Possibly related PRs

Suggested reviewers: thedavidmeister

Poem

A rabbit checked the supply at dawn,
And fixed the delta before moving on.
Slots now track balances true,
Tests split and fuzz them too.
🥕 The ledger stays in tune.

🚥 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 clearly and concisely describes the primary H01 fix: synchronizing OpenZeppelin's raw _totalSupply with rebased vault balances.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/audit-h01-raw-totalsupply

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.

Copy link
Copy Markdown
Contributor Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@hardyjosh
hardyjosh marked this pull request as ready for review August 4, 2026 10:11

@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: 2

🤖 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 `@CHANGELOG.md`:
- Around line 17-18: Update the V4 release note around migrateAccount to replace
LibERC20Storage.setUnderlyingTotalSupply with the actual helper
LibERC20Storage.applyBalanceDeltaToTotalSupply, preserving the rest of the
changelog text.

In `@foundry.toml`:
- Line 25: Update the comment adjacent to optimizer_runs in foundry.toml to
describe the current 2000 setting and its measured EIP-170 runtime-size margin,
removing the stale 5000 claim while leaving the configuration unchanged.
🪄 Autofix

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 Plus

Run ID: 0a25adac-1bc8-4414-a131-d69bf18e366a

📥 Commits

Reviewing files that changed from the base of the PR and between 5785920 and 0a8c62c.

⛔ Files ignored due to path filters (12)
  • src/generated/candidate/ST0xOrchestrator.pointers.sol is excluded by !**/generated/**
  • src/generated/candidate/ST0xOrchestratorBeaconSetDeployer.pointers.sol is excluded by !**/generated/**
  • src/generated/candidate/StoxCorporateActionsFacet.pointers.sol is excluded by !**/generated/**
  • src/generated/candidate/StoxOffchainAssetReceiptVaultAuthorizerV1.pointers.sol is excluded by !**/generated/**
  • src/generated/candidate/StoxOffchainAssetReceiptVaultBeaconSetDeployer.pointers.sol is excluded by !**/generated/**
  • src/generated/candidate/StoxOffchainAssetReceiptVaultPaymentMintAuthorizerV1.pointers.sol is excluded by !**/generated/**
  • src/generated/candidate/StoxReceipt.pointers.sol is excluded by !**/generated/**
  • src/generated/candidate/StoxReceiptVault.pointers.sol is excluded by !**/generated/**
  • src/generated/candidate/StoxUnifiedDeployer.pointers.sol is excluded by !**/generated/**
  • src/generated/candidate/StoxWrappedTokenVault.pointers.sol is excluded by !**/generated/**
  • src/generated/candidate/StoxWrappedTokenVaultBeacon.pointers.sol is excluded by !**/generated/**
  • src/generated/candidate/StoxWrappedTokenVaultBeaconSetDeployer.pointers.sol is excluded by !**/generated/**
📒 Files selected for processing (9)
  • CHANGELOG.md
  • foundry.toml
  • src/concrete/StoxReceiptVault.sol
  • src/lib/LibERC20Storage.sol
  • test/src/concrete/StoxReceiptVault.rawTotalSupply.t.sol
  • test/src/concrete/TestStoxReceiptVault.sol
  • test/src/lib/LibERC20Storage.t.sol
  • test/src/lib/LibProdDeployV4.t.sol
  • test/src/lib/TestERC20.sol

Comment thread CHANGELOG.md Outdated
Comment thread foundry.toml
@thedavidmeister

Copy link
Copy Markdown
Contributor

Reviewed 7f15d5f: APPROVE

Independently verified beyond the PR's own claims:

  • Live-chain check re-run today (2026-08-07), extended to Ethereum: all 29 Base receipt vaults AND all 29 Ethereum receipt vaults (productionTokensEthereum) have totalSupply() == raw _totalSupply slot — zero drift anywhere, so the bug is latent on both chains and the fix's checked-arithmetic precondition (supply >= oldBalance) holds at upgrade time.
  • OZ v5 _update mechanism confirmed: burn subtracts the raw slot unchecked, mint adds checked — the wrap-then-brick sequence is real, and 1000 - 1002 → 2**256 - 2 matches.
  • Receipt at rain.vats sol-v0.1.6 is plain ERC1155Upgradeable (no ERC1155Supply) — the receipt-side scope claim holds. StoxWrappedTokenVault has no LibERC20Storage writes.
  • migrateAccount is the only direct balance-write path; the delta sync makes _totalSupply == Σ stored balances inductive across every _update shape, and both LibTotalSupply's pot-invariant proof and LibCorporateAction bootstrap seeding (unmigrated[0] = underlyingTotalSupply()) depend on exactly the invariant this restores.
  • Frozen _0_1_1 pins byte-identical in the generated diff (only _CANDIDATE moved); EIP-170 fit is proven by the real Zoltu CREATE in testDeployAddress*; both CodeRabbit threads resolved; CI fully green.
  • Mutation check on the fix: dropping the sync call, swapping the delta args, or mis-offsetting the slot write are each killed by the new tests.

Two non-blocking notes:

  1. The cited st0x.deploy 5.0 (July 2026) Protofire report is not in audit/protofire/ — only the May 2026 v0.1.1 report is. Convention since Move the Protofire audit under audit/protofire/ so the scan finds it #261 is that reports live there for the roh-scan. Worth committing the PDF (here or as its own PR) once it's shareable.
  2. The coordinated 3-contract release choreography is written Base-only, but Ethereum has 29 live vaults on its own beacons (and HyperEVM is in flight in feat(multichain): HyperEVM V4 authoriser clone pin plumbing (RAI-1511) #274ops(script): HyperEVM token deploy (RAI-1511) #278). The beacon repoint + facet + receipt lockstep, and the before-any-split deadline, apply per chain.

…SLOT

The +2 offset from the ERC-7201 root was derived inline in both
underlyingTotalSupply and setUnderlyingTotalSupply, and re-derived a third
time in LibTotalSupplyHarness. Name it once as a file-level constant next to
ERC20_STORAGE_LOCATION and use it for every read and write of the
accumulator.

The harness now writes through LibERC20Storage.setUnderlyingTotalSupply —
its inline assembly predated the H01 fix reintroducing the setter, and its
comment claiming the library exposes no setter was stale.

Candidate pointers regenerated (BuildPointers + fmt, converged): the folded
slot constant shifts StoxReceiptVault and StoxCorporateActionsFacet
bytecode, cascading to ST0xOrchestrator and the three deployers. Frozen
0_1_1 snapshots untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@thedavidmeister

Copy link
Copy Markdown
Contributor

Reviewed 4f01c7f: APPROVE

Delta since 7f15d5f: names the _totalSupply slot once — ERC20_TOTAL_SUPPLY_SLOT = ERC20_STORAGE_LOCATION + 2 — and routes every read/write through it (underlyingTotalSupply, setUnderlyingTotalSupply, and LibTotalSupplyHarness, whose inline re-derivation and stale "no setter exists" comment predated this PR reintroducing the setter). No behavior change: 72/72 across the LibERC20Storage / LibTotalSupply / rawTotalSupply / LibProdDeployV4 suites, BuildPointers re-converged (vault + facet move, orchestrator + three deployers follow, frozen 0_1_1 pins byte-identical), forge fmt clean.

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/lib/LibERC20Storage.sol (1)

127-127: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use a delta-aware addition.

applyBalanceDeltaToTotalSupply adds newBalance before subtracting oldBalance. If the split multiplier is valid and the final supply fits, the intermediate supply + newBalance can still overflow and revert. Branch on the delta sign or add unsigned arithmetic before applying the change.

🤖 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 `@src/lib/LibERC20Storage.sol` at line 127, Update
applyBalanceDeltaToTotalSupply so it applies the balance change using
delta-aware unsigned arithmetic: subtract oldBalance from newBalance before
adding when the delta is positive, and subtract the difference when it is
negative, avoiding the intermediate supply + newBalance overflow while
preserving the final total supply.
🤖 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.

Outside diff comments:
In `@src/lib/LibERC20Storage.sol`:
- Line 127: Update applyBalanceDeltaToTotalSupply so it applies the balance
change using delta-aware unsigned arithmetic: subtract oldBalance from
newBalance before adding when the delta is positive, and subtract the difference
when it is negative, avoiding the intermediate supply + newBalance overflow
while preserving the final total supply.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6b58a235-3541-4834-b552-fa5207d07a95

📥 Commits

Reviewing files that changed from the base of the PR and between 0a8c62c and 4f01c7f.

⛔ Files ignored due to path filters (6)
  • src/generated/candidate/ST0xOrchestrator.pointers.sol is excluded by !**/generated/**
  • src/generated/candidate/ST0xOrchestratorBeaconSetDeployer.pointers.sol is excluded by !**/generated/**
  • src/generated/candidate/StoxCorporateActionsFacet.pointers.sol is excluded by !**/generated/**
  • src/generated/candidate/StoxOffchainAssetReceiptVaultBeaconSetDeployer.pointers.sol is excluded by !**/generated/**
  • src/generated/candidate/StoxReceiptVault.pointers.sol is excluded by !**/generated/**
  • src/generated/candidate/StoxUnifiedDeployer.pointers.sol is excluded by !**/generated/**
📒 Files selected for processing (6)
  • CHANGELOG.md
  • foundry.toml
  • src/lib/LibERC20Storage.sol
  • test/script/20260619-deploy-v4-authoriser-clone.t.sol
  • test/src/concrete/upgrade/V3UpgradeShadowFork.t.sol
  • test/src/lib/LibTotalSupplyHarness.sol

@thedavidmeister

Copy link
Copy Markdown
Contributor

Reviewed 4f01c7f: APPROVE
Rulings-conformance: repo CLAUDE.md has no rulings section (verified by scan); checked against every ruling stated for this work and the standing deploy-repo rulings. (1) "make a constant for that slot and use it consistently" — ERC20_TOTAL_SUPPLY_SLOT is declared once and every read/write (underlyingTotalSupply, setUnderlyingTotalSupply, LibTotalSupplyHarness) resolves through it; a repo-wide sweep found no remaining inline derivation. (2) EIP-170 sizing at the compiler, never by editing audited source — sizing handled solely by optimizer_runs 5000→2000; source edits are the audit fix itself. (3) Deploy-repo convention (audited code only, version↔snapshot↔pin consistency, tag-release lifecycle) — candidate pins repointed, testDeployAddress*/testFrozenRedeploy* enforce consistency, no release tag cut in-PR. (4) Snapshot immutability vs main — frozen 0_1_1 pointer files byte-identical (0 changed lines, verified per commit). (5) Pins embed runtime bytecode — fixtures etch RUNTIME_CODE_0_1_1, not codehash-only pins. (6) Split-lifecycle repos: deploys never block merges — merging without deploy; coordinated per-chain release documented in CHANGELOG. (7) Pragma convention — ^ on the lib, = on concrete/test contracts, unchanged. (8) Comments describe current behavior only — the harness's stale "no setter exists" comment was removed with the code it described. (9) Merge mechanics — explicit per-PR human approval given; --merge (no squash), no branch deletion. The artifact obeys every one.

@thedavidmeister
thedavidmeister merged commit 985dfdd into main Aug 7, 2026
6 checks passed
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