Skip to content

test: vUnits deviation accumulator round-trip invariant - #629

Open
AnubisQuantumCipher wants to merge 1 commit into
ssvlabs:mainfrom
AnubisQuantumCipher:feat/vunits-cycle-invariant-test
Open

test: vUnits deviation accumulator round-trip invariant#629
AnubisQuantumCipher wants to merge 1 commit into
ssvlabs:mainfrom
AnubisQuantumCipher:feat/vunits-cycle-invariant-test

Conversation

@AnubisQuantumCipher

Copy link
Copy Markdown

Summary

Adds test/sanity/vunits-cycle-roundtrip-invariant.test.ts — a single deterministic sanity test that asserts the deviation-only model's strong endpoint identity after a full cluster lifecycle cycle:

daoTotalEthVUnits == ethDaoValidatorCount * BPS_DENOMINATOR

The cycle exercised:

register
  -> updateClusterBalance(EB up to 64 ETH)
    -> liquidate
      -> reactivate
        -> updateClusterBalance(EB down to 32 ETH baseline)

After this sequence, every per-operator stored deviation must have netted to zero; otherwise ProtocolLib.networkTotalEarnings (contracts/libraries/ProtocolLib.sol:84-90) would over- or under-collect against the cluster owners' real obligations on subsequent updates.

Pin: branch main HEAD at the time of writing (9bb7b21d4432f34f623bed3e0bb3fa77f1e5d2b9).

Why a new sanity test?

The existing Echidna property echidna_vunits_deviation_consistent at test/echidna/SSVAccountingEchidna.sol:1023 asserts the weaker self-consistent identity:

daoTotalEthVUnits == sum over clusters of clusterEB.vUnits

That identity can mirror a corruption because both sides read from the same storage the SUT writes. The endpoint identity in this PR is stronger: it requires every per-operator deviation accumulator to have rolled back to zero across the cycle, which an internal-storage-recompute property cannot detect.

Two additional reasons this property is currently uncovered:

  1. updateClusterBalance is not in SSVAccountingEchidna's action surface (the 22 actions enumerated at the top of that contract do not include an action_update_cluster_balance_* variant), so the explicit-EB up/down sequence this invariant probes is structurally unreachable for the existing harness.
  2. No Echidna harness in test/echidna/ chains the full register -> EB up -> liquidate -> reactivate -> EB down cycle through a single stateful sequence. SSVEdgeCasesEchidna's echidna_reactivation_restores_vunits at :425 covers the reactivation step but not the full cycle and not the post-cycle DAO identity.

This test pins the deviation-only model's expected behavior to a runnable artifact and would catch any future regression that introduces drift on this exact cycle.

How it fits the repo

  • Uses ssvClustersHarnessFixture and the existing setupTestContext / registerAndParseCluster / mockEBAndUpdate / parseClusterFromEvent helpers — no new test infrastructure.
  • Lives under test/sanity/ next to the other regression-style sanity tests (ssv2-frozen-supply-quorum.test.ts, ssv3-stale-vunits-liquidation.test.ts, etc.).
  • Zero fees on both network and operator side so the orthogonal fee-index dimension does not cloud the deviation-only invariant being probed. Other tests can extend the same scaffold with non-zero fees.

Test plan

  • NO_GAS_ENFORCE=true npx hardhat test test/sanity/vunits-cycle-roundtrip-invariant.test.ts passes locally on main (1 passing, ~85 ms).
  • No new dependencies (uses Hardhat / chai / ethers / existing helpers).
  • Mirrors the conventions of test/unit/SSVClusters/ebAutoLiquidation.test.ts for the EB-update + liquidate flow and test/sanity/ssv3-stale-vunits-liquidation.test.ts for the sanity-test shape.
  • Reviewers: please run the suite locally and confirm no interaction with other sanity tests.

Provenance

This test was authored as part of an independent post-v2.0.0 audit pass; the property the test probes was derived from a symbolic walk-through of OperatorLib.updateSnapshotSt (contracts/libraries/OperatorLib.sol:52-72) and the _executeLiquidation flow (contracts/modules/SSVClusters.sol:555-616). No issue was found at 9bb7b21d — the invariant holds, this PR pins it.

Asserts the strong DAO accumulator identity
  daoTotalEthVUnits == ethDaoValidatorCount * BPS_DENOMINATOR
holds after the full lifecycle
  register -> updateClusterBalance(EB up) -> liquidate ->
  reactivate -> updateClusterBalance(EB down to baseline)
on a single cluster with zero fees (orthogonal to the vUnits-deviation
dimension being probed).

This is the deviation-only-model endpoint identity: the per-operator
deviation MUST net to zero across the cycle, otherwise
networkTotalEarnings (ProtocolLib.sol:84-90) under- or over-collects
against the cluster owners' real obligations.

Complements the existing Echidna property
echidna_vunits_deviation_consistent at SSVAccountingEchidna.sol:1023,
which asserts the weaker self-consistent identity
  daoTotalEthVUnits == sum(clusterEB.vUnits)
that can mirror a corruption since both sides read from the same storage
the SUT writes. This test asserts the stronger endpoint identity on a
deterministic state-machine sequence that the Echidna action_* surface
does not chain together (updateClusterBalance is not part of
SSVAccountingEchidna's action set, and no Echidna harness sequences a
full register -> EB up -> liquidate -> reactivate -> EB down cycle).

Pin: HEAD 9bb7b21.
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