test(xvs-vault): invariant/fuzz suite, PoCs & live-fork verification#692
Draft
Debugger022 wants to merge 11 commits into
Draft
test(xvs-vault): invariant/fuzz suite, PoCs & live-fork verification#692Debugger022 wants to merge 11 commits into
Debugger022 wants to merge 11 commits into
Conversation
Adversarial fuzz/invariant layer for the 0.5.16 XVSVault, isolated from the Hardhat setup (scoped src, separate out/cache). Covers solvency, pending accounting, withdrawal lock, and vote conservation, plus targeted vote-inflation scenarios (300k stake must not yield >300k votes). - invariants: I1 solvency, I2/I3 pending, V1 vote conservation, V4 vote solvency - scenarios: S1-S6 delta/absolute seam, same-block, snapshot, execute-neutrality - 0.8 tests drive the 0.5.16 vault via deployCode + IXVSVault - 15 tests, all passing
Debugger022
force-pushed
the
feat/VPD-1576
branch
from
July 9, 2026 07:47
c0765f3 to
490fbc6
Compare
Extend the XVSVault suite for the 'malicious holder' threat model: 2-3 colluding accounts that already own XVS trying to steal funds or manipulate votes through the public API. Adds cross-user integrity invariants (victim untouched, no principal inflation), delegateBySig replay/expiry/chainId/ malleability guards, reward-path integrity (double-claim, vault-debt repay, pending slice), and uint96 vote-cap overflow checks.
…teBySig fuzz coverage
Perform each pre-0.8 exploit class against the 0.5.16 vault and assert it is blocked: H1 integer underflow, H2 reward-debt wrap-to-mint, H3 signature malleability replay, H4 ecrecover forge, H5 checkpoint double-vote. All 5 blocked; no valid hack found.
Run H1-H5 against the deployed vault (proxy 0x0511..9204, impl 0x74c8..B378), real XVS + store, via ARCHIVE_NODE_bscmainnet (skips when unset). Fund attackers with deal; mock the Prime xvsUpdated hook (live pool 0 is the Prime pool, which reverts on a fork) to isolate vault logic. All 5 blocked on live bytecode.
…lity, I11a) - StoreShortfall P1 fully overlapped RewardIntegrity X6 (same under-funded-store debt-repay flow); drop the file and the now-dead _storeFunding base hook. - Solc0516Hacks H3 duplicated DelegateBySig X5c (and fork H3); remove local copy. - Fold I11a's nonce-consumed check into X4 (which already relays); drop I11a. No coverage lost. 45 tests pass.
…path Cover the pre-upgrade request branch that no prior test exercised: requestOld- Withdrawal (burns votes, skips totalPendingWithdrawals) + the beforeUpgrade executeWithdrawal branch (pays reward on full amount, no _moveDelegates). - LegacyPathInvariants: solvency, vote conservation/solvency, emission cap hold under fuzzed legacy+new interleavings (I2 intentionally excluded — legacy requests legitimately diverge totalPendingWithdrawals). - LegacyPath scenarios: legacy vote consistency, reward bounded, and a documented latent finding — mixed legacy+new requests permanently freeze a position (inconsistent state). Inert on mainnet: production requestWithdrawal blocks it (LG_GUARD proves the guard is load-bearing) and requestOldWithdrawal does not exist on the deployed contract.
LG_FREEZE only demonstrated behavior of the scenario-only requestOldWithdrawal used in an order production forbids — it exercised no deployed code, so it was not a real finding. Keep LG1/LG2 (the beforeUpgrade execute branch is live mainnet code) and LG_GUARD (tests a real production guard). Document legacy files in the README.
Findings from the XVSVault security engagement (2 manual audits, invariant + legacy-path fuzzing, hack PoCs local + live-fork, and 5 agent audits across core logic, integration, governance lifecycle, economics, cross-chain periphery). No Critical/High/Medium; five Low items (all centralization or defense-in-depth, none attacker-reachable) with locations, impact, reachability, and fixes.
Runnable PoCs (test-fuzz/audits/pocs/): L1 Prime-hook freeze, L2 store emergency-withdraw drain, I1 donation dilution, I2 zero-pending request (local); L3 governance vote-snapshot timing (live fork). L4/L5 are governance-contracts scope (need Governor/executor + LZ harness) and are documented as PoC code in the report appendix. Each finding now links its PoC. 59 tests pass.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adversarial Foundry fuzz / invariant / PoC layer for the 0.5.16
XVSVault, fully isolated from the Hardhat setup (sources scoped totest-fuzz/, output in gitignoredout-forge//cache-forge/). Motivated by the active XVS-accumulation threat: prove the vault an attacker would actually hit is not exploitable. 59 tests — all pass; no exploitable vulnerability found, on both the repo source and the live bscmainnet bytecode.Stateful invariants (checked after every handler call)
Core (
invariants/XVSVaultInvariants.t.sol,RewardSolvency.t.sol):balanceOf(vault) >= Σ user.amounttotalPendingWithdrawals == Σ user.pendingWithdrawalsuser.pendingWithdrawals <= user.amountΣ currentVotes == Σ (amount − pending)over delegatorsΣ currentVotes <= balanceOf(vault) − totalPending<= rewardPerBlock * elapsedBlockspendingRewardnever reverts (no reward-debt underflow → no user DoS)Cross-user integrity (
invariants/CrossUserIntegrity.t.sol): X1 victim frozen against all attacker actions; X2Σ attacker withdrawn ≤ Σ attacker deposited.Legacy path (
invariants/LegacyPathInvariants.t.sol) — the pre-upgrade (afterUpgrade=0) withdrawal branch that live mainnet users with old requests still hit; solvency + vote conservation/solvency + emission cap under fuzzed legacy/new interleavings. (I2 excluded by design — legacy requests legitimately divergetotalPendingWithdrawals.) Held under 600k calls/invariant on the deep profile.Scenario fuzz tests
Vote inflation (
VoteInflation.t.sol): S1–S6 — the delta/absolute checkpoint seam (re-delegate after partial request, same-target re-delegate, undelegated deposit, same-block storm, historical snapshot ≤ stake, execute vote-neutral).Reward path (
RewardDebt.t.sol,RewardIntegrity.t.sol): R1a–c reward-debt consistency through the withdrawal lifecycle; X3a/b no double-collect / correct account credited; X6 under-funded-store debt repaid exactly once; X9 pending slice stops earning.delegateBySig (
DelegateBySig.t.sol): X4 relayed-once + replay rejected, X5a expiry, X5b wrong-chainId, X5c malleable high-s rejected, I11d forged payload never moves a victim.Multi-pool (
MultiPool.t.sol) & vote-cap overflow (VoteOverflow.t.sol): M1 rewards isolated per pool, M2 only the XVS pool grants votes, X7a/b uint96 vote-cap guards.Legacy scenarios (
LegacyPath.t.sol): LG1 legacy request→execute keeps votes == live stake, LG2 legacy-branch reward bounded by emission, LG_GUARD the production guard blocks new requests while a beforeUpgrade one is pending (proving that guard is load-bearing).Solidity-0.5.x hack PoCs — local and on live bytecode
Each performs a canonical pre-0.8 exploit and asserts it is blocked (a failed exploit is the proof).
scenarios/Solc0516Hacks.t.sol(local) andfork/ForkLiveHacks.t.sol(fork: proxy0x0511…9204, impl0x74c8…B378, real XVS + store):stake + 1(would wrapamountto ~2^256).suband drain the store(v'=28, s'=n−s)of a used delegationDelegateBySig::X5c+ fork)ecrecover(0)forgeThe fork suite funds attacker wallets via
deal, skips whenARCHIVE_NODE_bscmainnetis unset, andvm.mockCalls the PrimexvsUpdatedhook (live pool 0 is the Prime pool, which reverts on a fork) to isolate the vault's own logic.Technique
forge-std needs solc ≥0.6, so test files are 0.8 and instantiate the 0.5.16 vault via
deployCode(...)+IXVSVault; an anchor forces the 0.5.16 graph to compile into named artifacts. The invariant handler includes adversarial actions (donate— raw transfer bypassingdeposit;warpRoll— BSC-like block/time advance clearing the 7-day lock). Nocontracts/source is touched.Run
Result
59/59 pass. No insolvency, vote inflation, lock bypass, cross-user theft, reward-debt underflow, reward mint/double-collect, cross-pool leak, or signature replay — on repo source and live bscmainnet bytecode. Consistent with the two manual audits: the deployed vault has no exploitable path for an XVS holder to manipulate XVS or steal another user's funds.
Security review summary
A consolidated findings report is included at
test-fuzz/audits/xvs-vault-findings.md, covering the full engagement: 2 manual audits, the invariant + legacy-path fuzzing above (600k calls/invariant on the legacy path), the hack PoCs (local + live-fork), and 5 focused audits — core money/vote logic, integration/privileged surface, governance lifecycle (GovernorBravoDelegate+Timelock), economic/game-theory, and cross-chain periphery (OmnichainGovernanceExecutor, proxy, ACM).Result: no Critical / High / Medium. Five Low findings, none attacker-reachable — each is either a trusted-key/centralization power or a defense-in-depth hardening item:
xvsUpdatedhook can freeze deposit/requestWithdrawal (no fund loss)XVSStore.emergencyRewardWithdrawbypasses allowlist & capsetNewOwnerstartBlock(standard Bravo)castVoteBySigballot has no nonce/expiryhasVotedEconomic conclusion: governance capture by the accumulation cluster is not viable (immutable 1.5M quorum, no XVS borrow venue at scale, guardian cancel); only residual is proposal-spam/FUD. Top actionable item is L1 (call the Prime hook defensively so a Prime problem can never block withdrawals). Full detail, locations, and fixes are in the report.
Every finding has a PoC. Vault-scope PoCs are runnable in
test-fuzz/audits/pocs/— L1 (Prime-hook freeze), L2 (store emergency-withdraw drain), I1 (donation dilution), I2 (zero-pending request) run locally; L3 (governance vote-snapshot timing) runs against live bscmainnet bytecode. L4 and L5 are governance-contracts scope (need the Governor/executor + LayerZero harness), so they're documented as PoC code in the report's Appendix. Each finding links its PoC; run withforge test --offline --match-path "test-fuzz/audits/pocs/*".Checklist
test-fuzz/README.md)