Skip to content

test(xvs-vault): invariant/fuzz suite, PoCs & live-fork verification#692

Draft
Debugger022 wants to merge 11 commits into
developfrom
feat/VPD-1576
Draft

test(xvs-vault): invariant/fuzz suite, PoCs & live-fork verification#692
Debugger022 wants to merge 11 commits into
developfrom
feat/VPD-1576

Conversation

@Debugger022

@Debugger022 Debugger022 commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Description

Adversarial Foundry fuzz / invariant / PoC layer for the 0.5.16 XVSVault, fully isolated from the Hardhat setup (sources scoped to test-fuzz/, output in gitignored out-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):

Id Property
I1 Solvency: balanceOf(vault) >= Σ user.amount
I2 totalPendingWithdrawals == Σ user.pendingWithdrawals
I3 user.pendingWithdrawals <= user.amount
V1 Vote conservation: Σ currentVotes == Σ (amount − pending) over delegators
V4 Vote solvency: Σ currentVotes <= balanceOf(vault) − totalPending
R2 Emission cap: store payout <= rewardPerBlock * elapsedBlocks
R1b pendingReward never 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 diverge totalPendingWithdrawals.) 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) and fork/ForkLiveHacks.t.sol (fork: proxy 0x0511…9204, impl 0x74c8…B378, real XVS + store):

Id Pre-0.8 hack class Attack performed Guard
H1 Integer underflow withdraw stake + 1 (would wrap amount to ~2^256) amount guard reverts; stake intact
H2 Reward-debt wrap → infinite mint churn deposit/request/execute to wrap .sub and drain the store SafeMath reverts; store never over-drained
H3 Signature malleability replay replay the malleable twin (v'=28, s'=n−s) of a used delegation OZ ECDSA rejects high-s (lives in DelegateBySig::X5c + fork)
H4 ecrecover(0) forge fabricated low-s sig to empower a chosen delegatee recovers an uncontrollable stakeless phantom; target gets 0 votes
H5 Compound/Venus checkpoint double-vote withdraw + move XVS to a 2nd wallet to vote the same coins twice votes burned at request; total == staked, not 2×

The fork suite funds attacker wallets via deal, skips when ARCHIVE_NODE_bscmainnet is unset, and vm.mockCalls the Prime xvsUpdated hook (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 bypassing deposit; warpRoll — BSC-like block/time advance clearing the 7-day lock). No contracts/ source is touched.

Run

forge test --offline -vv                                  # everything (local)
forge test --offline --match-contract ForkLiveHacksTest   # live-fork PoCs (needs ARCHIVE_NODE_bscmainnet)
FOUNDRY_PROFILE=deep forge test --offline --match-path "test-fuzz/invariants/*"  # 3000 runs × 200 depth

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:

ID Title Reachable
L1 Prime xvsUpdated hook can freeze deposit/requestWithdrawal (no fund loss) No — gov misconfig / bad Prime upgrade
L2 XVSStore.emergencyRewardWithdraw bypasses allowlist & cap No — requires Timelock setNewOwner
L3 Governance vote snapshot at startBlock (standard Bravo) No — bounded by immutable 1.5M quorum
L4 castVoteBySig ballot has no nonce/expiry No — bounded by hasVoted
L5 Cross-chain dedup guard vacuous for proposal id 0 No — trusted-remote + 3-timelock gated

Economic 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 with forge test --offline --match-path "test-fuzz/audits/pocs/*".

Checklist

  • I have updated the documentation to account for the changes in the code. (test-fuzz/README.md)
  • If I added new functionality, I added tests covering it. (this PR is tests)
  • If I fixed a bug, I added a test preventing this bug from silently reappearing again. (N/A — no bug fix)
  • My contribution follows Venus contribution guidelines.

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
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.
@Debugger022 Debugger022 self-assigned this Jul 9, 2026
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.
@Debugger022 Debugger022 changed the title test(xvs-vault): add Foundry invariant and vote-inflation fuzz suite test(xvs-vault): invariant/fuzz suite, 0.5.x hack PoCs & live-fork verification Jul 9, 2026
@Debugger022 Debugger022 changed the title test(xvs-vault): invariant/fuzz suite, 0.5.x hack PoCs & live-fork verification test(xvs-vault): invariant/fuzz suite, PoCs & live-fork verification Jul 9, 2026
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.
@github-actions

Copy link
Copy Markdown

Code Coverage

Package Line Rate Branch Rate Health
contracts 100% 100%
contracts.Admin 88% 41%
contracts.Comptroller 100% 90%
contracts.Comptroller.Diamond 95% 64%
contracts.Comptroller.Diamond.facets 88% 74%
contracts.Comptroller.Diamond.interfaces 100% 100%
contracts.Comptroller.Types 100% 100%
contracts.Comptroller.legacy 100% 100%
contracts.Comptroller.legacy.Diamond 0% 0%
contracts.Comptroller.legacy.Diamond.facets 0% 0%
contracts.Comptroller.legacy.Diamond.interfaces 100% 100%
contracts.DelegateBorrowers 100% 89%
contracts.FlashLoan.interfaces 100% 100%
contracts.Governance 68% 45%
contracts.InterestRateModels 74% 59%
contracts.Lens 44% 47%
contracts.Liquidator 83% 60%
contracts.Oracle 100% 100%
contracts.PegStability 88% 84%
contracts.Swap 87% 57%
contracts.Swap.interfaces 100% 100%
contracts.Swap.lib 81% 53%
contracts.Tokens 100% 100%
contracts.Tokens.Prime 97% 81%
contracts.Tokens.Prime.Interfaces 100% 100%
contracts.Tokens.Prime.libs 90% 77%
contracts.Tokens.VAI 82% 52%
contracts.Tokens.VRT 20% 9%
contracts.Tokens.VTokens 71% 53%
contracts.Tokens.VTokens.legacy 0% 0%
contracts.Tokens.VTokens.legacy.Utils 0% 0%
contracts.Tokens.XVS 19% 8%
contracts.Tokens.test 100% 100%
contracts.Utils 52% 31%
contracts.VAIVault 50% 45%
contracts.VRTVault 49% 36%
contracts.XVSVault 63% 50%
contracts.external 100% 100%
contracts.lib 89% 71%
Summary 61% (4404 / 7203) 47% (1663 / 3556)

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