diff --git a/.claude/skills/audit/SKILL.md b/.claude/skills/audit/SKILL.md deleted file mode 100644 index b95adf597..000000000 --- a/.claude/skills/audit/SKILL.md +++ /dev/null @@ -1,366 +0,0 @@ ---- -name: audit -description: "Run a standardized security and correctness audit on SSV Network contracts. Use when the user wants to audit a module, PR, branch diff, or the full codebase. Outputs findings in MAINNET-READINESS.md format." -argument-hint: "[scope: module name, pr number, 'full', or file path]" ---- - -# SSV Network — Contract Audit Skill - -Run a standardized audit against SSV Network v2.0.0 smart contracts. Dispatches parallel subtask workers to check spec compliance, security, accounting correctness, edge cases, test coverage, and code quality. - -## Scope Resolution - -Parse `$ARGUMENTS` to determine the audit scope: - -| Input | Scope | Files | -|-------|-------|-------| -| `clusters` | SSVClusters module | `contracts/modules/SSVClusters.sol`, `contracts/libraries/ClusterLib.sol` | -| `operators` | SSVOperators module | `contracts/modules/SSVOperators.sol`, `contracts/libraries/OperatorLib.sol`, `contracts/modules/SSVOperatorsWhitelist.sol` | -| `validators` | SSVValidators module | `contracts/modules/SSVValidators.sol`, `contracts/libraries/ValidatorLib.sol` | -| `staking` | SSVStaking module | `contracts/modules/SSVStaking.sol`, `contracts/token/CSSVToken.sol`, `contracts/libraries/storage/SSVStorageStaking.sol` | -| `dao` | SSVDAO module | `contracts/modules/SSVDAO.sol`, `contracts/libraries/ProtocolLib.sol` | -| `views` | SSVViews module | `contracts/modules/SSVViews.sol` | -| `pr ` | Pull request diff | Run `gh pr diff ` to get files | -| `full` | Full codebase | All `contracts/` files | -| `` | Specific file | The given file | - -If no argument provided, ask the user what to audit. - -## Execution - -Use `subtask` to dispatch **3 parallel workers**, each handling a different audit dimension. All workers must use `--base-branch` matching the current branch. - -**IMPORTANT:** Unset CLAUDECODE before running subtask commands: `unset CLAUDECODE && subtask ...` - -### Worker 1: Security & Spec Compliance - -```bash -unset CLAUDECODE && subtask draft audit/security-[SCOPE] --base-branch [BRANCH] --title "Security audit: [SCOPE]" <<'TASK' -You are performing a security and spec compliance audit on SSV Network v2.0.0. - -## Required Reading -1. `CLAUDE.md` — Architecture, storage pattern, security rules -2. `docs/SPEC.md` — Technical specification (source of truth) -3. `docs/FLOWS.md` — Contract flows with invariants -4. `ssv-review/Internal - [DIP-X] SSV Staking.txt` — DIP-X proposal (source of truth for requirements) - -## Scope -[SCOPE_FILES] - -## Checks - -### 1. Spec Compliance -- [ ] Every function matches its specification in SPEC.md -- [ ] Event signatures and parameters match SPEC.md -- [ ] Error conditions and reverts match SPEC.md -- [ ] State mutations match FLOWS.md -- [ ] Invariants from FLOWS.md hold after every state transition -- [ ] DIP-X requirements satisfied — use claim-by-claim comparison with verdicts: MATCH / PARTIAL / MISMATCH / GAP / EXTRA - -### 2. Memory/Storage Safety (CRITICAL — caught our worst bug) -- [ ] **Stale memory copy detection:** For each function that reads a struct into `memory`, check: does any subsequent call modify the same struct in `storage`? If so, does the memory copy get written back, overwriting the storage change? -- [ ] **Storage→memory→storage roundtrip audit:** List every `Type memory x = s.something; ...; s.something = x;` pattern. Verify no storage-modifying functions are called between the read and write-back. -- [ ] **Flag every explicit downcast** (`uint64()`, `uint128()`, `uint192()`) — is there overflow checking? - -### 3. Entity Lifecycle State Machine (caught multiple HIGH bugs) -- [ ] **Operator lifecycle:** Map full state machine (registered → active → fee-changing → removed). For each state, list which fields are non-zero/zero. Check every function that interacts with operators — does it detect the state correctly? -- [ ] **Cluster lifecycle:** Map (created → active → liquidated → reactivated → migrated). For each transition, verify what state is cleaned up and what persists. -- [ ] **"Removed" detection consistency:** Grep for EVERY check that determines if an operator/cluster is removed/dead. Verify ALL checks use the same condition. -- [ ] **State resurrection:** Can any function unintentionally make a dead entity appear alive? (e.g., setting a zeroed field back to non-zero) - -### 4. Double-Accounting Prevention (caught HIGH bug) -- [ ] **Resource cleanup tracing:** For every counter/balance cleaned up on lifecycle transitions (liquidation, removal, migration), trace ALL code paths that modify it. Verify no path assumes another hasn't run. -- [ ] **Sequential operation analysis:** For critical pairs (liquidate → remove validators, EB update → liquidate, register → EB update), trace state changes and verify no double-counting or double-subtraction. - -### 5. Reentrancy -- [ ] **Completeness audit:** List EVERY `external`/`public` function across ALL modules. For each: has `nonReentrant`? Makes external calls? Document justification for any missing guard. -- [ ] **Shared slot verification:** Verify all modules use the same reentrancy guard storage slot via `SSVStorageReentrancy`. - -### 6. Access Control -- [ ] Owner-only at proxy level (`onlyOwner` modifier on SSVNetwork.sol) -- [ ] Operator owner: `operator.checkOwner()` in every operator management function -- [ ] Cluster owner: keyed by `keccak256(owner, operatorIds)` -- [ ] Oracle-only: `oracleIdOf[msg.sender] != 0` in `commitRoot` -- [ ] cSSV-only: `msg.sender == CSSV_ADDRESS` in `onCSSVTransfer` - -### 7. Cross-Module State Dependencies -- [ ] **State dependency graph:** Identify storage variables read by one module and written by another. Any variable with cross-module read/write without synchronization? -- [ ] **Coupled state variables:** Identify pairs that must stay synchronized (e.g., `ethDaoBalance` ↔ `stakingEthPoolBalance`). Verify all mutating functions maintain the coupling. - -### 8. Accounting Correctness -- [ ] **Per-operation balance flow:** For each operation (deposit, withdraw, liquidate, reactivate, migrate, register, remove, claimEthRewards, withdrawOperatorEarnings), trace what increases/decreases `contract.balance` and each accounting bucket. Do both sides match? -- [ ] **Cross-pool isolation:** Can any code path cause ETH to flow from operator pool to staking pool or vice versa? -- [ ] **vUnit math:** ceiling for ETH→vUnits (`ebToVUnits`), floor for vUnits→ETH (`vUnitsToEB`), BPS_DENOMINATOR = 10_000 -- [ ] **Packed types:** non-divisible values revert with MaxPrecisionExceeded -- [ ] **Liquidation threshold:** vUnit-weighted burn rate correctly computed - -### 9. Accumulator Edge Analysis -- [ ] **Zero-supply state:** What happens when cSSV totalSupply is 0? Are rewards lost, deferred, or correctly handled? -- [ ] **Regression state:** Can `accEthPerShare` decrease? If so, what happens to users whose index is higher? -- [ ] **Dust analysis:** Maximum dust per operation? Where does it accumulate? Can it be recovered? -- [ ] **First-staker advantage:** Can the first staker after a gap capture undistributed rewards? - -### 10. Governance Parameter Validation -- [ ] **For every governance setter:** What is min/max valid value? Is there bounds validation? What breaks at 0 or max? -- [ ] **Single-block attack chains:** Can governance execute a dangerous sequence in one tx? (e.g., updateQuorumBps(0) → replaceOracle → commitRoot) -- [ ] **Timelock presence:** Which critical governance functions lack a timelock? - -### 11. UUPS Proxy Safety -- [ ] `_disableInitializers()` called in implementation constructor -- [ ] `_authorizeUpgrade()` is `onlyOwner` -- [ ] `reinitializer(N)` version correct for target chain (current: N=3) -- [ ] No storage slot collisions across 5 storage libraries (verify keccak256 strings are unique) -- [ ] Fallback function routes correctly to SSVViews -- [ ] `msg.sender` and `msg.value` preserved correctly through delegatecall -- [ ] No module uses `address(this)` expecting implementation address - -### 12. Merkle Tree Security -- [ ] Double-hash convention verified (prevents second preimage attack) -- [ ] Cross-cluster proof substitution impossible (leaf includes clusterID) -- [ ] Proof replay across root transitions blocked (staleness + monotonicity) -- [ ] Zero/empty leaf handling - -### 13. Oracle Security -- [ ] Vote weight consistency across voting window (totalStaked can change between votes) -- [ ] Oracle replacement mid-vote (pending votes from replaced oracle persist) -- [ ] Multi-root voting (same oracle, conflicting roots, same block) -- [ ] Quorum unreachability (100% quorum + integer division) -- [ ] Oracle liveness failure handling - -### 14. Flash Loan Resistance -- [ ] Can flash-loaned SSV affect oracle voting weight? (check cooldown enforcement) -- [ ] Can flash-loaned ETH manipulate cluster balance checks? -- [ ] Are governance-sensitive calculations resistant to same-block manipulation? - -### 15. ERC20 Interaction Safety -- [ ] SSV token confirmed as standard ERC20 (no callbacks, no fee-on-transfer) -- [ ] Return values checked on all token transfers -- [ ] `rescueERC20` correctly blocks SSV and cSSV - -### 16. Event Completeness -- [ ] Every state change emits a corresponding event -- [ ] No ambiguous event reuse (same event for semantically different operations) -- [ ] Events provide enough data for off-chain state reconstruction (oracle, liquidator bot) - -### 17. Guard Consistency -- [ ] For operations with parallel implementations (normal liquidation vs auto-liquidation, SSV vs ETH paths), compare conditions side by side. Flag any inconsistency. - -## Output Format - -Write findings to `ssv-review/planning/verified/audit-security-[SCOPE]-[DATE].md` - -Before reporting, check `ssv-review/planning/MAINNET-READINESS.md` — skip already-tracked items. - -Include a **Verified Safe** section documenting areas investigated and confirmed correct. - -For each NEW issue use this format: -### [NEW-N] Title -- **Type:** Critical Bug Fix / Security Hardening / etc. -- **Priority:** P0 / P1 / P2 -- **Status:** Open - -**Requirement:** -**Context:** -**Acceptance Criteria:** -- [ ] criterion - -**Agent Instructions:** -TASK -``` - -### Worker 2: Test Coverage & Edge Cases - -```bash -unset CLAUDECODE && subtask draft audit/tests-[SCOPE] --base-branch [BRANCH] --title "Test coverage audit: [SCOPE]" <<'TASK' -You are auditing test coverage for SSV Network v2.0.0. - -## Required Reading -1. `CLAUDE.md` — Test conventions, helpers, patterns -2. The scoped contract files: [SCOPE_FILES] -3. ALL test files related to this scope in `test/unit/`, `test/integration/`, `test/sanity/` -4. Test helpers: `test/helpers/contract-helpers.ts`, `test/common/constants.ts`, `test/common/errors.ts`, `test/common/events.ts` -5. Echidna tests if relevant: `test/echidna/` - -## Checks - -### 1. Test Coverage Mapping -- [ ] Read every test file for the scoped module -- [ ] List what IS tested (scenarios covered) -- [ ] List what is NOT tested (gaps) -- [ ] For gaps, classify: P0 (security), P1 (correctness), P2 (edge case) - -### 2. Systemic Blind Spot Detection (caught our worst test gaps) -- [ ] **Parameter coverage matrix:** For each test file, check: do tests use non-zero operator fees? Non-baseline EB? Multiple operators? Multiple validators? If ANY major parameter is always zero/default across ALL tests, flag as P0. -- [ ] **Fee path coverage:** Every function that settles fees must be tested with concrete non-zero fees and verified against manual calculation. -- [ ] **EB path coverage:** Every function that uses vUnits must be tested with non-baseline EB (e.g., EB=1000, vUnits=312500). - -### 3. Balance Delta Assertions -- [ ] Every function that transfers ETH or SSV must have a test checking `balance_before - balance_after == expected_amount`. -- [ ] Check contract.balance, not just user balance. -- [ ] Liquidation: verify liquidator receives correct residual. -- [ ] Operator withdrawal: verify exact ETH/SSV amount. -- [ ] Staking claims: verify exact reward payout. - -### 4. Test Quality Deep Checks -- [ ] **Mock fidelity:** Do mock contracts faithfully reproduce production behavior? Check MockCSSV has `onCSSVTransfer` callback. -- [ ] **Commented-out assertions:** Search for assertions inside `/* */` or after `//` — flag immediately as P0. -- [ ] **Echidna invariant correctness:** Read each property: (a) assertion direction correct? (b) no identical properties? (c) helper functions bug-free? -- [ ] **View function verification:** Do tests call view functions after state changes to verify state? -- [ ] **Revert testing:** Are reverts tested with exact custom error names, not just generic revert? - -### 5. Specific Missing Test Patterns -- [ ] **Full lifecycle test:** register → EB update → fee accrual → liquidate → reactivate → EB update → withdraw → operator withdraw — with concrete balance verification at each step. -- [ ] **Sequential operation tests:** liquidate then remove validators, EB update then withdraw, register then EB decrease. -- [ ] **Stress test:** 13 operators, max fee, 3000 validators, EB=2048, 5-year block advance — verify no overflow. -- [ ] **Cross-module E2E:** commitRoot → updateClusterBalance → fee recalculation with concrete verification. - -### 6. Edge Cases -- [ ] Zero values: 0 validators, 0 balance, 0 fees, 0 operators, 0 staked -- [ ] Max values: 13 operators, 3000 validators/operator, EB=2048 -- [ ] Boundaries: exact liquidation threshold, exact min/max EB, exact cooldown expiry -- [ ] Empty/removed: removed operators, liquidated clusters, 0 cSSV supply -- [ ] Ordering: does operation order matter? (register before deposit, migrate before add) -- [ ] Concurrency: shared operators, same-block operations, EB update + withdraw - -### 7. Write Specific Test Descriptions -For each gap found, write a concrete test description including: -- Test name: `it('should [behavior] when [condition]')` -- Setup: what state to create -- Action: what function to call with what params -- Assertions: what to check (specific values, not just "should work") - -## Output Format - -Write findings to `ssv-review/planning/verified/audit-tests-[SCOPE]-[DATE].md` - -Check `ssv-review/planning/MAINNET-READINESS.md` first — skip already-tracked items. - -Include a **Well-Covered Areas** section documenting what IS tested adequately. - -Use MAINNET-READINESS.md format: [NEW-N] with Type, Priority, Requirement, Context, Acceptance Criteria, Agent Instructions. -TASK -``` - -### Worker 3: Code Quality & Best Practices - -```bash -unset CLAUDECODE && subtask draft audit/quality-[SCOPE] --base-branch [BRANCH] --title "Code quality audit: [SCOPE]" <<'TASK' -You are auditing code quality and best practices for SSV Network v2.0.0. - -## Required Reading -1. `CLAUDE.md` — Code conventions, architecture -2. The scoped contract files: [SCOPE_FILES] -3. `ssv-review/Internal - [DIP-X] SSV Staking.txt` — DIP-X proposal - -## Checks - -### 1. Memory/Storage Patterns (CRITICAL — caught our worst bug) -- [ ] **Flag every `Type memory x = s.field; ...; s.field = x;` pattern** as potentially dangerous. Check if any storage-modifying function is called between read and write-back. -- [ ] **Flag every explicit downcast** (`uint64()`, `uint128()`, `uint192()`) — is there overflow risk? -- [ ] **Flag every `unchecked` block** — is the arithmetic truly safe? - -### 2. Dead Code -- [ ] Unused functions, events, errors, imports, structs -- [ ] Commented-out code (should be removed) -- [ ] TODO/FIXME/HACK comments - -### 3. Code Quality -- [ ] Naming: variables/functions match behavior -- [ ] Patterns: consistent with rest of codebase -- [ ] Duplication: repeated logic that should be shared -- [ ] Gas: redundant SLOADs, unnecessary memory copies, storage→memory→storage roundtrips -- [ ] NatSpec: public/external functions documented - -### 4. Guard Consistency -- [ ] For operations with parallel implementations (normal liquidation vs auto-liquidation, SSV vs ETH paths), compare conditions side by side. Flag any inconsistency. -- [ ] Check that all "is entity removed/dead?" checks use the same condition across all functions. - -### 5. Dead State Cleanup -- [ ] On operator removal: list every storage field. Is each cleared? If not, can it cause issues? -- [ ] On cluster liquidation: what state persists? Can it cause issues on reactivation? -- [ ] Pending operations (fee change requests, unstake requests) — cleaned up on entity removal? -- [ ] Whitelist state — cleaned up on operator removal? - -### 6. Backward Compatibility -- [ ] Event signature changes (breaks oracle: ValidatorAdded, ClusterLiquidated, etc.) -- [ ] Function signature changes (breaks SDK/webapp) -- [ ] Cluster struct changes (breaks everything) -- [ ] Check against oracle ABI dependencies - -### 7. DIP Compliance -- [ ] **Claim-by-claim comparison:** For each DIP section in scope, enumerate every claim. Verdict: MATCH / PARTIAL / MISMATCH / GAP / EXTRA. -- [ ] **Precision/packability validation:** Every DIP-specified numeric value — is it storable in the packed type? (divisible by ETH_DEDUCTED_DIGITS or DEDUCTED_DIGITS) -- [ ] **Check for EXTRA behavior:** Code does more than spec says — is it intentional and safe? - -### 8. Compiler & Dependency Safety -- [ ] Compiler version pinned (not floating `^`) -- [ ] Optimizer settings documented and appropriate -- [ ] OpenZeppelin version current, no known CVEs -- [ ] Import paths match package versions - -### 9. Deployment Script Validation -- [ ] Script function signatures match contract ABIs -- [ ] Constructor arguments correct for all contracts -- [ ] Initializer parameters complete (check quorumBps, defaultOracleIds, cooldownDuration) -- [ ] No hardcoded addresses that differ per chain -- [ ] Scripts don't import from test files - -### 10. Deployment Readiness -- [ ] Contract sizes under 24KB (which are close to limit?) -- [ ] Constructor args correct -- [ ] Initializer version correct (reinitializer(3)) -- [ ] Governance parameters match DIP-X spec - -## Output Format - -Write findings to `ssv-review/planning/verified/audit-quality-[SCOPE]-[DATE].md` - -Check `ssv-review/planning/MAINNET-READINESS.md` first — skip already-tracked items. - -Include a **Already Correct** section documenting areas verified as clean. - -Use MAINNET-READINESS.md format for new findings. -TASK -``` - -## After Workers Complete - -1. Read all 3 output files from `ssv-review/planning/verified/` -2. Present a summary to the user: - - Total new findings by severity - - Key highlights - - Items already tracked in MAINNET-READINESS.md (skipped) - - Verified-safe areas -3. Ask the user if they want to merge new findings into MAINNET-READINESS.md -4. If yes, dispatch a merge worker: - -```bash -unset CLAUDECODE && subtask draft merge/audit-[SCOPE] --base-branch [BRANCH] --title "Merge audit findings for [SCOPE]" <<'TASK' -Read the 3 audit output files: -- ssv-review/planning/verified/audit-security-[SCOPE]-[DATE].md -- ssv-review/planning/verified/audit-tests-[SCOPE]-[DATE].md -- ssv-review/planning/verified/audit-quality-[SCOPE]-[DATE].md - -Read the current: ssv-review/planning/MAINNET-READINESS.md - -For each NEW finding (not already in MAINNET-READINESS.md): -1. Assign a real ID (continue from highest existing: BUG-N, SEC-N, TEST-N, etc.) -2. Append to the correct Type section in MAINNET-READINESS.md -3. Add to the Priority Summary table - -Do NOT remove or rewrite existing items. Only ADD. -Commit the changes. -TASK -``` - -## PR Audit Variant - -When auditing a PR, get the diff first: -```bash -gh pr diff [NUMBER] --name-only -``` -Then use those files as the scope for all 3 workers. Also include: -```bash -gh pr view [NUMBER] --json title,body,commits -``` -as context in each worker's task description. diff --git a/.openzeppelin/goerli.json b/.openzeppelin/goerli.json deleted file mode 100644 index 6b2088288..000000000 --- a/.openzeppelin/goerli.json +++ /dev/null @@ -1,7804 +0,0 @@ -{ - "manifestVersion": "3.2", - "proxies": [ - { - "address": "0x3A23a7F455E853058d900f5dc86f1Bb1589b54F9", - "txHash": "0x9802d184ef00cd873e770ecabe377fb98e5fd7c422946c5d46a2866f42dc93d2", - "kind": "uups" - }, - { - "address": "0x4ddfE2966f7Cdfe1F7d4f7d48949b3AB16BCc6B5", - "txHash": "0x7f46d97d62a3f25fe62473d9839871949408b23a07bd4541b52eb4a03090523f", - "kind": "uups" - }, - { - "address": "0xAfdb141Dd99b5a101065f40e3D7636262dce65b3", - "txHash": "0xc7a324e98962c685088d1ad056d33c9a57f64770e9ee8d755dd1133552419c38", - "kind": "uups" - }, - { - "address": "0x8dB45282d7C4559fd093C26f677B3837a5598914", - "txHash": "0xb8a3be822bd5dda92d8a03a0ced6dbac1f60aedacf4970ef912a81e25cbc5bec", - "kind": "uups" - }, - { - "address": "0x78ccf8eD5A7324866B1F663938dc0923bd2Fa8Df", - "txHash": "0x4fda09cc41369a54f3b750f8f757f01721427f4f81c9f3a9924ef94d7f971e63", - "kind": "uups" - }, - { - "address": "0x4935780f792bBc06BBbA6933d900698F7E74a51a", - "txHash": "0x54836afb5d593fce4c5aa4ee36c1d6c34d1b6f379984301632e0c96d1ecb3d7e", - "kind": "uups" - }, - { - "address": "0x9883B43048697382e2d27436Dc3e7C5E44cd858C", - "txHash": "0x7eefc191d7b6fa26999d4d2d4db52c45315191d95be96a667453c55345a4a0e8", - "kind": "uups" - }, - { - "address": "0x4B133c68A084B8A88f72eDCd7944B69c8D545f03", - "txHash": "0x653a2a3ec82d5d44ca8a4f4a99cb5c7f3e65b5fa42927f6e9ae5637292142a46", - "kind": "uups" - }, - { - "address": "0x0d7F42f447Db3819b7Cd227F7b5a208C8672F29C", - "txHash": "0x77d062dbf0ab1c3f0ba58d34fe6030920e566affff2d0648677be1a413f1a44b", - "kind": "uups" - }, - { - "address": "0x15C59e2a9be515bD002be918738d43d2CC915601", - "txHash": "0x0a89a6ab4fffb4a19672ddc97915f2465672ea9993a475b5681ab7dd3f24c4e2", - "kind": "uups" - }, - { - "address": "0x55660cbfDcD33649062B6182E2ee0E4930CdCFa7", - "txHash": "0xacc63b265d8741b3636caf4dd4369688ef362b1bf5a2de398047a1fdea935240", - "kind": "uups" - }, - { - "address": "0xB7D5Aa053315c9902825CE9E30F3A9cfA148dc2a", - "txHash": "0x1842096c7db0a02b25d2efcdb18f0488bde07ca65e524c8e9dbadb0e85fa228a", - "kind": "uups" - }, - { - "address": "0x45B831727DC96035e6a2f77AAAcE4835195a54Af", - "txHash": "0x31d03fed514af9138a6fca1784aade113e22794485b80ed6c8a975879b0716b4", - "kind": "uups" - }, - { - "address": "0x6F47C9Dbe0e3a369aE0ccDFF982183881CcDfb42", - "txHash": "0xe3cfdd1b88ac3b1d654c7761e982907f799f765865375e964512e09742b88b52", - "kind": "uups" - }, - { - "address": "0x9d3F908cB3b132379A97b0E0f8171F0B42756E28", - "txHash": "0x340d08b660b8055728de7f10ef6bef6daf2537b45f8e65237ed95b2e1cd9ce5f", - "kind": "uups" - }, - { - "address": "0x5b10c20D163Ed06Fe80630935439010295AE4C3B", - "txHash": "0xa90d4a0e1be7a3c257fc7a76d58289bd7c15b025174c55f890bd96e3f05389d1", - "kind": "uups" - }, - { - "address": "0x13F6DDF7B84dF02Cad4d75c39602Dc2cb2a275E9", - "txHash": "0x0cd98b8410dddac987d2de8c221713ea8250938550e037a4ef1581251518c178", - "kind": "uups" - }, - { - "address": "0x17dbb473c152Ff977607d82BBE7Bc7B9597cEF22", - "txHash": "0x2777281a792d66e198ebede6d8d7b5fdf89dfcbc8fd60f5e933039e8a6c98ac0", - "kind": "uups" - }, - { - "address": "0x56EEd6e3a358EaaA8Bc9BABb3be9C30b450833e8", - "txHash": "0x68529993097f5e848600a8c94b7edd859cf95f7833f93daa27bf9f83bc38b31c", - "kind": "uups" - }, - { - "address": "0xB4f76eC1cF546BcB2b091d3316F159179Dfbc2d3", - "txHash": "0xaf564eecba8a6623c2f6c9225c9401fc0bab7e226562b7c79f71b01f999df78c", - "kind": "uups" - }, - { - "address": "0x5a03e2a7e3A63E403f4Bd08421c88B4726eCbfB7", - "txHash": "0xd8c70ccf52ac3c957f1c936f76db3c563ca3d362cdd557bcb938e7d3f939e82e", - "kind": "uups" - }, - { - "address": "0x807E241D3118fC8F231948C60aa42a4C606C2545", - "txHash": "0x92c5acb7a80fc7456f09cfa170be120b1f087685d1d199d4350b1cb59dbd08f1", - "kind": "uups" - }, - { - "address": "0xC3CD9A0aE89Fff83b71b58b6512D43F8a41f363D", - "txHash": "0x40fb3000b9aca259b09fd24a83e92d017885f42b7245b8ca804a39e9584282f6", - "kind": "uups" - }, - { - "address": "0xAE2C84c48272F5a1746150ef333D5E5B51F68763", - "txHash": "0x7aa677a741c4b779346c8b179b0f3f47007dd94d90c7073b47a826ba969b86a5", - "kind": "uups" - }, - { - "address": "0xd6b633304Db2DD59ce93753FA55076DA367e5b2c", - "txHash": "0x48e42568ba5cea62bddf9f00d348623677ed38fb083acd760f830f454f290500", - "kind": "uups" - }, - { - "address": "0xcDc4423E9ffa9542d4CdDf42a70859C84859d2A9", - "txHash": "0x964e728e77bd4afa121c93bfd55076c36a5de0b764214dbb9ee574fa1976a9ad", - "kind": "uups" - }, - { - "address": "0xFe35A31e57946E8aadd25158BdF303A36dEf3332", - "txHash": "0x73d4d1df08c7c3d95e8b34545aa55b9ceb7e7e07f7138cb524c7565c56b03e91", - "kind": "uups" - } - ], - "impls": { - "84c23f7724698de84eb813dbfda03172032dfda80fc9218f7edeef2aa8404809": { - "address": "0xC3f92f9F001De4Fe36f9aF7A093842d7fc1a8718", - "txHash": "0x7206af5ce75169aac83dcaf88d81a0744f58834e456cf984a89af704e1a57ea5", - "layout": { - "solcVersion": "0.8.16", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "lastOperatorId", - "offset": 0, - "slot": "251", - "type": "t_struct(Counter)1401_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:40" - }, - { - "label": "operators", - "offset": 0, - "slot": "252", - "type": "t_mapping(t_uint64,t_struct(Operator)1833_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:46" - }, - { - "label": "operatorFeeChangeRequests", - "offset": 0, - "slot": "253", - "type": "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)1840_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:47" - }, - { - "label": "clusters", - "offset": 0, - "slot": "254", - "type": "t_mapping(t_bytes32,t_bytes32)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:49" - }, - { - "label": "_validatorPKs", - "offset": 0, - "slot": "255", - "type": "t_mapping(t_bytes32,t_struct(Validator)1812_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:50" - }, - { - "label": "version", - "offset": 0, - "slot": "256", - "type": "t_bytes32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:52" - }, - { - "label": "validatorsPerOperatorLimit", - "offset": 0, - "slot": "257", - "type": "t_uint32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:54" - }, - { - "label": "declareOperatorFeePeriod", - "offset": 4, - "slot": "257", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:55" - }, - { - "label": "executeOperatorFeePeriod", - "offset": 12, - "slot": "257", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:56" - }, - { - "label": "operatorMaxFeeIncrease", - "offset": 20, - "slot": "257", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:57" - }, - { - "label": "minimumBlocksBeforeLiquidation", - "offset": 0, - "slot": "258", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:58" - }, - { - "label": "dao", - "offset": 0, - "slot": "259", - "type": "t_struct(DAO)1858_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:60" - }, - { - "label": "_token", - "offset": 0, - "slot": "260", - "type": "t_contract(IERC20)1395", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:61" - }, - { - "label": "network", - "offset": 0, - "slot": "261", - "type": "t_struct(Network)1865_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:62" - }, - { - "label": "__gap", - "offset": 0, - "slot": "262", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:66" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_bytes32": { - "label": "bytes32", - "numberOfBytes": "32" - }, - "t_contract(IERC20)1395": { - "label": "contract IERC20", - "numberOfBytes": "20" - }, - "t_mapping(t_bytes32,t_bytes32)": { - "label": "mapping(bytes32 => bytes32)", - "numberOfBytes": "32" - }, - "t_mapping(t_bytes32,t_struct(Validator)1812_storage)": { - "label": "mapping(bytes32 => struct ISSVNetworkCore.Validator)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(Operator)1833_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.Operator)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)1840_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.OperatorFeeChangeRequest)", - "numberOfBytes": "32" - }, - "t_struct(Counter)1401_storage": { - "label": "struct Counters.Counter", - "members": [ - { - "label": "_value", - "type": "t_uint256", - "offset": 0, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(DAO)1858_storage": { - "label": "struct ISSVNetworkCore.DAO", - "members": [ - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 0, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 4, - "slot": "0" - }, - { - "label": "block", - "type": "t_uint64", - "offset": 12, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Network)1865_storage": { - "label": "struct ISSVNetworkCore.Network", - "members": [ - { - "label": "networkFee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "networkFeeIndex", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "networkFeeIndexBlockNumber", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Operator)1833_storage": { - "label": "struct ISSVNetworkCore.Operator", - "members": [ - { - "label": "owner", - "type": "t_address", - "offset": 0, - "slot": "0" - }, - { - "label": "fee", - "type": "t_uint64", - "offset": 20, - "slot": "0" - }, - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 28, - "slot": "0" - }, - { - "label": "snapshot", - "type": "t_struct(Snapshot)1822_storage", - "offset": 0, - "slot": "1" - } - ], - "numberOfBytes": "64" - }, - "t_struct(OperatorFeeChangeRequest)1840_storage": { - "label": "struct ISSVNetworkCore.OperatorFeeChangeRequest", - "members": [ - { - "label": "fee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "approvalBeginTime", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "approvalEndTime", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Snapshot)1822_storage": { - "label": "struct ISSVNetworkCore.Snapshot", - "members": [ - { - "label": "block", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "index", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Validator)1812_storage": { - "label": "struct ISSVNetworkCore.Validator", - "members": [ - { - "label": "owner", - "type": "t_address", - "offset": 0, - "slot": "0" - }, - { - "label": "active", - "type": "t_bool", - "offset": 20, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint32": { - "label": "uint32", - "numberOfBytes": "4" - }, - "t_uint64": { - "label": "uint64", - "numberOfBytes": "8" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "660ed01ee43f3f2af646276a03204bac7cb226ab7013ef4ca1a2a5900bd9b6c2": { - "address": "0xE858F79E4220fC552522563aE2C55Be6f5d661f4", - "txHash": "0xbbedf6dd62602c37ad101e4575336fa1bc815365581c3d1dc0e79490c9da3b5b", - "layout": { - "solcVersion": "0.8.16", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "_ssvNetwork", - "offset": 0, - "slot": "251", - "type": "t_contract(SSVNetwork)5018", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:26" - }, - { - "label": "__gap", - "offset": 0, - "slot": "252", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:30" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_contract(SSVNetwork)5018": { - "label": "contract SSVNetwork", - "numberOfBytes": "20" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "1e164419e5042d71372eda74f4b4650521a5473bbd51b5c1e1cee4b82afe94b7": { - "address": "0xe7A57a7a489d884C30946573A61C173928e03F9B", - "txHash": "0x50f9eca193ce521eecbdd47b73cb5718767b23db6f85d93c363df8f0ed5b5deb", - "layout": { - "solcVersion": "0.8.16", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "lastOperatorId", - "offset": 0, - "slot": "251", - "type": "t_struct(Counter)1401_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:40" - }, - { - "label": "operators", - "offset": 0, - "slot": "252", - "type": "t_mapping(t_uint64,t_struct(Operator)1833_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:46" - }, - { - "label": "operatorFeeChangeRequests", - "offset": 0, - "slot": "253", - "type": "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)1840_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:47" - }, - { - "label": "clusters", - "offset": 0, - "slot": "254", - "type": "t_mapping(t_bytes32,t_bytes32)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:49" - }, - { - "label": "_validatorPKs", - "offset": 0, - "slot": "255", - "type": "t_mapping(t_bytes32,t_struct(Validator)1812_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:50" - }, - { - "label": "version", - "offset": 0, - "slot": "256", - "type": "t_bytes32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:52" - }, - { - "label": "validatorsPerOperatorLimit", - "offset": 0, - "slot": "257", - "type": "t_uint32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:54" - }, - { - "label": "declareOperatorFeePeriod", - "offset": 4, - "slot": "257", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:55" - }, - { - "label": "executeOperatorFeePeriod", - "offset": 12, - "slot": "257", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:56" - }, - { - "label": "operatorMaxFeeIncrease", - "offset": 20, - "slot": "257", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:57" - }, - { - "label": "minimumBlocksBeforeLiquidation", - "offset": 0, - "slot": "258", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:58" - }, - { - "label": "dao", - "offset": 0, - "slot": "259", - "type": "t_struct(DAO)1858_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:60" - }, - { - "label": "_token", - "offset": 0, - "slot": "260", - "type": "t_contract(IERC20)1395", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:61" - }, - { - "label": "network", - "offset": 0, - "slot": "261", - "type": "t_struct(Network)1865_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:62" - }, - { - "label": "__gap", - "offset": 0, - "slot": "262", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:66" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_bytes32": { - "label": "bytes32", - "numberOfBytes": "32" - }, - "t_contract(IERC20)1395": { - "label": "contract IERC20", - "numberOfBytes": "20" - }, - "t_mapping(t_bytes32,t_bytes32)": { - "label": "mapping(bytes32 => bytes32)", - "numberOfBytes": "32" - }, - "t_mapping(t_bytes32,t_struct(Validator)1812_storage)": { - "label": "mapping(bytes32 => struct ISSVNetworkCore.Validator)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(Operator)1833_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.Operator)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)1840_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.OperatorFeeChangeRequest)", - "numberOfBytes": "32" - }, - "t_struct(Counter)1401_storage": { - "label": "struct Counters.Counter", - "members": [ - { - "label": "_value", - "type": "t_uint256", - "offset": 0, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(DAO)1858_storage": { - "label": "struct ISSVNetworkCore.DAO", - "members": [ - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 0, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 4, - "slot": "0" - }, - { - "label": "block", - "type": "t_uint64", - "offset": 12, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Network)1865_storage": { - "label": "struct ISSVNetworkCore.Network", - "members": [ - { - "label": "networkFee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "networkFeeIndex", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "networkFeeIndexBlockNumber", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Operator)1833_storage": { - "label": "struct ISSVNetworkCore.Operator", - "members": [ - { - "label": "owner", - "type": "t_address", - "offset": 0, - "slot": "0" - }, - { - "label": "fee", - "type": "t_uint64", - "offset": 20, - "slot": "0" - }, - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 28, - "slot": "0" - }, - { - "label": "snapshot", - "type": "t_struct(Snapshot)1822_storage", - "offset": 0, - "slot": "1" - } - ], - "numberOfBytes": "64" - }, - "t_struct(OperatorFeeChangeRequest)1840_storage": { - "label": "struct ISSVNetworkCore.OperatorFeeChangeRequest", - "members": [ - { - "label": "fee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "approvalBeginTime", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "approvalEndTime", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Snapshot)1822_storage": { - "label": "struct ISSVNetworkCore.Snapshot", - "members": [ - { - "label": "block", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "index", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Validator)1812_storage": { - "label": "struct ISSVNetworkCore.Validator", - "members": [ - { - "label": "owner", - "type": "t_address", - "offset": 0, - "slot": "0" - }, - { - "label": "active", - "type": "t_bool", - "offset": 20, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint32": { - "label": "uint32", - "numberOfBytes": "4" - }, - "t_uint64": { - "label": "uint64", - "numberOfBytes": "8" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "c7f446c2126eeceebac6ff1bbb1cc583a527079acd7f8929332ade86b5852b55": { - "address": "0x2fe8da61509Fd14a1ac00ad589Ffd0Bf1145956D", - "txHash": "0x740ae16c8cf791324207db865b65aceb45ecacb2bcb75306c57fa410de574589", - "layout": { - "solcVersion": "0.8.16", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "lastOperatorId", - "offset": 0, - "slot": "251", - "type": "t_struct(Counter)1401_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:40" - }, - { - "label": "operators", - "offset": 0, - "slot": "252", - "type": "t_mapping(t_uint64,t_struct(Operator)1833_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:46" - }, - { - "label": "operatorFeeChangeRequests", - "offset": 0, - "slot": "253", - "type": "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)1840_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:47" - }, - { - "label": "clusters", - "offset": 0, - "slot": "254", - "type": "t_mapping(t_bytes32,t_bytes32)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:49" - }, - { - "label": "_validatorPKs", - "offset": 0, - "slot": "255", - "type": "t_mapping(t_bytes32,t_struct(Validator)1812_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:50" - }, - { - "label": "version", - "offset": 0, - "slot": "256", - "type": "t_bytes32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:52" - }, - { - "label": "validatorsPerOperatorLimit", - "offset": 0, - "slot": "257", - "type": "t_uint32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:54" - }, - { - "label": "declareOperatorFeePeriod", - "offset": 4, - "slot": "257", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:55" - }, - { - "label": "executeOperatorFeePeriod", - "offset": 12, - "slot": "257", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:56" - }, - { - "label": "operatorMaxFeeIncrease", - "offset": 20, - "slot": "257", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:57" - }, - { - "label": "minimumBlocksBeforeLiquidation", - "offset": 0, - "slot": "258", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:58" - }, - { - "label": "dao", - "offset": 0, - "slot": "259", - "type": "t_struct(DAO)1858_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:60" - }, - { - "label": "_token", - "offset": 0, - "slot": "260", - "type": "t_contract(IERC20)1395", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:61" - }, - { - "label": "network", - "offset": 0, - "slot": "261", - "type": "t_struct(Network)1865_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:62" - }, - { - "label": "__gap", - "offset": 0, - "slot": "262", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:66" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_bytes32": { - "label": "bytes32", - "numberOfBytes": "32" - }, - "t_contract(IERC20)1395": { - "label": "contract IERC20", - "numberOfBytes": "20" - }, - "t_mapping(t_bytes32,t_bytes32)": { - "label": "mapping(bytes32 => bytes32)", - "numberOfBytes": "32" - }, - "t_mapping(t_bytes32,t_struct(Validator)1812_storage)": { - "label": "mapping(bytes32 => struct ISSVNetworkCore.Validator)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(Operator)1833_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.Operator)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)1840_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.OperatorFeeChangeRequest)", - "numberOfBytes": "32" - }, - "t_struct(Counter)1401_storage": { - "label": "struct Counters.Counter", - "members": [ - { - "label": "_value", - "type": "t_uint256", - "offset": 0, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(DAO)1858_storage": { - "label": "struct ISSVNetworkCore.DAO", - "members": [ - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 0, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 4, - "slot": "0" - }, - { - "label": "block", - "type": "t_uint64", - "offset": 12, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Network)1865_storage": { - "label": "struct ISSVNetworkCore.Network", - "members": [ - { - "label": "networkFee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "networkFeeIndex", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "networkFeeIndexBlockNumber", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Operator)1833_storage": { - "label": "struct ISSVNetworkCore.Operator", - "members": [ - { - "label": "owner", - "type": "t_address", - "offset": 0, - "slot": "0" - }, - { - "label": "fee", - "type": "t_uint64", - "offset": 20, - "slot": "0" - }, - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 28, - "slot": "0" - }, - { - "label": "snapshot", - "type": "t_struct(Snapshot)1822_storage", - "offset": 0, - "slot": "1" - } - ], - "numberOfBytes": "64" - }, - "t_struct(OperatorFeeChangeRequest)1840_storage": { - "label": "struct ISSVNetworkCore.OperatorFeeChangeRequest", - "members": [ - { - "label": "fee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "approvalBeginTime", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "approvalEndTime", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Snapshot)1822_storage": { - "label": "struct ISSVNetworkCore.Snapshot", - "members": [ - { - "label": "block", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "index", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Validator)1812_storage": { - "label": "struct ISSVNetworkCore.Validator", - "members": [ - { - "label": "owner", - "type": "t_address", - "offset": 0, - "slot": "0" - }, - { - "label": "active", - "type": "t_bool", - "offset": 20, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint32": { - "label": "uint32", - "numberOfBytes": "4" - }, - "t_uint64": { - "label": "uint64", - "numberOfBytes": "8" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "c4480254e15501bebfa4ff3227ffafce858a7084193ac664b9626254f8e6b23a": { - "address": "0x6b2CA261957B4b2f795aEeF5A806EdCc6bE04eB9", - "txHash": "0xcd619ab625bebdd3436c5b1158d1aeb76fb5b834c137a336b35c51050d454a93", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "lastOperatorId", - "offset": 0, - "slot": "251", - "type": "t_struct(Counter)2148_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:39" - }, - { - "label": "operators", - "offset": 0, - "slot": "252", - "type": "t_mapping(t_uint64,t_struct(Operator)2612_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:45" - }, - { - "label": "operatorsWhitelist", - "offset": 0, - "slot": "253", - "type": "t_mapping(t_uint64,t_address)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:46" - }, - { - "label": "operatorFeeChangeRequests", - "offset": 0, - "slot": "254", - "type": "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)2619_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:47" - }, - { - "label": "clusters", - "offset": 0, - "slot": "255", - "type": "t_mapping(t_bytes32,t_bytes32)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:48" - }, - { - "label": "validatorPKs", - "offset": 0, - "slot": "256", - "type": "t_mapping(t_bytes32,t_struct(Validator)2591_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:49" - }, - { - "label": "version", - "offset": 0, - "slot": "257", - "type": "t_bytes32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:51" - }, - { - "label": "validatorsPerOperatorLimit", - "offset": 0, - "slot": "258", - "type": "t_uint32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:53" - }, - { - "label": "declareOperatorFeePeriod", - "offset": 4, - "slot": "258", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:54" - }, - { - "label": "executeOperatorFeePeriod", - "offset": 12, - "slot": "258", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:55" - }, - { - "label": "operatorMaxFeeIncrease", - "offset": 20, - "slot": "258", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:56" - }, - { - "label": "minimumBlocksBeforeLiquidation", - "offset": 0, - "slot": "259", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:57" - }, - { - "label": "minimumLiquidationCollateral", - "offset": 8, - "slot": "259", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:58" - }, - { - "label": "dao", - "offset": 0, - "slot": "260", - "type": "t_struct(DAO)2637_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:60" - }, - { - "label": "_token", - "offset": 0, - "slot": "261", - "type": "t_contract(IERC20)2095", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:61" - }, - { - "label": "network", - "offset": 0, - "slot": "262", - "type": "t_struct(Network)2644_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:62" - }, - { - "label": "__gap", - "offset": 0, - "slot": "263", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:66" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_bytes32": { - "label": "bytes32", - "numberOfBytes": "32" - }, - "t_contract(IERC20)2095": { - "label": "contract IERC20", - "numberOfBytes": "20" - }, - "t_mapping(t_bytes32,t_bytes32)": { - "label": "mapping(bytes32 => bytes32)", - "numberOfBytes": "32" - }, - "t_mapping(t_bytes32,t_struct(Validator)2591_storage)": { - "label": "mapping(bytes32 => struct ISSVNetworkCore.Validator)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_address)": { - "label": "mapping(uint64 => address)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(Operator)2612_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.Operator)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)2619_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.OperatorFeeChangeRequest)", - "numberOfBytes": "32" - }, - "t_struct(Counter)2148_storage": { - "label": "struct Counters.Counter", - "members": [ - { - "label": "_value", - "type": "t_uint256", - "offset": 0, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(DAO)2637_storage": { - "label": "struct ISSVNetworkCore.DAO", - "members": [ - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 0, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 4, - "slot": "0" - }, - { - "label": "block", - "type": "t_uint64", - "offset": 12, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Network)2644_storage": { - "label": "struct ISSVNetworkCore.Network", - "members": [ - { - "label": "networkFee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "networkFeeIndex", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "networkFeeIndexBlockNumber", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Operator)2612_storage": { - "label": "struct ISSVNetworkCore.Operator", - "members": [ - { - "label": "owner", - "type": "t_address", - "offset": 0, - "slot": "0" - }, - { - "label": "fee", - "type": "t_uint64", - "offset": 20, - "slot": "0" - }, - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 28, - "slot": "0" - }, - { - "label": "snapshot", - "type": "t_struct(Snapshot)2601_storage", - "offset": 0, - "slot": "1" - } - ], - "numberOfBytes": "64" - }, - "t_struct(OperatorFeeChangeRequest)2619_storage": { - "label": "struct ISSVNetworkCore.OperatorFeeChangeRequest", - "members": [ - { - "label": "fee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "approvalBeginTime", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "approvalEndTime", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Snapshot)2601_storage": { - "label": "struct ISSVNetworkCore.Snapshot", - "members": [ - { - "label": "block", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "index", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Validator)2591_storage": { - "label": "struct ISSVNetworkCore.Validator", - "members": [ - { - "label": "owner", - "type": "t_address", - "offset": 0, - "slot": "0" - }, - { - "label": "active", - "type": "t_bool", - "offset": 20, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint32": { - "label": "uint32", - "numberOfBytes": "4" - }, - "t_uint64": { - "label": "uint64", - "numberOfBytes": "8" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "106a1e0908460b53147eafb4015f9beae2025df135b0a046f54cfb62b99ab5c4": { - "address": "0x8383d719377047b1B8824CbB7f8ba7f24F12c715", - "txHash": "0x923fe64b46b47b4a91612bb457980cca97017d569c91ed2343ec7ad04cac8693", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "_ssvNetwork", - "offset": 0, - "slot": "251", - "type": "t_contract(SSVNetwork)5302", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:21" - }, - { - "label": "__gap", - "offset": 0, - "slot": "252", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:25" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_contract(SSVNetwork)5302": { - "label": "contract SSVNetwork", - "numberOfBytes": "20" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "2eb98242bc5110431e468ef0b6b4893fc5af474b8bfada3b85b4ffdbf6fbaf5c": { - "address": "0xDea29CF8d8769c0b015360636E07e5f9953F2dDd", - "txHash": "0xb03f69580afbd2d728030fc53fc391f071b75176e05876113a044ab9d74bf99f", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "lastOperatorId", - "offset": 0, - "slot": "251", - "type": "t_struct(Counter)1401_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:39" - }, - { - "label": "operators", - "offset": 0, - "slot": "252", - "type": "t_mapping(t_uint64,t_struct(Operator)1865_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:45" - }, - { - "label": "operatorsWhitelist", - "offset": 0, - "slot": "253", - "type": "t_mapping(t_uint64,t_address)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:46" - }, - { - "label": "operatorFeeChangeRequests", - "offset": 0, - "slot": "254", - "type": "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)1872_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:47" - }, - { - "label": "clusters", - "offset": 0, - "slot": "255", - "type": "t_mapping(t_bytes32,t_bytes32)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:48" - }, - { - "label": "validatorPKs", - "offset": 0, - "slot": "256", - "type": "t_mapping(t_bytes32,t_struct(Validator)1844_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:49" - }, - { - "label": "version", - "offset": 0, - "slot": "257", - "type": "t_bytes32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:51" - }, - { - "label": "validatorsPerOperatorLimit", - "offset": 0, - "slot": "258", - "type": "t_uint32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:53" - }, - { - "label": "declareOperatorFeePeriod", - "offset": 4, - "slot": "258", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:54" - }, - { - "label": "executeOperatorFeePeriod", - "offset": 12, - "slot": "258", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:55" - }, - { - "label": "operatorMaxFeeIncrease", - "offset": 20, - "slot": "258", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:56" - }, - { - "label": "minimumBlocksBeforeLiquidation", - "offset": 0, - "slot": "259", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:57" - }, - { - "label": "minimumLiquidationCollateral", - "offset": 8, - "slot": "259", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:58" - }, - { - "label": "dao", - "offset": 0, - "slot": "260", - "type": "t_struct(DAO)1890_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:60" - }, - { - "label": "_token", - "offset": 0, - "slot": "261", - "type": "t_contract(IERC20)1395", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:61" - }, - { - "label": "network", - "offset": 0, - "slot": "262", - "type": "t_struct(Network)1897_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:62" - }, - { - "label": "__gap", - "offset": 0, - "slot": "263", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:66" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_bytes32": { - "label": "bytes32", - "numberOfBytes": "32" - }, - "t_contract(IERC20)1395": { - "label": "contract IERC20", - "numberOfBytes": "20" - }, - "t_mapping(t_bytes32,t_bytes32)": { - "label": "mapping(bytes32 => bytes32)", - "numberOfBytes": "32" - }, - "t_mapping(t_bytes32,t_struct(Validator)1844_storage)": { - "label": "mapping(bytes32 => struct ISSVNetworkCore.Validator)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_address)": { - "label": "mapping(uint64 => address)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(Operator)1865_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.Operator)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)1872_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.OperatorFeeChangeRequest)", - "numberOfBytes": "32" - }, - "t_struct(Counter)1401_storage": { - "label": "struct Counters.Counter", - "members": [ - { - "label": "_value", - "type": "t_uint256", - "offset": 0, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(DAO)1890_storage": { - "label": "struct ISSVNetworkCore.DAO", - "members": [ - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 0, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 4, - "slot": "0" - }, - { - "label": "block", - "type": "t_uint64", - "offset": 12, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Network)1897_storage": { - "label": "struct ISSVNetworkCore.Network", - "members": [ - { - "label": "networkFee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "networkFeeIndex", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "networkFeeIndexBlockNumber", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Operator)1865_storage": { - "label": "struct ISSVNetworkCore.Operator", - "members": [ - { - "label": "owner", - "type": "t_address", - "offset": 0, - "slot": "0" - }, - { - "label": "fee", - "type": "t_uint64", - "offset": 20, - "slot": "0" - }, - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 28, - "slot": "0" - }, - { - "label": "snapshot", - "type": "t_struct(Snapshot)1854_storage", - "offset": 0, - "slot": "1" - } - ], - "numberOfBytes": "64" - }, - "t_struct(OperatorFeeChangeRequest)1872_storage": { - "label": "struct ISSVNetworkCore.OperatorFeeChangeRequest", - "members": [ - { - "label": "fee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "approvalBeginTime", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "approvalEndTime", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Snapshot)1854_storage": { - "label": "struct ISSVNetworkCore.Snapshot", - "members": [ - { - "label": "block", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "index", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Validator)1844_storage": { - "label": "struct ISSVNetworkCore.Validator", - "members": [ - { - "label": "owner", - "type": "t_address", - "offset": 0, - "slot": "0" - }, - { - "label": "active", - "type": "t_bool", - "offset": 20, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint32": { - "label": "uint32", - "numberOfBytes": "4" - }, - "t_uint64": { - "label": "uint64", - "numberOfBytes": "8" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "983c95fc77dd302eb14262414744b549e13786067160e21a04a61f5def161f3e": { - "address": "0xad77AFA0c42a2056AB310cfd1f13dd5CCE5cF584", - "txHash": "0x0709e6ac3b6fe7ce4b2e1ffaab2d00131d8626436748ad3a49b3551a99ea94b0", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "lastOperatorId", - "offset": 0, - "slot": "251", - "type": "t_struct(Counter)2148_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:39" - }, - { - "label": "operators", - "offset": 0, - "slot": "252", - "type": "t_mapping(t_uint64,t_struct(Operator)2612_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:45" - }, - { - "label": "operatorsWhitelist", - "offset": 0, - "slot": "253", - "type": "t_mapping(t_uint64,t_address)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:46" - }, - { - "label": "operatorFeeChangeRequests", - "offset": 0, - "slot": "254", - "type": "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)2619_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:47" - }, - { - "label": "clusters", - "offset": 0, - "slot": "255", - "type": "t_mapping(t_bytes32,t_bytes32)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:48" - }, - { - "label": "validatorPKs", - "offset": 0, - "slot": "256", - "type": "t_mapping(t_bytes32,t_struct(Validator)2591_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:49" - }, - { - "label": "version", - "offset": 0, - "slot": "257", - "type": "t_bytes32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:51" - }, - { - "label": "validatorsPerOperatorLimit", - "offset": 0, - "slot": "258", - "type": "t_uint32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:53" - }, - { - "label": "declareOperatorFeePeriod", - "offset": 4, - "slot": "258", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:54" - }, - { - "label": "executeOperatorFeePeriod", - "offset": 12, - "slot": "258", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:55" - }, - { - "label": "operatorMaxFeeIncrease", - "offset": 20, - "slot": "258", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:56" - }, - { - "label": "minimumBlocksBeforeLiquidation", - "offset": 0, - "slot": "259", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:57" - }, - { - "label": "minimumLiquidationCollateral", - "offset": 8, - "slot": "259", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:58" - }, - { - "label": "dao", - "offset": 0, - "slot": "260", - "type": "t_struct(DAO)2637_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:60" - }, - { - "label": "_token", - "offset": 0, - "slot": "261", - "type": "t_contract(IERC20)2095", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:61" - }, - { - "label": "network", - "offset": 0, - "slot": "262", - "type": "t_struct(Network)2644_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:62" - }, - { - "label": "__gap", - "offset": 0, - "slot": "263", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:66" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_bytes32": { - "label": "bytes32", - "numberOfBytes": "32" - }, - "t_contract(IERC20)2095": { - "label": "contract IERC20", - "numberOfBytes": "20" - }, - "t_mapping(t_bytes32,t_bytes32)": { - "label": "mapping(bytes32 => bytes32)", - "numberOfBytes": "32" - }, - "t_mapping(t_bytes32,t_struct(Validator)2591_storage)": { - "label": "mapping(bytes32 => struct ISSVNetworkCore.Validator)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_address)": { - "label": "mapping(uint64 => address)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(Operator)2612_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.Operator)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)2619_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.OperatorFeeChangeRequest)", - "numberOfBytes": "32" - }, - "t_struct(Counter)2148_storage": { - "label": "struct Counters.Counter", - "members": [ - { - "label": "_value", - "type": "t_uint256", - "offset": 0, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(DAO)2637_storage": { - "label": "struct ISSVNetworkCore.DAO", - "members": [ - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 0, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 4, - "slot": "0" - }, - { - "label": "block", - "type": "t_uint64", - "offset": 12, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Network)2644_storage": { - "label": "struct ISSVNetworkCore.Network", - "members": [ - { - "label": "networkFee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "networkFeeIndex", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "networkFeeIndexBlockNumber", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Operator)2612_storage": { - "label": "struct ISSVNetworkCore.Operator", - "members": [ - { - "label": "owner", - "type": "t_address", - "offset": 0, - "slot": "0" - }, - { - "label": "fee", - "type": "t_uint64", - "offset": 20, - "slot": "0" - }, - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 28, - "slot": "0" - }, - { - "label": "snapshot", - "type": "t_struct(Snapshot)2601_storage", - "offset": 0, - "slot": "1" - } - ], - "numberOfBytes": "64" - }, - "t_struct(OperatorFeeChangeRequest)2619_storage": { - "label": "struct ISSVNetworkCore.OperatorFeeChangeRequest", - "members": [ - { - "label": "fee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "approvalBeginTime", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "approvalEndTime", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Snapshot)2601_storage": { - "label": "struct ISSVNetworkCore.Snapshot", - "members": [ - { - "label": "block", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "index", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Validator)2591_storage": { - "label": "struct ISSVNetworkCore.Validator", - "members": [ - { - "label": "owner", - "type": "t_address", - "offset": 0, - "slot": "0" - }, - { - "label": "active", - "type": "t_bool", - "offset": 20, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint32": { - "label": "uint32", - "numberOfBytes": "4" - }, - "t_uint64": { - "label": "uint64", - "numberOfBytes": "8" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "ae20f2d8556f8a2bf11dfc6c88ce1515ad5a155729bfba9541b7140d324fb981": { - "address": "0xBB09D3d97f5AF7e96a782158aa0c55d3c5BAaC1F", - "txHash": "0x04fa9049c4a67eb8d6312379a5e8299c7d1a6e56a2a37a336d19b143d08383ba", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "ssvNetwork", - "offset": 0, - "slot": "201", - "type": "t_contract(ISSVNetwork)1935", - "contract": "RegisterAuth", - "src": "contracts/RegisterAuth.sol:22" - }, - { - "label": "authorization", - "offset": 0, - "slot": "202", - "type": "t_mapping(t_address,t_struct(Authorization)2215_storage)", - "contract": "RegisterAuth", - "src": "contracts/RegisterAuth.sol:24" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_contract(ISSVNetwork)1935": { - "label": "contract ISSVNetwork", - "numberOfBytes": "20" - }, - "t_mapping(t_address,t_struct(Authorization)2215_storage)": { - "label": "mapping(address => struct IRegisterAuth.Authorization)", - "numberOfBytes": "32" - }, - "t_struct(Authorization)2215_storage": { - "label": "struct IRegisterAuth.Authorization", - "members": [ - { - "label": "registerOperator", - "type": "t_bool", - "offset": 0, - "slot": "0" - }, - { - "label": "registerValidator", - "type": "t_bool", - "offset": 1, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "3e3e3c0dffab2beea5c1f587b838efbb3ef4872be93e0fe0f285a3e77f4812a6": { - "address": "0x746C33ccC28b1363c35c09baDAF41b2FFa7E6D56", - "txHash": "0x8841dc01dcfa20c49f931101540ffc8111ad46f8e1d78d4df4e0c9f50d58adb2", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "lastOperatorId", - "offset": 0, - "slot": "251", - "type": "t_struct(Counter)1408_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:41" - }, - { - "label": "operatorsPKs", - "offset": 0, - "slot": "252", - "type": "t_mapping(t_bytes32,t_uint64)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:47" - }, - { - "label": "operators", - "offset": 0, - "slot": "253", - "type": "t_mapping(t_uint64,t_struct(Operator)1963_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:48" - }, - { - "label": "operatorsWhitelist", - "offset": 0, - "slot": "254", - "type": "t_mapping(t_uint64,t_address)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:49" - }, - { - "label": "operatorFeeChangeRequests", - "offset": 0, - "slot": "255", - "type": "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)1970_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:50" - }, - { - "label": "clusters", - "offset": 0, - "slot": "256", - "type": "t_mapping(t_bytes32,t_bytes32)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:51" - }, - { - "label": "validatorPKs", - "offset": 0, - "slot": "257", - "type": "t_mapping(t_bytes32,t_struct(Validator)1942_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:52" - }, - { - "label": "version", - "offset": 0, - "slot": "258", - "type": "t_bytes32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:54" - }, - { - "label": "validatorsPerOperatorLimit", - "offset": 0, - "slot": "259", - "type": "t_uint32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:56" - }, - { - "label": "minimumBlocksBeforeLiquidation", - "offset": 4, - "slot": "259", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:58" - }, - { - "label": "minimumLiquidationCollateral", - "offset": 12, - "slot": "259", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:59" - }, - { - "label": "operatorFeeConfig", - "offset": 0, - "slot": "260", - "type": "t_struct(OperatorFeeConfig)1977_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:61" - }, - { - "label": "dao", - "offset": 0, - "slot": "261", - "type": "t_struct(DAO)1995_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:62" - }, - { - "label": "token", - "offset": 0, - "slot": "262", - "type": "t_contract(IERC20)1402", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:63" - }, - { - "label": "network", - "offset": 0, - "slot": "263", - "type": "t_struct(Network)2002_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:64" - }, - { - "label": "__gap", - "offset": 0, - "slot": "264", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:71" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_bytes32": { - "label": "bytes32", - "numberOfBytes": "32" - }, - "t_contract(IERC20)1402": { - "label": "contract IERC20", - "numberOfBytes": "20" - }, - "t_mapping(t_bytes32,t_bytes32)": { - "label": "mapping(bytes32 => bytes32)", - "numberOfBytes": "32" - }, - "t_mapping(t_bytes32,t_struct(Validator)1942_storage)": { - "label": "mapping(bytes32 => struct ISSVNetworkCore.Validator)", - "numberOfBytes": "32" - }, - "t_mapping(t_bytes32,t_uint64)": { - "label": "mapping(bytes32 => uint64)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_address)": { - "label": "mapping(uint64 => address)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(Operator)1963_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.Operator)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)1970_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.OperatorFeeChangeRequest)", - "numberOfBytes": "32" - }, - "t_struct(Counter)1408_storage": { - "label": "struct Counters.Counter", - "members": [ - { - "label": "_value", - "type": "t_uint256", - "offset": 0, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(DAO)1995_storage": { - "label": "struct ISSVNetworkCore.DAO", - "members": [ - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 0, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 4, - "slot": "0" - }, - { - "label": "block", - "type": "t_uint64", - "offset": 12, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Network)2002_storage": { - "label": "struct ISSVNetworkCore.Network", - "members": [ - { - "label": "networkFee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "networkFeeIndex", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "networkFeeIndexBlockNumber", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Operator)1963_storage": { - "label": "struct ISSVNetworkCore.Operator", - "members": [ - { - "label": "owner", - "type": "t_address", - "offset": 0, - "slot": "0" - }, - { - "label": "fee", - "type": "t_uint64", - "offset": 20, - "slot": "0" - }, - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 28, - "slot": "0" - }, - { - "label": "snapshot", - "type": "t_struct(Snapshot)1952_storage", - "offset": 0, - "slot": "1" - } - ], - "numberOfBytes": "64" - }, - "t_struct(OperatorFeeChangeRequest)1970_storage": { - "label": "struct ISSVNetworkCore.OperatorFeeChangeRequest", - "members": [ - { - "label": "fee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "approvalBeginTime", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "approvalEndTime", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(OperatorFeeConfig)1977_storage": { - "label": "struct ISSVNetworkCore.OperatorFeeConfig", - "members": [ - { - "label": "declareOperatorFeePeriod", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "executeOperatorFeePeriod", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "operatorMaxFeeIncrease", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Snapshot)1952_storage": { - "label": "struct ISSVNetworkCore.Snapshot", - "members": [ - { - "label": "block", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "index", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Validator)1942_storage": { - "label": "struct ISSVNetworkCore.Validator", - "members": [ - { - "label": "hashedOperatorIds", - "type": "t_bytes32", - "offset": 0, - "slot": "0" - }, - { - "label": "active", - "type": "t_bool", - "offset": 0, - "slot": "1" - } - ], - "numberOfBytes": "64" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint32": { - "label": "uint32", - "numberOfBytes": "4" - }, - "t_uint64": { - "label": "uint64", - "numberOfBytes": "8" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "1ccb2436e943c40bde1109243cc48355f47a96ed143c09aa5164f32ba5a8d6ac": { - "address": "0xE436092ce35Ad4bca28e210F38D48E6adf1A7bdd", - "txHash": "0xa3d0a364576c6f5e2c0e972c201cdfee3b2a76a630db53561cdc91ea6cdae7ba", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "_ssvNetwork", - "offset": 0, - "slot": "251", - "type": "t_contract(ISSVNetwork)1935", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:21" - }, - { - "label": "private__gap", - "offset": 0, - "slot": "252", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:25" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_contract(ISSVNetwork)1935": { - "label": "contract ISSVNetwork", - "numberOfBytes": "20" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "da4b22c408604c799c21f702c80cbc4b28a5e686332301a5ea685abfdbd59d12": { - "address": "0x2fD5091C7cCCE39c3cA8267BBf7F6e73e8aF3De3", - "txHash": "0xb63f4e40372c683d6fda6945db349afc64daaff7554903e6581ca27211ea0ad0", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "lastOperatorId", - "offset": 0, - "slot": "251", - "type": "t_struct(Counter)2155_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:41" - }, - { - "label": "operatorsPKs", - "offset": 0, - "slot": "252", - "type": "t_mapping(t_bytes32,t_uint64)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:47" - }, - { - "label": "operators", - "offset": 0, - "slot": "253", - "type": "t_mapping(t_uint64,t_struct(Operator)2710_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:48" - }, - { - "label": "operatorsWhitelist", - "offset": 0, - "slot": "254", - "type": "t_mapping(t_uint64,t_address)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:49" - }, - { - "label": "operatorFeeChangeRequests", - "offset": 0, - "slot": "255", - "type": "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)2717_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:50" - }, - { - "label": "clusters", - "offset": 0, - "slot": "256", - "type": "t_mapping(t_bytes32,t_bytes32)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:51" - }, - { - "label": "validatorPKs", - "offset": 0, - "slot": "257", - "type": "t_mapping(t_bytes32,t_struct(Validator)2689_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:52" - }, - { - "label": "version", - "offset": 0, - "slot": "258", - "type": "t_bytes32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:54" - }, - { - "label": "validatorsPerOperatorLimit", - "offset": 0, - "slot": "259", - "type": "t_uint32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:56" - }, - { - "label": "minimumBlocksBeforeLiquidation", - "offset": 4, - "slot": "259", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:58" - }, - { - "label": "minimumLiquidationCollateral", - "offset": 12, - "slot": "259", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:59" - }, - { - "label": "operatorFeeConfig", - "offset": 0, - "slot": "260", - "type": "t_struct(OperatorFeeConfig)2724_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:61" - }, - { - "label": "dao", - "offset": 0, - "slot": "261", - "type": "t_struct(DAO)2742_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:62" - }, - { - "label": "token", - "offset": 0, - "slot": "262", - "type": "t_contract(IERC20)2102", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:63" - }, - { - "label": "network", - "offset": 0, - "slot": "263", - "type": "t_struct(Network)2749_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:64" - }, - { - "label": "__gap", - "offset": 0, - "slot": "264", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:71" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_bytes32": { - "label": "bytes32", - "numberOfBytes": "32" - }, - "t_contract(IERC20)2102": { - "label": "contract IERC20", - "numberOfBytes": "20" - }, - "t_mapping(t_bytes32,t_bytes32)": { - "label": "mapping(bytes32 => bytes32)", - "numberOfBytes": "32" - }, - "t_mapping(t_bytes32,t_struct(Validator)2689_storage)": { - "label": "mapping(bytes32 => struct ISSVNetworkCore.Validator)", - "numberOfBytes": "32" - }, - "t_mapping(t_bytes32,t_uint64)": { - "label": "mapping(bytes32 => uint64)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_address)": { - "label": "mapping(uint64 => address)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(Operator)2710_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.Operator)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)2717_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.OperatorFeeChangeRequest)", - "numberOfBytes": "32" - }, - "t_struct(Counter)2155_storage": { - "label": "struct Counters.Counter", - "members": [ - { - "label": "_value", - "type": "t_uint256", - "offset": 0, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(DAO)2742_storage": { - "label": "struct ISSVNetworkCore.DAO", - "members": [ - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 0, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 4, - "slot": "0" - }, - { - "label": "block", - "type": "t_uint64", - "offset": 12, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Network)2749_storage": { - "label": "struct ISSVNetworkCore.Network", - "members": [ - { - "label": "networkFee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "networkFeeIndex", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "networkFeeIndexBlockNumber", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Operator)2710_storage": { - "label": "struct ISSVNetworkCore.Operator", - "members": [ - { - "label": "owner", - "type": "t_address", - "offset": 0, - "slot": "0" - }, - { - "label": "fee", - "type": "t_uint64", - "offset": 20, - "slot": "0" - }, - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 28, - "slot": "0" - }, - { - "label": "snapshot", - "type": "t_struct(Snapshot)2699_storage", - "offset": 0, - "slot": "1" - } - ], - "numberOfBytes": "64" - }, - "t_struct(OperatorFeeChangeRequest)2717_storage": { - "label": "struct ISSVNetworkCore.OperatorFeeChangeRequest", - "members": [ - { - "label": "fee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "approvalBeginTime", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "approvalEndTime", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(OperatorFeeConfig)2724_storage": { - "label": "struct ISSVNetworkCore.OperatorFeeConfig", - "members": [ - { - "label": "declareOperatorFeePeriod", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "executeOperatorFeePeriod", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "operatorMaxFeeIncrease", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Snapshot)2699_storage": { - "label": "struct ISSVNetworkCore.Snapshot", - "members": [ - { - "label": "block", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "index", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Validator)2689_storage": { - "label": "struct ISSVNetworkCore.Validator", - "members": [ - { - "label": "hashedOperatorIds", - "type": "t_bytes32", - "offset": 0, - "slot": "0" - }, - { - "label": "active", - "type": "t_bool", - "offset": 0, - "slot": "1" - } - ], - "numberOfBytes": "64" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint32": { - "label": "uint32", - "numberOfBytes": "4" - }, - "t_uint64": { - "label": "uint64", - "numberOfBytes": "8" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "1ee8c0149a057c048f20b833a01728a0e20412639ef24f718b562ddfb6a55a16": { - "address": "0x74d8aC7C183b38DF8f34C6b2c9048aaDcA4F1B8f", - "txHash": "0x75bb9b28c921f2774b5a503a25702f18ca98d4d3572e8345c6cb8e9fe6b3a942", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "lastOperatorId", - "offset": 0, - "slot": "251", - "type": "t_struct(Counter)2155_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:41" - }, - { - "label": "operatorsPKs", - "offset": 0, - "slot": "252", - "type": "t_mapping(t_bytes32,t_uint64)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:47" - }, - { - "label": "operators", - "offset": 0, - "slot": "253", - "type": "t_mapping(t_uint64,t_struct(Operator)2710_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:48" - }, - { - "label": "operatorsWhitelist", - "offset": 0, - "slot": "254", - "type": "t_mapping(t_uint64,t_address)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:49" - }, - { - "label": "operatorFeeChangeRequests", - "offset": 0, - "slot": "255", - "type": "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)2717_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:50" - }, - { - "label": "clusters", - "offset": 0, - "slot": "256", - "type": "t_mapping(t_bytes32,t_bytes32)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:51" - }, - { - "label": "validatorPKs", - "offset": 0, - "slot": "257", - "type": "t_mapping(t_bytes32,t_struct(Validator)2689_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:52" - }, - { - "label": "version", - "offset": 0, - "slot": "258", - "type": "t_bytes32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:54" - }, - { - "label": "validatorsPerOperatorLimit", - "offset": 0, - "slot": "259", - "type": "t_uint32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:56" - }, - { - "label": "minimumBlocksBeforeLiquidation", - "offset": 4, - "slot": "259", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:58" - }, - { - "label": "minimumLiquidationCollateral", - "offset": 12, - "slot": "259", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:59" - }, - { - "label": "operatorFeeConfig", - "offset": 0, - "slot": "260", - "type": "t_struct(OperatorFeeConfig)2724_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:61" - }, - { - "label": "dao", - "offset": 0, - "slot": "261", - "type": "t_struct(DAO)2742_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:62" - }, - { - "label": "token", - "offset": 0, - "slot": "262", - "type": "t_contract(IERC20)2102", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:63" - }, - { - "label": "network", - "offset": 0, - "slot": "263", - "type": "t_struct(Network)2749_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:64" - }, - { - "label": "__gap", - "offset": 0, - "slot": "264", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:71" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_bytes32": { - "label": "bytes32", - "numberOfBytes": "32" - }, - "t_contract(IERC20)2102": { - "label": "contract IERC20", - "numberOfBytes": "20" - }, - "t_mapping(t_bytes32,t_bytes32)": { - "label": "mapping(bytes32 => bytes32)", - "numberOfBytes": "32" - }, - "t_mapping(t_bytes32,t_struct(Validator)2689_storage)": { - "label": "mapping(bytes32 => struct ISSVNetworkCore.Validator)", - "numberOfBytes": "32" - }, - "t_mapping(t_bytes32,t_uint64)": { - "label": "mapping(bytes32 => uint64)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_address)": { - "label": "mapping(uint64 => address)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(Operator)2710_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.Operator)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)2717_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.OperatorFeeChangeRequest)", - "numberOfBytes": "32" - }, - "t_struct(Counter)2155_storage": { - "label": "struct Counters.Counter", - "members": [ - { - "label": "_value", - "type": "t_uint256", - "offset": 0, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(DAO)2742_storage": { - "label": "struct ISSVNetworkCore.DAO", - "members": [ - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 0, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 4, - "slot": "0" - }, - { - "label": "block", - "type": "t_uint64", - "offset": 12, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Network)2749_storage": { - "label": "struct ISSVNetworkCore.Network", - "members": [ - { - "label": "networkFee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "networkFeeIndex", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "networkFeeIndexBlockNumber", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Operator)2710_storage": { - "label": "struct ISSVNetworkCore.Operator", - "members": [ - { - "label": "owner", - "type": "t_address", - "offset": 0, - "slot": "0" - }, - { - "label": "fee", - "type": "t_uint64", - "offset": 20, - "slot": "0" - }, - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 28, - "slot": "0" - }, - { - "label": "snapshot", - "type": "t_struct(Snapshot)2699_storage", - "offset": 0, - "slot": "1" - } - ], - "numberOfBytes": "64" - }, - "t_struct(OperatorFeeChangeRequest)2717_storage": { - "label": "struct ISSVNetworkCore.OperatorFeeChangeRequest", - "members": [ - { - "label": "fee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "approvalBeginTime", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "approvalEndTime", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(OperatorFeeConfig)2724_storage": { - "label": "struct ISSVNetworkCore.OperatorFeeConfig", - "members": [ - { - "label": "declareOperatorFeePeriod", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "executeOperatorFeePeriod", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "operatorMaxFeeIncrease", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Snapshot)2699_storage": { - "label": "struct ISSVNetworkCore.Snapshot", - "members": [ - { - "label": "block", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "index", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Validator)2689_storage": { - "label": "struct ISSVNetworkCore.Validator", - "members": [ - { - "label": "hashedOperatorIds", - "type": "t_bytes32", - "offset": 0, - "slot": "0" - }, - { - "label": "active", - "type": "t_bool", - "offset": 0, - "slot": "1" - } - ], - "numberOfBytes": "64" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint32": { - "label": "uint32", - "numberOfBytes": "4" - }, - "t_uint64": { - "label": "uint64", - "numberOfBytes": "8" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "f7b3d32617efa532ec2ed3b21818c645afaa65acc745236500f0974b4b3d1d5e": { - "address": "0x703FcfeFF4cC45be17eA2Cfe90E7FD1b0d16BF23", - "txHash": "0x7b347767d3df89f0dc287be2650f1add0aaf8e49a8eacbae73628937cf50bdac", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "ssvNetwork", - "offset": 0, - "slot": "201", - "type": "t_contract(ISSVNetwork)2682", - "contract": "RegisterAuth", - "src": "contracts/RegisterAuth.sol:22" - }, - { - "label": "authorization", - "offset": 0, - "slot": "202", - "type": "t_mapping(t_address,t_struct(Authorization)2961_storage)", - "contract": "RegisterAuth", - "src": "contracts/RegisterAuth.sol:24" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_contract(ISSVNetwork)2682": { - "label": "contract ISSVNetwork", - "numberOfBytes": "20" - }, - "t_mapping(t_address,t_struct(Authorization)2961_storage)": { - "label": "mapping(address => struct IRegisterAuth.Authorization)", - "numberOfBytes": "32" - }, - "t_struct(Authorization)2961_storage": { - "label": "struct IRegisterAuth.Authorization", - "members": [ - { - "label": "registerOperator", - "type": "t_bool", - "offset": 0, - "slot": "0" - }, - { - "label": "registerValidator", - "type": "t_bool", - "offset": 1, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "65580310b4febb04a9c3e4042265ca50fcd8f9a33e23a1f904fcb65f9f1369cd": { - "address": "0x0a59b28a3D4d6F715eb42FB80D02c2474cBebCc7", - "txHash": "0x6d2b29a6e7b1cf3bd30c84eabf21e1dc937c650bab042d830dcaadf981693e93", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "lastOperatorId", - "offset": 0, - "slot": "251", - "type": "t_struct(Counter)2155_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:41" - }, - { - "label": "operatorsPKs", - "offset": 0, - "slot": "252", - "type": "t_mapping(t_bytes32,t_uint64)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:47" - }, - { - "label": "operators", - "offset": 0, - "slot": "253", - "type": "t_mapping(t_uint64,t_struct(Operator)2710_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:48" - }, - { - "label": "operatorsWhitelist", - "offset": 0, - "slot": "254", - "type": "t_mapping(t_uint64,t_address)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:49" - }, - { - "label": "operatorFeeChangeRequests", - "offset": 0, - "slot": "255", - "type": "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)2717_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:50" - }, - { - "label": "clusters", - "offset": 0, - "slot": "256", - "type": "t_mapping(t_bytes32,t_bytes32)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:51" - }, - { - "label": "validatorPKs", - "offset": 0, - "slot": "257", - "type": "t_mapping(t_bytes32,t_struct(Validator)2689_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:52" - }, - { - "label": "version", - "offset": 0, - "slot": "258", - "type": "t_bytes32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:54" - }, - { - "label": "validatorsPerOperatorLimit", - "offset": 0, - "slot": "259", - "type": "t_uint32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:56" - }, - { - "label": "minimumBlocksBeforeLiquidation", - "offset": 4, - "slot": "259", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:58" - }, - { - "label": "minimumLiquidationCollateral", - "offset": 12, - "slot": "259", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:59" - }, - { - "label": "operatorFeeConfig", - "offset": 0, - "slot": "260", - "type": "t_struct(OperatorFeeConfig)2724_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:61" - }, - { - "label": "dao", - "offset": 0, - "slot": "261", - "type": "t_struct(DAO)2742_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:62" - }, - { - "label": "token", - "offset": 0, - "slot": "262", - "type": "t_contract(IERC20)2102", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:63" - }, - { - "label": "network", - "offset": 0, - "slot": "263", - "type": "t_struct(Network)2749_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:64" - }, - { - "label": "__gap", - "offset": 0, - "slot": "264", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:71" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_bytes32": { - "label": "bytes32", - "numberOfBytes": "32" - }, - "t_contract(IERC20)2102": { - "label": "contract IERC20", - "numberOfBytes": "20" - }, - "t_mapping(t_bytes32,t_bytes32)": { - "label": "mapping(bytes32 => bytes32)", - "numberOfBytes": "32" - }, - "t_mapping(t_bytes32,t_struct(Validator)2689_storage)": { - "label": "mapping(bytes32 => struct ISSVNetworkCore.Validator)", - "numberOfBytes": "32" - }, - "t_mapping(t_bytes32,t_uint64)": { - "label": "mapping(bytes32 => uint64)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_address)": { - "label": "mapping(uint64 => address)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(Operator)2710_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.Operator)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)2717_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.OperatorFeeChangeRequest)", - "numberOfBytes": "32" - }, - "t_struct(Counter)2155_storage": { - "label": "struct Counters.Counter", - "members": [ - { - "label": "_value", - "type": "t_uint256", - "offset": 0, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(DAO)2742_storage": { - "label": "struct ISSVNetworkCore.DAO", - "members": [ - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 0, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 4, - "slot": "0" - }, - { - "label": "block", - "type": "t_uint64", - "offset": 12, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Network)2749_storage": { - "label": "struct ISSVNetworkCore.Network", - "members": [ - { - "label": "networkFee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "networkFeeIndex", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "networkFeeIndexBlockNumber", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Operator)2710_storage": { - "label": "struct ISSVNetworkCore.Operator", - "members": [ - { - "label": "owner", - "type": "t_address", - "offset": 0, - "slot": "0" - }, - { - "label": "fee", - "type": "t_uint64", - "offset": 20, - "slot": "0" - }, - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 28, - "slot": "0" - }, - { - "label": "snapshot", - "type": "t_struct(Snapshot)2699_storage", - "offset": 0, - "slot": "1" - } - ], - "numberOfBytes": "64" - }, - "t_struct(OperatorFeeChangeRequest)2717_storage": { - "label": "struct ISSVNetworkCore.OperatorFeeChangeRequest", - "members": [ - { - "label": "fee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "approvalBeginTime", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "approvalEndTime", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(OperatorFeeConfig)2724_storage": { - "label": "struct ISSVNetworkCore.OperatorFeeConfig", - "members": [ - { - "label": "declareOperatorFeePeriod", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "executeOperatorFeePeriod", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "operatorMaxFeeIncrease", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Snapshot)2699_storage": { - "label": "struct ISSVNetworkCore.Snapshot", - "members": [ - { - "label": "block", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "index", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Validator)2689_storage": { - "label": "struct ISSVNetworkCore.Validator", - "members": [ - { - "label": "hashedOperatorIds", - "type": "t_bytes32", - "offset": 0, - "slot": "0" - }, - { - "label": "active", - "type": "t_bool", - "offset": 0, - "slot": "1" - } - ], - "numberOfBytes": "64" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint32": { - "label": "uint32", - "numberOfBytes": "4" - }, - "t_uint64": { - "label": "uint64", - "numberOfBytes": "8" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "e98130a58d8e48a4d04e9bc712106b357518d6734da0f381d35a64479a761798": { - "address": "0xf05E0930eFD59CA23EFCbcA66b82Ed42cA3ADf73", - "txHash": "0x7465bda45bfddaddef837ae6cfe383506e8b9a80f24433f5a7ca2c6588975a5c", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "_ssvNetwork", - "offset": 0, - "slot": "251", - "type": "t_contract(ISSVNetwork)2682", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:21" - }, - { - "label": "private__gap", - "offset": 0, - "slot": "252", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:25" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_contract(ISSVNetwork)2682": { - "label": "contract ISSVNetwork", - "numberOfBytes": "20" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "1efc026838fe83210dbd8c56ac7e1f6759bc502143b5879da8be1d153a23d7dd": { - "address": "0x9b25f247C6d055C21c9a85a224CF32078523011a", - "txHash": "0xd9711d06798086fbd7da5017e3fff20632c421c8aa62d124649393d749bd314c", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "lastOperatorId", - "offset": 0, - "slot": "251", - "type": "t_struct(Counter)1408_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:41" - }, - { - "label": "operatorsPKs", - "offset": 0, - "slot": "252", - "type": "t_mapping(t_bytes32,t_uint64)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:47" - }, - { - "label": "operators", - "offset": 0, - "slot": "253", - "type": "t_mapping(t_uint64,t_struct(Operator)1963_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:48" - }, - { - "label": "operatorsWhitelist", - "offset": 0, - "slot": "254", - "type": "t_mapping(t_uint64,t_address)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:49" - }, - { - "label": "operatorFeeChangeRequests", - "offset": 0, - "slot": "255", - "type": "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)1970_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:50" - }, - { - "label": "clusters", - "offset": 0, - "slot": "256", - "type": "t_mapping(t_bytes32,t_bytes32)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:51" - }, - { - "label": "validatorPKs", - "offset": 0, - "slot": "257", - "type": "t_mapping(t_bytes32,t_struct(Validator)1942_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:52" - }, - { - "label": "version", - "offset": 0, - "slot": "258", - "type": "t_bytes32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:54" - }, - { - "label": "validatorsPerOperatorLimit", - "offset": 0, - "slot": "259", - "type": "t_uint32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:56" - }, - { - "label": "minimumBlocksBeforeLiquidation", - "offset": 4, - "slot": "259", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:58" - }, - { - "label": "minimumLiquidationCollateral", - "offset": 12, - "slot": "259", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:59" - }, - { - "label": "operatorFeeConfig", - "offset": 0, - "slot": "260", - "type": "t_struct(OperatorFeeConfig)1977_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:61" - }, - { - "label": "dao", - "offset": 0, - "slot": "261", - "type": "t_struct(DAO)1995_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:62" - }, - { - "label": "token", - "offset": 0, - "slot": "262", - "type": "t_contract(IERC20)1402", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:63" - }, - { - "label": "network", - "offset": 0, - "slot": "263", - "type": "t_struct(Network)2002_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:64" - }, - { - "label": "__gap", - "offset": 0, - "slot": "264", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:71" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_bytes32": { - "label": "bytes32", - "numberOfBytes": "32" - }, - "t_contract(IERC20)1402": { - "label": "contract IERC20", - "numberOfBytes": "20" - }, - "t_mapping(t_bytes32,t_bytes32)": { - "label": "mapping(bytes32 => bytes32)", - "numberOfBytes": "32" - }, - "t_mapping(t_bytes32,t_struct(Validator)1942_storage)": { - "label": "mapping(bytes32 => struct ISSVNetworkCore.Validator)", - "numberOfBytes": "32" - }, - "t_mapping(t_bytes32,t_uint64)": { - "label": "mapping(bytes32 => uint64)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_address)": { - "label": "mapping(uint64 => address)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(Operator)1963_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.Operator)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)1970_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.OperatorFeeChangeRequest)", - "numberOfBytes": "32" - }, - "t_struct(Counter)1408_storage": { - "label": "struct Counters.Counter", - "members": [ - { - "label": "_value", - "type": "t_uint256", - "offset": 0, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(DAO)1995_storage": { - "label": "struct ISSVNetworkCore.DAO", - "members": [ - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 0, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 4, - "slot": "0" - }, - { - "label": "block", - "type": "t_uint64", - "offset": 12, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Network)2002_storage": { - "label": "struct ISSVNetworkCore.Network", - "members": [ - { - "label": "networkFee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "networkFeeIndex", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "networkFeeIndexBlockNumber", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Operator)1963_storage": { - "label": "struct ISSVNetworkCore.Operator", - "members": [ - { - "label": "owner", - "type": "t_address", - "offset": 0, - "slot": "0" - }, - { - "label": "fee", - "type": "t_uint64", - "offset": 20, - "slot": "0" - }, - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 28, - "slot": "0" - }, - { - "label": "snapshot", - "type": "t_struct(Snapshot)1952_storage", - "offset": 0, - "slot": "1" - } - ], - "numberOfBytes": "64" - }, - "t_struct(OperatorFeeChangeRequest)1970_storage": { - "label": "struct ISSVNetworkCore.OperatorFeeChangeRequest", - "members": [ - { - "label": "fee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "approvalBeginTime", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "approvalEndTime", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(OperatorFeeConfig)1977_storage": { - "label": "struct ISSVNetworkCore.OperatorFeeConfig", - "members": [ - { - "label": "declareOperatorFeePeriod", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "executeOperatorFeePeriod", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "operatorMaxFeeIncrease", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Snapshot)1952_storage": { - "label": "struct ISSVNetworkCore.Snapshot", - "members": [ - { - "label": "block", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "index", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Validator)1942_storage": { - "label": "struct ISSVNetworkCore.Validator", - "members": [ - { - "label": "hashedOperatorIds", - "type": "t_bytes32", - "offset": 0, - "slot": "0" - }, - { - "label": "active", - "type": "t_bool", - "offset": 0, - "slot": "1" - } - ], - "numberOfBytes": "64" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint32": { - "label": "uint32", - "numberOfBytes": "4" - }, - "t_uint64": { - "label": "uint64", - "numberOfBytes": "8" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "0f9a95bf887a1d295574106829b58f2b931d31b43347c5d7ebf5c432f0d1cf82": { - "address": "0x0Eb002b608133f761A5496A7AD68fFb9a5ae70d1", - "txHash": "0x0e0e17e15a72f233157c5f4a4964a0b5a9ec755199150b9adbb8a58da1ccd5e4", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "07f0ad8d638b80e3e0c46a0ffab2878d79893dbc88ef70a1e05f76ae796932ca": { - "address": "0xC0DE4424F1C2B9BC9b80F19cC479b0adaD38Af44", - "txHash": "0x02b2a5d2c7d92c2fa5a82d1f5694c9225da8e51b7d16792546fbb344c0c167ef", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "ssvNetwork", - "offset": 0, - "slot": "251", - "type": "t_contract(IFnSSVViews)3530", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:19" - }, - { - "label": "private__gap", - "offset": 0, - "slot": "252", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:23" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_contract(IFnSSVViews)3530": { - "label": "contract IFnSSVViews", - "numberOfBytes": "20" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "064c555064aab91647555e9d6781b7254cafff19ce48d4c940fb3dbec372bff0": { - "address": "0xe31b3B1455CCe05030feB1d43a53fB49a5448C30", - "txHash": "0xd91d392a1368afa223ae9947b12551893424425f0d0261baf7f37418a023d81f", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "54c5f42e38cd6e80926108ca0d604abbc840af9bd2d1006bfc27edbf9ab86b0c": { - "address": "0xaaA153ed386F353B793c20868C4F0179B4Ad7604", - "txHash": "0x31682999189ec4b2b2495e6350e97fca5bffeb0a035aab8363abc818b9fa8892", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "06c55e88f7f16ecefd8ce649742571d8712c1164ba0a39a3f273716195d24be1": { - "address": "0xc7fCFeEc5FB9962bDC2234A7a25dCec739e27f9f", - "txHash": "0x59707232c74530e4af16199656dda3dc63b9dfe5a5232fba421daa6cf185a2f3", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "ssvNetwork", - "offset": 0, - "slot": "251", - "type": "t_contract(ISSVViews)3521", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:19" - }, - { - "label": "private__gap", - "offset": 0, - "slot": "252", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:23" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_contract(ISSVViews)3521": { - "label": "contract ISSVViews", - "numberOfBytes": "20" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "5d5e858f3c695d1a8c3e7e337239b4cbff1683e92f0839c61c0024f83661cd8b": { - "address": "0xc108c97aD33e10A5A3c87aE686eDb2a7d7c2f45C", - "txHash": "0x27717075d4d11ff649e2cd6523d4afdf9646c007249d4d7a787bdb6efc60807b", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "e6676711f192bb030762553ef7a425b58ee1593a0fc019a3fa85de4b34586bfa": { - "address": "0xE1914816A5AA165A9D828b822Fd4Aa068e1669f8", - "txHash": "0x275ce6a384977642576396837c3c2c3adb78a6a44ad764b06693c7d68c0e6d33", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "d3dca384c0bca6f3341cfc9d6d0fbcdecb40c156c3132b6887361c512f1b8f82": { - "address": "0xA772Ad9BD8b8F5e482dFB4225eDB80a450C0D66A", - "txHash": "0xcc32549ed0dbfaf8da2f084f8b998a9bf994943a39e84a27764205d920c93415", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "4520774aa64c18c9ddf1ef74c95a2ccf9bb21a0078a21ea1067552fdbd846048": { - "address": "0x74C82BD3F46Ab4F2A98635eaEa1f84E1BA5BE98c", - "txHash": "0x8d83beee85106b30f181eaa1abe432a4d75230e10965422d43bb8678f2cbdba9", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "ssvNetwork", - "offset": 0, - "slot": "251", - "type": "t_contract(ISSVViews)4181", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:19" - }, - { - "label": "private__gap", - "offset": 0, - "slot": "252", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:23" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_contract(ISSVViews)4181": { - "label": "contract ISSVViews", - "numberOfBytes": "20" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "3f9800ada4d2a9cecd5dbe695c48130fa7bc0dcdc84b9b238164a16c6a2802fe": { - "address": "0xEcd38CFb1c04C73AEa71350c742dD2A6613861C8", - "txHash": "0x6926c384301d3c540df529e49f0e253be4e178eaba77e9b46d99bc717b990313", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "0b47d2abd2279e76837ff94805e14a88d019593eed05755759cc40256af16f0e": { - "address": "0x296C821446f8756A6d30784C6CF63B65c2B82863", - "txHash": "0xc449b9dd299fe868cc5e284843897df96fb79db91b313450c96ee7e6c3b80a69", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "368acb5d5852996f01c66abefb8ca54bce00f1977c8aaed5efcf0c75f250303d": { - "address": "0xE20E557C5173D505a58eEBf3C4E6aD2672c57Fd1", - "txHash": "0xcde725501175a0ce2045f30df0fc6b3ed3fed0f2baab220f25163ec7c4f93933", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "ssvNetwork", - "offset": 0, - "slot": "251", - "type": "t_contract(ISSVViews)4189", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:19" - }, - { - "label": "__gap", - "offset": 0, - "slot": "252", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:23" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_contract(ISSVViews)4189": { - "label": "contract ISSVViews", - "numberOfBytes": "20" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "1e550124c6e28f236ee8632b9abc9a68dac2f537aff421981b339520c87ad539": { - "address": "0x0097bBea812414d42D2AD6d76c7da1c794AA16A9", - "txHash": "0xf3a3e1c1742cd1d11b7271abf2768c6046122eb06a2f66971a931021b25763c5", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "d876760172e5575262f8fe267daaa9f5045a6c995da21c74c61e769c00d0cb79": { - "address": "0x5fBf3Fe05112DE129Bf26dB70B03630Bc9A7233a", - "txHash": "0xa7cdfbbb6c6085cf5a574d3c1a93fbbb270987c566a33bcaa9f305d29347b2ea", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "c319dae004e883cdd24ab7834715e77958c6beccd0f2cea7e46e804b8f89f295": { - "address": "0xFA2e88093a4Ad20E204290f6169410CcF96e8858", - "txHash": "0xa476d771b2ad2793f613e84194fbfd393e61d6641371592337cadddbca813245", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "8a3382629da06790720d2d37ed76d51b1f949d6c3b17919f08f3b6842b9de108": { - "address": "0x7450a96d73E070210a52ceC327029F52fd156043", - "txHash": "0x9b95840ebe867c296beabe54ce9e7421ea9eb720ee26234d9e8418479cd7903d", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "ssvNetwork", - "offset": 0, - "slot": "251", - "type": "t_contract(ISSVViews)4293", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:19" - }, - { - "label": "__gap", - "offset": 0, - "slot": "252", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:23" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_contract(ISSVViews)4293": { - "label": "contract ISSVViews", - "numberOfBytes": "20" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "e311afb1f25419f9f90569ca2bf47a87990372364d587ec21789bb9aa6e83966": { - "address": "0xc25ad8Ebf84E08797b3076bb861C35056D3728e9", - "txHash": "0x7c1551d856363b706c688ef21bb39ab5cf30154806d2198d9a3666af45e40b7c", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - }, - "namespaces": {} - } - }, - "ea22005a44aa654c8170296ef0052ed50bdb1eb52b46b0b5a9aefc6275bf48c0": { - "address": "0x5888116f5863CA1A311F51ab79a03fBd34Ac6487", - "txHash": "0x1754042f1c35ae05c7ea6c1090e2af0bd38dcf99ce86374720222ed1c66b8fba", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - }, - "namespaces": {} - } - } - } -} diff --git a/.openzeppelin/holesky.json b/.openzeppelin/holesky.json deleted file mode 100644 index fe420fd3e..000000000 --- a/.openzeppelin/holesky.json +++ /dev/null @@ -1,1762 +0,0 @@ -{ - "manifestVersion": "3.2", - "proxies": [ - { - "address": "0x0d33801785340072C452b994496B19f196b7eE15", - "txHash": "0x9c6605dab4a2d23e480bf90f476b34498bd6d7a4e3c232835544bd8c94971427", - "kind": "uups" - }, - { - "address": "0x656d5cC4e7d49EaCC063cBB8D3e072F2841D68b4", - "txHash": "0x05fec282860f01d95aa19fea01907f0e377d529685c33da59dd0b7c85f96b0b2", - "kind": "uups" - }, - { - "address": "0x38A4794cCEd47d3baf7370CcC43B560D3a1beEFA", - "txHash": "0x998c38ff37b47e69e23c21a8079168b7e0e0ade7244781587b00be3f08a725c6", - "kind": "uups" - }, - { - "address": "0x352A18AEe90cdcd825d1E37d9939dCA86C00e281", - "txHash": "0xf36e0e114031e961a1e3452477ed71658cf0f809be94832b4dc4a99a293ef664", - "kind": "uups" - }, - { - "address": "0x4fA60408D9c0428b43FCa0E26c2f9aAa510cCeE2", - "txHash": "0x72d9a2c0e2442046a87816203f08b0ad4cb65ef25de40c59c5fe4f83b8834370", - "kind": "uups" - }, - { - "address": "0xEa0Fd295Be44Fb909d654dA90198c8E9d766FB74", - "txHash": "0x8cbbb4f8c3fa01e88d0aed6ffcccab3c063d332bf465541a4515fe3070177687", - "kind": "uups" - }, - { - "address": "0x4404f2EBBFc2Ab622C161fA8531404C68606260a", - "txHash": "0xcebce7af4e88fed3dab9c20188ed72b9165e48f12ff70b1ba9aa14e26441967d", - "kind": "uups" - }, - { - "address": "0xAd76Ff4a0931ce5F856044507A0400bA4eA59FB3", - "txHash": "0x53e06d337a8a9f8f000b5b9c9ec552ebc8751d6b69331297ff85d4c7b41de8d3", - "kind": "uups" - }, - { - "address": "0xC9A1594b2F8d48b1e8e84ffbB1448Ebcde00c154", - "txHash": "0x88bd899c0f8ff164525289cccf2fd60c4125dec28bd22fd6aa4f61a1616370e9", - "kind": "uups" - } - ], - "impls": { - "d876760172e5575262f8fe267daaa9f5045a6c995da21c74c61e769c00d0cb79": { - "address": "0x116522F4D00b42Efce0aA77f7ddAd1d27705F36b", - "txHash": "0xd04d7ebb1f3211c5006d965f1c3762c866e8eb77027428ab8989904b4af28a16", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "368acb5d5852996f01c66abefb8ca54bce00f1977c8aaed5efcf0c75f250303d": { - "address": "0xa9d0096bdbf97401F1B5E8D5330Ee8b7f0cb975D", - "txHash": "0x6fbd987b485c9f50a953b1ad38de6bcca366fbe9f9cb1ae88d81aa9b085fb3c6", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "ssvNetwork", - "offset": 0, - "slot": "251", - "type": "t_contract(ISSVViews)2185", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:19" - }, - { - "label": "private__gap", - "offset": 0, - "slot": "252", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:23" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_contract(ISSVViews)2185": { - "label": "contract ISSVViews", - "numberOfBytes": "20" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "6cbab0be5caec362fa591f99e9ab60ab2ec706c3cb7d8437dff9b7e9a204232e": { - "address": "0x58EabC62cC2c254AC43E35Edbb0D1f74f3DAd508", - "txHash": "0xd28b5d89a5f212ac291259398cbc79d035ef497bf8f42099edb229e8800aae6e", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "c319dae004e883cdd24ab7834715e77958c6beccd0f2cea7e46e804b8f89f295": { - "address": "0xE74C70Ea8A688de29d3b1F631b1FF8decAd52833", - "txHash": "0x5500f4fcf7c7487ea6c16c5f11fb7e81abc28a6dc0e33207ab5a15e696287aef", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "8a3382629da06790720d2d37ed76d51b1f949d6c3b17919f08f3b6842b9de108": { - "address": "0x7118C9B049E834B2351C1c9a0ECEE12610A1a29E", - "txHash": "0x1f189d0a9a3a88acfeefcfd6bec1309f969d6249550cac23396bfff6e4e39d17", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "ssvNetwork", - "offset": 0, - "slot": "251", - "type": "t_contract(ISSVViews)4293", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:19" - }, - { - "label": "__gap", - "offset": 0, - "slot": "252", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:23" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_contract(ISSVViews)4293": { - "label": "contract ISSVViews", - "numberOfBytes": "20" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "141551db18457f12435a89572b53feb0543d6d3f184915332fb095bfb8164fdf": { - "address": "0x21A40764aFEb2DA98eEB95Ce720212A15F87c5d7", - "txHash": "0x5433d717bd39e46df0e0345050bf5a0a7f136f96289b6d20b6f9b40b51dcab90", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - }, - "namespaces": {} - } - }, - "e311afb1f25419f9f90569ca2bf47a87990372364d587ec21789bb9aa6e83966": { - "address": "0x249e2769bF15082e1e44D15D819c0230d4500d54", - "txHash": "0x61e70aa81e1c355fdfd3a0554a42043651d7a032435c3a87d06de6f67854ddf8", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - }, - "namespaces": {} - } - }, - "ea22005a44aa654c8170296ef0052ed50bdb1eb52b46b0b5a9aefc6275bf48c0": { - "address": "0x990B226E8D74B42414F1296CCf2d8BA454879621", - "txHash": "0xfdde435768aa2ea9fe64206e700dfb6eac13345e5d733beca1a723e59a578683", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - }, - "namespaces": {} - } - }, - "cace716c99b7c3331ef14bea41153e9b79ee38508d038ed764ebfa2f3addf8d6": { - "address": "0xEFccE07BeC6418e32e72a28aFf0cdf442AEc14ea", - "txHash": "0x3da46b8992ed89a293c740d4e6a724eb0f24fc7b8fd2257bcd8af434ca1f956c", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - }, - "namespaces": {} - } - }, - "39c6606e3e0309dc1e40c1da00f5f6fea1a7f681100fa39bffeab8a902d3e822": { - "address": "0xc4267540782292Bb143d1ac4791a870174F76B26", - "txHash": "0x6182c8cadd407d87fbef290d92f0e026e1a3203e00a2806fa8dad7df075060b6", - "layout": { - "solcVersion": "0.8.24", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:40" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:21" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - }, - "namespaces": {} - } - }, - "d7d016016e8901e15ef80cd31dabc235c6db346324f22178ef7f0075e8481236": { - "address": "0x2fd5fd777bB818bf10A7ab803A9c3ae510E06Ea2", - "txHash": "0x2e05685de9deb43f83ba2b427f8eb31c7ef675d0aff896b4f9c86136c6d0b46e", - "layout": { - "solcVersion": "0.8.24", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:40" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:21" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "ssvNetwork", - "offset": 0, - "slot": "251", - "type": "t_contract(ISSVViews)4946", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:19" - }, - { - "label": "__gap", - "offset": 0, - "slot": "252", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:23" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_contract(ISSVViews)4946": { - "label": "contract ISSVViews", - "numberOfBytes": "20" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - }, - "namespaces": {} - } - }, - "a3d9e24ff7a910e9c585a007869df7fef9bed3087299d4534d453e5cd2bcfb5a": { - "address": "0x9Fe9ae58ABe43271313E87DCEAECB2780bE6E2c1", - "txHash": "0xcc870a141b68547d2fa9e71df10f273359e4509dd8948c958e6096cf1e5f2392", - "layout": { - "solcVersion": "0.8.24", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:40" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:21" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - }, - "namespaces": {} - } - }, - "7e9059cf74010ecbc029661757fcbec0cf7eee8077142bd25354cb83a7152fa8": { - "address": "0x8A8543f0323Fdf9c67Cb5d10B869F565FA737177", - "txHash": "0xf1a132ce867fb1ede636f649cef559d29668cbe1c44af46d4abfe491fb37c26f", - "layout": { - "solcVersion": "0.8.24", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:40" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:21" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - }, - "namespaces": {} - } - }, - "da06f711e239a6b927747cdeefb5954a754f3a4e01a17212488672f6689ac6f1": { - "address": "0x753B24E62c90B468Bd410b552555717552eC56Ff", - "txHash": "0xce31a279871c200cba53c2726dcbfc4493e4b86ffdd31862713323f9cf60a4cb", - "layout": { - "solcVersion": "0.8.24", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:40" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:21" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "ssvNetwork", - "offset": 0, - "slot": "251", - "type": "t_contract(ISSVViews)4950", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:19" - }, - { - "label": "__gap", - "offset": 0, - "slot": "252", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:23" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_contract(ISSVViews)4950": { - "label": "contract ISSVViews", - "numberOfBytes": "20" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - }, - "namespaces": {} - } - }, - "350f8e1c7a01c817bb83103cd78dcb1d0b3510b15601eff2bea87d6506ccd751": { - "address": "0x5Dbf9a62BbcC8135AF60912A8B0212a73e4a6629", - "txHash": "0xcdaf8e8d6193ad7d0fb8b358031620a874d5d5f16b9a8044e8fa4bfba01c141c", - "layout": { - "solcVersion": "0.8.24", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:40" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:21" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - }, - "namespaces": {} - } - } - } -} diff --git a/.openzeppelin/mainnet.json b/.openzeppelin/mainnet.json deleted file mode 100644 index ca45b2928..000000000 --- a/.openzeppelin/mainnet.json +++ /dev/null @@ -1,1298 +0,0 @@ -{ - "manifestVersion": "3.2", - "proxies": [ - { - "address": "0x42Cd8D240E30102B715d7516f97864ECeC4441Ab", - "txHash": "0x2c2e2e6fcd70e75f404d8bc5e09d9a1e4cff2bb9e6c80195b47c6227f06a8a63", - "kind": "uups" - }, - { - "address": "0xb54E555A7f8a0143C829C67F85fCe71523621E45", - "txHash": "0xe3250f79b6fb013dceb5628662e30943a1cbecaa765f52b9de8a12cdea70fcdb", - "kind": "uups" - }, - { - "address": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", - "txHash": "0x4a11a560d3c2f693e96f98abb1feb447646b01b36203ecab0a96a1cf45fd650b", - "kind": "uups" - }, - { - "address": "0xafE830B6Ee262ba11cce5F32fDCd760FFE6a66e4", - "txHash": "0x98028d64fb4e2428b8a523c085bb4edc2e5e2bd32542802784adb6aa7a12a2e2", - "kind": "uups" - } - ], - "impls": { - "064c555064aab91647555e9d6781b7254cafff19ce48d4c940fb3dbec372bff0": { - "address": "0x99a26a746d950a2E117E1220a765a018beDB0029", - "txHash": "0x320eb3ce44973dd6cfb31bc464d65314c4a97a9e33618db3d77bebbe62f3d909", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "07f0ad8d638b80e3e0c46a0ffab2878d79893dbc88ef70a1e05f76ae796932ca": { - "address": "0x2c14476920E931Eb1DA21EdB4215792A68bEAeA6", - "txHash": "0x880b616c1c13a1065b62c1cc624555f6f34375bb79370fabd4853acb7a85a2d5", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "ssvNetwork", - "offset": 0, - "slot": "251", - "type": "t_contract(IFnSSVViews)3548", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:20" - }, - { - "label": "private__gap", - "offset": 0, - "slot": "252", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:24" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_contract(IFnSSVViews)3548": { - "label": "contract IFnSSVViews", - "numberOfBytes": "20" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "5d5e858f3c695d1a8c3e7e337239b4cbff1683e92f0839c61c0024f83661cd8b": { - "address": "0xdc1E8E50673B893c16c18D88e81e13B4415F6292", - "txHash": "0xe6536ad3ddd1fd7b66930558431355ea92a730415a9bd09a6037dcc843ce279c", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "06c55e88f7f16ecefd8ce649742571d8712c1164ba0a39a3f273716195d24be1": { - "address": "0xe183d6eEac469B1544f19Cb5a37Fe6eBFc913C4E", - "txHash": "0xc20fc9836f5f9bcfa3dee10efe883183f3afc93b2b2f6a864e6e49236cc1b460", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "ssvNetwork", - "offset": 0, - "slot": "251", - "type": "t_contract(ISSVViews)3521", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:19" - }, - { - "label": "private__gap", - "offset": 0, - "slot": "252", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:23" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_contract(ISSVViews)3521": { - "label": "contract ISSVViews", - "numberOfBytes": "20" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "400b4079d3549b56702f7dfadb688c5395e64dd3211c072e6f584ae7ae02f79f": { - "address": "0xE2d1Cf93CD4D5E0EEEF1b33ca51Bb82c829A1b75", - "txHash": "0x5c1c55b3073cc2579fd426616636484a188f1205a4278dc37063bc891c397494", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "0bbad3c785dcdd02f463ea2ab702971e2ed300422a4e44ee35105568b16edf1b": { - "address": "0x7B6C84186be89bf0f28A3b5fAacFEd0b4d9D1c01", - "txHash": "0x6e4654a9681d21c10744dfb2e5c81e6acfda103ca3a26ff0bc7bc372b565f11d", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "723c9fc111a6d734d5819515928ae5423cf51ac95735557de2cdd68e886f1a04": { - "address": "0x050E94A68440531f3E89e93C33F349270e9D1750", - "txHash": "0x960563ae4a7743c7668e43d8a0c3f409e7b03d2c96c616ed098750ba530af4c8", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "00fc0f9456fc94965feb33fc5cbf61cbcb1758dd875f7adcab6b65862efa4474": { - "address": "0xfe11c3811eD58C518F5Bd23aDb1FAac487a16cBC", - "txHash": "0x9a5321fc1fa20140c23c9a6b07dc8bd0f4e230480f722f800fe930cf16803124", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "ssvNetwork", - "offset": 0, - "slot": "251", - "type": "t_contract(ISSVViews)4228", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:19" - }, - { - "label": "__gap", - "offset": 0, - "slot": "252", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:23" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_contract(ISSVViews)4228": { - "label": "contract ISSVViews", - "numberOfBytes": "20" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "cace716c99b7c3331ef14bea41153e9b79ee38508d038ed764ebfa2f3addf8d6": { - "address": "0x9c0D5400F82561EbE54110f2aD73Ad76f2917943", - "txHash": "0x2aea34e54f6e3382ace77acc6f4a09f14ed4fe432ff529523f7bad99f531e8aa", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - }, - "namespaces": {} - } - }, - "350f8e1c7a01c817bb83103cd78dcb1d0b3510b15601eff2bea87d6506ccd751": { - "address": "0xA2f1DaDBb9E836B7Ec47330fF9E5947D2f36FC35", - "txHash": "0xca8c8769030dd820f01373318e271b4f7507cfb1596c8f451ec06bfe7fc6d28b", - "layout": { - "solcVersion": "0.8.24", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:40" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:21" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - }, - "namespaces": {} - } - }, - "da06f711e239a6b927747cdeefb5954a754f3a4e01a17212488672f6689ac6f1": { - "address": "0x052E5F6bD9DB71C08Db38377596875ceC5708a94", - "txHash": "0xfe4a43b2032e6b82e39dbb833e2a8ecbf4346370c39fcd786c058c7a86e9317b", - "layout": { - "solcVersion": "0.8.24", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:40" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:21" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "ssvNetwork", - "offset": 0, - "slot": "251", - "type": "t_contract(ISSVViews)4950", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:19" - }, - { - "label": "__gap", - "offset": 0, - "slot": "252", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:23" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_contract(ISSVViews)4950": { - "label": "contract ISSVViews", - "numberOfBytes": "20" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - }, - "namespaces": {} - } - } - } -} diff --git a/Justfile b/Justfile index a7c3e48ac..c99d4f6ba 100644 --- a/Justfile +++ b/Justfile @@ -78,6 +78,11 @@ upgrade env network="": generate-safe-batch env="mainnet": npx tsx scripts/generate-safe-batch.ts --env {{env}} +# Simulate a queued SAFE transaction on a local fork, verify the post-state, then run fork tests +simulate-safe-upgrade env tx_file network="local": + npx hardhat compile --force + npx tsx scripts/simulate-safe-upgrade.ts --env {{env}} --tx-file {{tx_file}} --network {{network}} + # Generate deployment attestation (bytecode hashes + config summary for committee review) generate-attestation env="mainnet" network="": npx tsx scripts/generate-deployment-attestation.ts --env {{env}} {{ if network == "" { "" } else { "--network " + network } }} diff --git a/abis/SSVClusters.json b/abis/SSVClusters.json index 6ef500af5..3dcecc905 100644 --- a/abis/SSVClusters.json +++ b/abis/SSVClusters.json @@ -186,6 +186,11 @@ "name": "InsufficientBalance", "type": "error" }, + { + "inputs": [], + "name": "InsufficientCSSVSupply", + "type": "error" + }, { "inputs": [], "name": "InvalidOperatorFeeIncreaseLimit", @@ -319,22 +324,22 @@ }, { "inputs": [], - "name": "OracleHasZeroWeight", + "name": "PublicKeysSharesLengthMismatch", "type": "error" }, { "inputs": [], - "name": "PublicKeysSharesLengthMismatch", + "name": "ReentrancyGuardReentrantCall", "type": "error" }, { "inputs": [], - "name": "ReentrancyGuardReentrantCall", + "name": "RootNotFound", "type": "error" }, { "inputs": [], - "name": "RootNotFound", + "name": "SafeCastOverflow", "type": "error" }, { @@ -450,6 +455,11 @@ "name": "ZeroAmount", "type": "error" }, + { + "inputs": [], + "name": "ZeroCSSVSupply", + "type": "error" + }, { "anonymous": false, "inputs": [ diff --git a/abis/SSVDAO.json b/abis/SSVDAO.json index 0b7426134..6ac668f18 100644 --- a/abis/SSVDAO.json +++ b/abis/SSVDAO.json @@ -197,6 +197,11 @@ "name": "InsufficientBalance", "type": "error" }, + { + "inputs": [], + "name": "InsufficientCSSVSupply", + "type": "error" + }, { "inputs": [], "name": "InvalidOperatorFeeIncreaseLimit", @@ -330,22 +335,22 @@ }, { "inputs": [], - "name": "OracleHasZeroWeight", + "name": "PublicKeysSharesLengthMismatch", "type": "error" }, { "inputs": [], - "name": "PublicKeysSharesLengthMismatch", + "name": "ReentrancyGuardReentrantCall", "type": "error" }, { "inputs": [], - "name": "ReentrancyGuardReentrantCall", + "name": "RootNotFound", "type": "error" }, { "inputs": [], - "name": "RootNotFound", + "name": "SafeCastOverflow", "type": "error" }, { @@ -461,6 +466,11 @@ "name": "ZeroAmount", "type": "error" }, + { + "inputs": [], + "name": "ZeroCSSVSupply", + "type": "error" + }, { "anonymous": false, "inputs": [ diff --git a/abis/SSVOperators.json b/abis/SSVOperators.json index 2eed3a22e..353eb3e08 100644 --- a/abis/SSVOperators.json +++ b/abis/SSVOperators.json @@ -197,6 +197,11 @@ "name": "InsufficientBalance", "type": "error" }, + { + "inputs": [], + "name": "InsufficientCSSVSupply", + "type": "error" + }, { "inputs": [], "name": "InvalidOperatorFeeIncreaseLimit", @@ -330,22 +335,22 @@ }, { "inputs": [], - "name": "OracleHasZeroWeight", + "name": "PublicKeysSharesLengthMismatch", "type": "error" }, { "inputs": [], - "name": "PublicKeysSharesLengthMismatch", + "name": "ReentrancyGuardReentrantCall", "type": "error" }, { "inputs": [], - "name": "ReentrancyGuardReentrantCall", + "name": "RootNotFound", "type": "error" }, { "inputs": [], - "name": "RootNotFound", + "name": "SafeCastOverflow", "type": "error" }, { @@ -461,6 +466,11 @@ "name": "ZeroAmount", "type": "error" }, + { + "inputs": [], + "name": "ZeroCSSVSupply", + "type": "error" + }, { "anonymous": false, "inputs": [ diff --git a/abis/SSVOperatorsWhitelist.json b/abis/SSVOperatorsWhitelist.json index bdcce4f08..8b79aeb8b 100644 --- a/abis/SSVOperatorsWhitelist.json +++ b/abis/SSVOperatorsWhitelist.json @@ -186,6 +186,11 @@ "name": "InsufficientBalance", "type": "error" }, + { + "inputs": [], + "name": "InsufficientCSSVSupply", + "type": "error" + }, { "inputs": [], "name": "InvalidOperatorFeeIncreaseLimit", @@ -317,11 +322,6 @@ "name": "OracleAlreadyAssigned", "type": "error" }, - { - "inputs": [], - "name": "OracleHasZeroWeight", - "type": "error" - }, { "inputs": [], "name": "PublicKeysSharesLengthMismatch", @@ -445,6 +445,11 @@ "name": "ZeroAmount", "type": "error" }, + { + "inputs": [], + "name": "ZeroCSSVSupply", + "type": "error" + }, { "anonymous": false, "inputs": [ diff --git a/abis/SSVStaking.json b/abis/SSVStaking.json index 10596171c..4162669ea 100644 --- a/abis/SSVStaking.json +++ b/abis/SSVStaking.json @@ -197,6 +197,11 @@ "name": "InsufficientBalance", "type": "error" }, + { + "inputs": [], + "name": "InsufficientCSSVSupply", + "type": "error" + }, { "inputs": [], "name": "InvalidOperatorFeeIncreaseLimit", @@ -330,22 +335,22 @@ }, { "inputs": [], - "name": "OracleHasZeroWeight", + "name": "PublicKeysSharesLengthMismatch", "type": "error" }, { "inputs": [], - "name": "PublicKeysSharesLengthMismatch", + "name": "ReentrancyGuardReentrantCall", "type": "error" }, { "inputs": [], - "name": "ReentrancyGuardReentrantCall", + "name": "RootNotFound", "type": "error" }, { "inputs": [], - "name": "RootNotFound", + "name": "SafeCastOverflow", "type": "error" }, { @@ -461,6 +466,11 @@ "name": "ZeroAmount", "type": "error" }, + { + "inputs": [], + "name": "ZeroCSSVSupply", + "type": "error" + }, { "anonymous": false, "inputs": [ diff --git a/abis/SSVValidators.json b/abis/SSVValidators.json index 3cba64284..1ad364e45 100644 --- a/abis/SSVValidators.json +++ b/abis/SSVValidators.json @@ -186,6 +186,11 @@ "name": "InsufficientBalance", "type": "error" }, + { + "inputs": [], + "name": "InsufficientCSSVSupply", + "type": "error" + }, { "inputs": [], "name": "InvalidOperatorFeeIncreaseLimit", @@ -319,17 +324,17 @@ }, { "inputs": [], - "name": "OracleHasZeroWeight", + "name": "PublicKeysSharesLengthMismatch", "type": "error" }, { "inputs": [], - "name": "PublicKeysSharesLengthMismatch", + "name": "RootNotFound", "type": "error" }, { "inputs": [], - "name": "RootNotFound", + "name": "SafeCastOverflow", "type": "error" }, { @@ -445,6 +450,11 @@ "name": "ZeroAmount", "type": "error" }, + { + "inputs": [], + "name": "ZeroCSSVSupply", + "type": "error" + }, { "anonymous": false, "inputs": [ diff --git a/abis/SSVViews.json b/abis/SSVViews.json index eb5b3cb76..a6bea9d0c 100644 --- a/abis/SSVViews.json +++ b/abis/SSVViews.json @@ -197,6 +197,11 @@ "name": "InsufficientBalance", "type": "error" }, + { + "inputs": [], + "name": "InsufficientCSSVSupply", + "type": "error" + }, { "inputs": [], "name": "InvalidOperatorFeeIncreaseLimit", @@ -330,17 +335,17 @@ }, { "inputs": [], - "name": "OracleHasZeroWeight", + "name": "PublicKeysSharesLengthMismatch", "type": "error" }, { "inputs": [], - "name": "PublicKeysSharesLengthMismatch", + "name": "RootNotFound", "type": "error" }, { "inputs": [], - "name": "RootNotFound", + "name": "SafeCastOverflow", "type": "error" }, { @@ -456,6 +461,11 @@ "name": "ZeroAmount", "type": "error" }, + { + "inputs": [], + "name": "ZeroCSSVSupply", + "type": "error" + }, { "inputs": [], "name": "CSSV_ADDRESS", diff --git a/contracts/audits/2026-03-24_Quantstamp_v1.2.0.pdf b/contracts/audits/2026-03-24_Quantstamp_v1.2.0.pdf new file mode 100644 index 000000000..140364890 Binary files /dev/null and b/contracts/audits/2026-03-24_Quantstamp_v1.2.0.pdf differ diff --git a/deployments/mainnet/config.json b/deployments/mainnet/config.json index e4ad6f8f5..06f868fdf 100644 --- a/deployments/mainnet/config.json +++ b/deployments/mainnet/config.json @@ -7,7 +7,7 @@ "ssvNetworkViews": "0xafE830B6Ee262ba11cce5F32fDCd760FFE6a66e4", "ssvToken": "0x9D65fF81a3c488d585bBfb0Bfe3c7707c7917f54", "cooldownDuration": 604800, - "upgradeTimestamp": 24684128, + "upgradeTimestamp": 1774351800, "quorumBps": 7500, "defaultOracleIds": [1, 2, 3, 4], "protocolParams": { diff --git a/deployments/mainnet/deploy-result.json b/deployments/mainnet/deploy-result.json new file mode 120000 index 000000000..03fd85e25 --- /dev/null +++ b/deployments/mainnet/deploy-result.json @@ -0,0 +1 @@ +deploy-result.v2.0.0.json \ No newline at end of file diff --git a/deployments/mainnet/deploy-result.v2.0.0.json b/deployments/mainnet/deploy-result.v2.0.0.json new file mode 100644 index 000000000..1bf9e9b94 --- /dev/null +++ b/deployments/mainnet/deploy-result.v2.0.0.json @@ -0,0 +1,24 @@ +{ + "deployer": "0x3187a42658417a4d60866163A4534Ce00D40C0C8", + "chainId": "1", + "network": "mainnet", + "deployedAt": "2026-03-23T09:03:25.491Z", + "blockNumber": 24719200, + "implementations": { + "SSVNetworkSSVStakingUpgrade": "0x93029DC6F03c951f353E51a8f16f722CAa210e5f", + "SSVNetworkViews": "0x98FEBF8824028A212875d797aBa88362A9B11cc9" + }, + "cssvToken": { + "address": "0xe018D31F120A637828F46aFD6c64EC099d960546", + "deployed": true + }, + "modules": { + "SSVOperators": "0x338554A41b6a2Ec9325157C01666AD8b0ACe6060", + "SSVClusters": "0xf26bFC86210e9b53f95F4DFDBdEd4B2A42e792ED", + "SSVDAO": "0x8AB722746a83eAE7158e55d43dc4aDe5bb9E0212", + "SSVViews": "0x055051fa508EEdA80c38De34CA936aBa59642C45", + "SSVOperatorsWhitelist": "0xd302E99feE1BAB03824Ce9aE20c6c578908CcFa5", + "SSVStaking": "0x1B844e7abB9779f551dDcCb5f0f34A54eC1c7034", + "SSVValidators": "0xB1E718d775811af33382eF9850a8C2CA1097c8fB" + } +} diff --git a/deployments/mainnet/deployment-attestation.json b/deployments/mainnet/deployment-attestation.json new file mode 100644 index 000000000..7c20d6a52 --- /dev/null +++ b/deployments/mainnet/deployment-attestation.json @@ -0,0 +1,111 @@ +{ + "generatedAt": "2026-03-23T09:08:18.750Z", + "deployment": { + "deployer": "0x3187a42658417a4d60866163A4534Ce00D40C0C8", + "chainId": "1", + "network": "mainnet", + "deployedAt": "2026-03-23T09:03:25.491Z", + "blockNumber": 24719200 + }, + "config": { + "currentVersion": "v1.2.0", + "targetVersion": "v2.0.0", + "ssvNetworkProxy": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "ssvNetworkViews": "0xafE830B6Ee262ba11cce5F32fDCd760FFE6a66e4", + "ssvToken": "0x9D65fF81a3c488d585bBfb0Bfe3c7707c7917f54", + "cooldownDuration": 604800, + "upgradeTimestamp": 1774351800, + "quorumBps": 7500, + "defaultOracleIds": [ + 1, + 2, + 3, + 4 + ], + "initialStakeAmount": "1000000000000000000", + "protocolParams": { + "networkFeeEth": "3557600000", + "maxOperatorEthFee": "5336500000", + "minOperatorEthFee": "10000000", + "minimumLiquidationCollateralEth": "644852000000000", + "liquidationThresholdPeriod": "21480", + "minBlocksBetweenUpdates": "0", + "minimumLiquidationCollateralSSV": "673652000000000000", + "liquidationThresholdPeriodSSV": "50120" + }, + "oracles": { + "1": "0xc61f7bd9ee5a3d011caf47aa0e5411f720593920", + "2": "0xc07332e05cec1c4896555a6d10361233fdf14422", + "3": "0x28bEa5B242362974d5DDb8f17a1E0e525446960B", + "4": "0x3A98EE5f80268Ed91F8A5880d93468b76a9F3bB4" + } + }, + "contracts": { + "SSVNetworkSSVStakingUpgrade": { + "address": "0x93029DC6F03c951f353E51a8f16f722CAa210e5f", + "constructorArgs": {}, + "initializerArgs": { + "function": "initializeSSVStaking(uint64,uint32[4],uint16)", + "cooldownDuration": "604800", + "defaultOracleIds": "[1,2,3,4]", + "quorumBps": "7500" + }, + "bytecodeHash": "0xbeae889794dbe8294055f399dffbcee2102f17ed6c2dcff639c4253ea19e49d5" + }, + "SSVNetworkViews": { + "address": "0x98FEBF8824028A212875d797aBa88362A9B11cc9", + "constructorArgs": {}, + "bytecodeHash": "0xcfde1aad92cceb355933abd26bca4584e5df77d052d5a1d79f127dfbcfca8a60" + }, + "CSSVToken": { + "address": "0xe018D31F120A637828F46aFD6c64EC099d960546", + "constructorArgs": { + "ssvNetworkProxy": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1" + }, + "bytecodeHash": "0x9d14ddbf6e0224f9863297ad56c10f06121aba20c32e2c5b3f62def709362861" + }, + "SSVOperators": { + "address": "0x338554A41b6a2Ec9325157C01666AD8b0ACe6060", + "constructorArgs": { + "upgradeTimestamp": "1774351800" + }, + "bytecodeHash": "0x3891623830d26723c0b1d63c5f2e0096c21f5d70394d70ab4b56b8a8068c4cfa" + }, + "SSVClusters": { + "address": "0xf26bFC86210e9b53f95F4DFDBdEd4B2A42e792ED", + "constructorArgs": {}, + "bytecodeHash": "0xebc7beaefe2d01a73540e3527bb3acee9157120c86e8355ec072088780f06e24" + }, + "SSVDAO": { + "address": "0x8AB722746a83eAE7158e55d43dc4aDe5bb9E0212", + "constructorArgs": { + "cssvToken": "0xe018D31F120A637828F46aFD6c64EC099d960546" + }, + "bytecodeHash": "0xd8d314f21c630ea5e35e8082dc307bb550bffc57c8003c18cbef0eb023379243" + }, + "SSVViews": { + "address": "0x055051fa508EEdA80c38De34CA936aBa59642C45", + "constructorArgs": { + "cssvToken": "0xe018D31F120A637828F46aFD6c64EC099d960546" + }, + "bytecodeHash": "0xfb2869ea9b85a67f4fab9265d730f51fe8636662f5865e58feb2b5950e64c2e2" + }, + "SSVOperatorsWhitelist": { + "address": "0xd302E99feE1BAB03824Ce9aE20c6c578908CcFa5", + "constructorArgs": {}, + "bytecodeHash": "0x851f6a3d025ea681cbecc7bf1400c8275801b91f74c3c1c48d4dfd1ec7fb2428" + }, + "SSVStaking": { + "address": "0x1B844e7abB9779f551dDcCb5f0f34A54eC1c7034", + "constructorArgs": { + "cssvToken": "0xe018D31F120A637828F46aFD6c64EC099d960546" + }, + "bytecodeHash": "0x2036caa06ba7cbcab9fb947944b43a8e307e3c525a14bfaf5acf18180c0797f7" + }, + "SSVValidators": { + "address": "0xB1E718d775811af33382eF9850a8C2CA1097c8fB", + "constructorArgs": {}, + "bytecodeHash": "0xb14692576e41e11990e347dcb68121457bebf2d6826e93214333cbf47f7bfc3e" + } + } +} diff --git a/deployments/mainnet/multisig-batch.json b/deployments/mainnet/multisig-batch.json new file mode 100644 index 000000000..a7504cc68 --- /dev/null +++ b/deployments/mainnet/multisig-batch.json @@ -0,0 +1,132 @@ +{ + "version": "1.0", + "chainId": "1", + "createdAt": 1774256802776, + "meta": { + "name": "SSV Network v2.0.0 Upgrade (mainnet)", + "description": "Upgrade SSVNetwork proxy, attach modules, set protocol parameters, and configure oracles for the mainnet environment.", + "createdFromSafeAddress": "0xb35096b074fdb9bBac63E3AdaE0Bbde512B2E6b6" + }, + "transactions": [ + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0x4f1ef28600000000000000000000000093029dc6f03c951f353e51a8f16f722caa210e5f000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c4d0937f030000000000000000000000000000000000000000000000000000000000093a8000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000001d4c00000000000000000000000000000000000000000000000000000000" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0xe3e324b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000338554a41b6a2ec9325157c01666ad8b0ace6060" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0xe3e324b00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000f26bfc86210e9b53f95f4dfdbded4b2a42e792ed" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0xe3e324b000000000000000000000000000000000000000000000000000000000000000020000000000000000000000008ab722746a83eae7158e55d43dc4ade5bb9e0212" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0xe3e324b00000000000000000000000000000000000000000000000000000000000000003000000000000000000000000055051fa508eeda80c38de34ca936aba59642c45" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0xe3e324b00000000000000000000000000000000000000000000000000000000000000004000000000000000000000000d302e99fee1bab03824ce9ae20c6c578908ccfa5" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0xe3e324b000000000000000000000000000000000000000000000000000000000000000050000000000000000000000001b844e7abb9779f551ddccb5f0f34a54ec1c7034" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0xe3e324b00000000000000000000000000000000000000000000000000000000000000006000000000000000000000000b1e718d775811af33382ef9850a8c2ca1097c8fb" + }, + { + "to": "0xafE830B6Ee262ba11cce5F32fDCd760FFE6a66e4", + "value": "0", + "data": "0x3659cfe600000000000000000000000098febf8824028a212875d797aba88362a9b11cc9" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0x1f1f9fd500000000000000000000000000000000000000000000000000000000d40cab00" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0x6512447d00000000000000000000000000000000000000000000000000000000000053e8" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0xe567ed58000000000000000000000000000000000000000000000000000000000000c3c8" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0x11dff2490000000000000000000000000000000000000000000000000000000000000000" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0xb4c9c40800000000000000000000000000000000000000000000000000024a7d4e648800" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0x9f5c130700000000000000000000000000000000000000000000000009594aecc23b4000" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0x6f4b158d000000000000000000000000000000000000000000000000000000013e148720" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0xe9d232cd0000000000000000000000000000000000000000000000000000000000989680" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0x9ba0e7000000000000000000000000000000000000000000000000000000000000001d4c" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0x5d756b1d0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000c61f7bd9ee5a3d011caf47aa0e5411f720593920" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0x5d756b1d0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c07332e05cec1c4896555a6d10361233fdf14422" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0x5d756b1d000000000000000000000000000000000000000000000000000000000000000300000000000000000000000028bea5b242362974d5ddb8f17a1e0e525446960b" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0x5d756b1d00000000000000000000000000000000000000000000000000000000000000040000000000000000000000003a98ee5f80268ed91f8a5880d93468b76a9f3bb4" + }, + { + "to": "0x9D65fF81a3c488d585bBfb0Bfe3c7707c7917f54", + "value": "0", + "data": "0x095ea7b3000000000000000000000000dd9bc35ae942ef0cfa76930954a156b3ff30a4e10000000000000000000000000000000000000000000000000de0b6b3a7640000" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0xa694fc3a0000000000000000000000000000000000000000000000000de0b6b3a7640000" + } + ] +} diff --git a/deployments/mainnet/safe-tx.nonce-606.json b/deployments/mainnet/safe-tx.nonce-606.json new file mode 100644 index 000000000..27fbebcfa --- /dev/null +++ b/deployments/mainnet/safe-tx.nonce-606.json @@ -0,0 +1,12 @@ +{ + "to": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", + "data": "0x8d80ff0a00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000df800dd9bc35ae942ef0cfa76930954a156b3ff30a4e1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001444f1ef28600000000000000000000000093029dc6f03c951f353e51a8f16f722caa210e5f000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c4d0937f030000000000000000000000000000000000000000000000000000000000093a8000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000001d4c0000000000000000000000000000000000000000000000000000000000dd9bc35ae942ef0cfa76930954a156b3ff30a4e100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044e3e324b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000338554a41b6a2ec9325157c01666ad8b0ace606000dd9bc35ae942ef0cfa76930954a156b3ff30a4e100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044e3e324b00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000f26bfc86210e9b53f95f4dfdbded4b2a42e792ed00dd9bc35ae942ef0cfa76930954a156b3ff30a4e100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044e3e324b000000000000000000000000000000000000000000000000000000000000000020000000000000000000000008ab722746a83eae7158e55d43dc4ade5bb9e021200dd9bc35ae942ef0cfa76930954a156b3ff30a4e100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044e3e324b00000000000000000000000000000000000000000000000000000000000000003000000000000000000000000055051fa508eeda80c38de34ca936aba59642c4500dd9bc35ae942ef0cfa76930954a156b3ff30a4e100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044e3e324b00000000000000000000000000000000000000000000000000000000000000004000000000000000000000000d302e99fee1bab03824ce9ae20c6c578908ccfa500dd9bc35ae942ef0cfa76930954a156b3ff30a4e100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044e3e324b000000000000000000000000000000000000000000000000000000000000000050000000000000000000000001b844e7abb9779f551ddccb5f0f34a54ec1c703400dd9bc35ae942ef0cfa76930954a156b3ff30a4e100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044e3e324b00000000000000000000000000000000000000000000000000000000000000006000000000000000000000000b1e718d775811af33382ef9850a8c2ca1097c8fb00afe830b6ee262ba11cce5f32fdcd760ffe6a66e4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000243659cfe600000000000000000000000098febf8824028a212875d797aba88362a9b11cc900dd9bc35ae942ef0cfa76930954a156b3ff30a4e1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000241f1f9fd500000000000000000000000000000000000000000000000000000000d40cab0000dd9bc35ae942ef0cfa76930954a156b3ff30a4e1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000246512447d00000000000000000000000000000000000000000000000000000000000053e800dd9bc35ae942ef0cfa76930954a156b3ff30a4e100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024e567ed58000000000000000000000000000000000000000000000000000000000000c3c800dd9bc35ae942ef0cfa76930954a156b3ff30a4e10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002411dff249000000000000000000000000000000000000000000000000000000000000000000dd9bc35ae942ef0cfa76930954a156b3ff30a4e100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024b4c9c40800000000000000000000000000000000000000000000000000024a7d4e64880000dd9bc35ae942ef0cfa76930954a156b3ff30a4e1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000249f5c130700000000000000000000000000000000000000000000000009594aecc23b400000dd9bc35ae942ef0cfa76930954a156b3ff30a4e1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000246f4b158d000000000000000000000000000000000000000000000000000000013e14872000dd9bc35ae942ef0cfa76930954a156b3ff30a4e100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024e9d232cd000000000000000000000000000000000000000000000000000000000098968000dd9bc35ae942ef0cfa76930954a156b3ff30a4e1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000249ba0e7000000000000000000000000000000000000000000000000000000000000001d4c00dd9bc35ae942ef0cfa76930954a156b3ff30a4e1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000445d756b1d0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000c61f7bd9ee5a3d011caf47aa0e5411f72059392000dd9bc35ae942ef0cfa76930954a156b3ff30a4e1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000445d756b1d0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c07332e05cec1c4896555a6d10361233fdf1442200dd9bc35ae942ef0cfa76930954a156b3ff30a4e1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000445d756b1d000000000000000000000000000000000000000000000000000000000000000300000000000000000000000028bea5b242362974d5ddb8f17a1e0e525446960b00dd9bc35ae942ef0cfa76930954a156b3ff30a4e1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000445d756b1d00000000000000000000000000000000000000000000000000000000000000040000000000000000000000003a98ee5f80268ed91f8a5880d93468b76a9f3bb4009d65ff81a3c488d585bbfb0bfe3c7707c7917f5400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044095ea7b3000000000000000000000000dd9bc35ae942ef0cfa76930954a156b3ff30a4e10000000000000000000000000000000000000000000000000de0b6b3a764000000dd9bc35ae942ef0cfa76930954a156b3ff30a4e100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024a694fc3a0000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000", + "value": "0", + "operation": 1, + "baseGas": "0", + "gasPrice": "0", + "gasToken": "0x0000000000000000000000000000000000000000", + "nonce": 606, + "refundReceiver": "0x0000000000000000000000000000000000000000", + "safeTxGas": "0" +} diff --git a/docs/SAFE_UPGRADE_SIMULATION_PLAYBOOK.md b/docs/SAFE_UPGRADE_SIMULATION_PLAYBOOK.md new file mode 100644 index 000000000..5b09b88a0 --- /dev/null +++ b/docs/SAFE_UPGRADE_SIMULATION_PLAYBOOK.md @@ -0,0 +1,368 @@ +# Safe Upgrade Fork Simulation Playbook + +## Purpose + +This document describes how to simulate the exact queued SAFE upgrade transaction for the SSV Network mainnet upgrade on a local mainnet fork. + +The goal is to validate the real SAFE execution path, not a simplified owner-direct upgrade flow. The simulation: + +- executes the queued SAFE transaction through `execTransaction` +- impersonates the required SAFE owners on a local fork +- verifies the post-upgrade on-chain state +- writes an `upgrade-result`-compatible output file +- runs the fork integration suite against the upgraded fork state + +This playbook is aligned with: + +- `deployments/mainnet/config.json` +- `deployments/mainnet/deploy-result.json` +- `deployments/mainnet/multisig-batch.json` +- `scripts/simulate-safe-upgrade.ts` +- `scripts/run-forked-tests.ts` + +## Current Mainnet Scope + +For the current `v1.2.0 -> v2.0.0` mainnet upgrade flow in this repository: + +- SAFE address: `0xb35096b074fdb9bBac63E3AdaE0Bbde512B2E6b6` +- queued SAFE nonce: `606` +- SSVNetwork proxy: `0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1` +- SSVNetworkViews proxy: `0xafE830B6Ee262ba11cce5F32fDCd760FFE6a66e4` +- MultiSend target: `0x40A2aCCbd92BCA938b02010E17A5b8929b49130D` + +The queued SAFE transaction is expected to be a `delegatecall` to `MultiSend`, containing the same `24` inner calls as `deployments/mainnet/multisig-batch.json`. + +## Current Config Snapshot + +The current repository config for this simulation is: + +```json +{ + "currentVersion": "v1.2.0", + "targetVersion": "v2.0.0", + "skipInitializer": false, + "owner": "0xb35096b074fdb9bBac63E3AdaE0Bbde512B2E6b6", + "ssvNetworkProxy": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "ssvNetworkViews": "0xafE830B6Ee262ba11cce5F32fDCd760FFE6a66e4", + "ssvToken": "0x9D65fF81a3c488d585bBfb0Bfe3c7707c7917f54", + "cooldownDuration": 604800, + "upgradeTimestamp": 1774351800, + "quorumBps": 7500, + "defaultOracleIds": [1, 2, 3, 4], + "protocolParams": { + "networkFeeEth": "3557600000", + "maxOperatorEthFee": "5336500000", + "minOperatorEthFee": "10000000", + "minimumLiquidationCollateralEth": "644852000000000", + "liquidationThresholdPeriod": "21480", + "minBlocksBetweenUpdates": "0", + "minimumLiquidationCollateralSSV": "673652000000000000", + "liquidationThresholdPeriodSSV": "50120" + }, + "initialStakeAmount": "1000000000000000000", + "oracles": { + "1": "0xc61f7bd9ee5a3d011caf47aa0e5411f720593920", + "2": "0xc07332e05cec1c4896555a6d10361233fdf14422", + "3": "0x28bEa5B242362974d5DDb8f17a1E0e525446960B", + "4": "0x3A98EE5f80268Ed91F8A5880d93468b76a9F3bB4" + } +} +``` + +## Current Deployment Snapshot + +The currently committed deployment output for the same upgrade is: + +```json +{ + "deployer": "0x3187a42658417a4d60866163A4534Ce00D40C0C8", + "chainId": "1", + "network": "mainnet", + "deployedAt": "2026-03-23T09:03:25.491Z", + "blockNumber": 24719200, + "implementations": { + "SSVNetworkSSVStakingUpgrade": "0x93029DC6F03c951f353E51a8f16f722CAa210e5f", + "SSVNetworkViews": "0x98FEBF8824028A212875d797aBa88362A9B11cc9" + }, + "cssvToken": { + "address": "0xe018D31F120A637828F46aFD6c64EC099d960546", + "deployed": true + }, + "modules": { + "SSVOperators": "0x338554A41b6a2Ec9325157C01666AD8b0ACe6060", + "SSVClusters": "0xf26bFC86210e9b53f95F4DFDBdEd4B2A42e792ED", + "SSVDAO": "0x8AB722746a83eAE7158e55d43dc4aDe5bb9E0212", + "SSVViews": "0x055051fa508EEdA80c38De34CA936aBa59642C45", + "SSVOperatorsWhitelist": "0xd302E99feE1BAB03824Ce9aE20c6c578908CcFa5", + "SSVStaking": "0x1B844e7abB9779f551dDcCb5f0f34A54eC1c7034", + "SSVValidators": "0xB1E718d775811af33382eF9850a8C2CA1097c8fB" + } +} +``` + +## Source of Truth + +Use the following inputs together: + +- `deployments/mainnet/config.json` +- `deployments/mainnet/deploy-result.json` +- `deployments/mainnet/multisig-batch.json` +- the queued SAFE transaction JSON exported from the SAFE UI or transaction service + +The queued SAFE transaction JSON must contain these fields: + +- `to` +- `data` +- `value` +- `operation` +- `baseGas` +- `gasPrice` +- `gasToken` +- `nonce` +- `refundReceiver` +- `safeTxGas` + +The simulation script treats the queued SAFE transaction JSON as the execution source of truth and uses `multisig-batch.json` as a consistency check on the decoded inner calls. + +## Preconditions + +Complete all of the following before running the simulation: + +1. Install project dependencies. +2. Ensure the repository is on the release candidate code intended for the upgrade. +3. Confirm `deployments/mainnet/config.json`, `deploy-result.json`, and `multisig-batch.json` are up to date. +4. Save the queued SAFE transaction JSON to a local file. +5. Start a writable local mainnet fork on `127.0.0.1:8545`. +6. Make sure the fork is at a pre-execution state where: + - the SAFE still has nonce `606` + - `SSVNetwork.getVersion()` still reports `v1.2.0` + - the queued SAFE transaction has not already been executed on that fork state + +## Step 1: Start the Local Mainnet Fork + +Run Anvil against mainnet: + +```bash +anvil --fork-url "$MAINNET_RPC_URL" --port 8545 +``` + +If you need deterministic replay, pin a specific block: + +```bash +anvil --fork-url "$MAINNET_RPC_URL" --fork-block-number --port 8545 +``` + +The simulation script expects a writable fork at: + +```text +http://127.0.0.1:8545 +``` + +## Step 2: Save the Queued SAFE Transaction JSON + +Store the queued SAFE transaction object locally, for example: + +```text +deployments/mainnet/safe-tx.nonce-606.json +``` + +The JSON should represent the actual queued transaction, not a reconstructed approximation. + +## Step 3: Run the SAFE Simulation + +Recommended command: + +```bash +just simulate-safe-upgrade mainnet deployments/mainnet/safe-tx.nonce-606.json +``` + +Equivalent direct command: + +```bash +npx tsx scripts/simulate-safe-upgrade.ts \ + --env mainnet \ + --tx-file deployments/mainnet/safe-tx.nonce-606.json \ + --network local \ + --output deployments/mainnet/safe-simulation-result.nonce-606.json +``` + +Optional flags: + +- `--skip-fork-tests true` + Use this if you only want SAFE execution plus post-upgrade verification without running the behavioral fork suite. +- `--test ` + Use this to run a narrower fork test file instead of the default full integration fork suite. + +## What the Script Does + +The simulation flow performs the following steps: + +1. Loads `config.json`, `deploy-result.json`, `multisig-batch.json`, and the queued SAFE transaction JSON. +2. Decodes the SAFE transaction `data` as `multiSend(bytes)`. +3. Verifies that the decoded inner calls match the repository batch by count, order, target, value, and calldata. +4. Reads `getOwners()`, `getThreshold()`, and `nonce()` from the SAFE on the local fork. +5. Verifies the fork is still in the expected pre-upgrade state. +6. Selects the first `threshold` SAFE owners returned by `getOwners()`. +7. Impersonates those owners on the fork and calls `approveHash(safeTxHash)` for each of them. +8. Builds SAFE pre-validated signature bytes. +9. Executes the exact queued transaction via `execTransaction`. +10. Confirms the SAFE emits `ExecutionSuccess` and increments nonce from `606` to `607`. +11. Verifies: + - `SSVNetwork` version + - `SSVNetworkViews` readability + - ERC-1967 implementation addresses + - module pointers + - cSSV token address + - protocol parameters + - oracle set, quorum, cooldown + - initial stake / cSSV supply effects +12. Writes an `upgrade-result`-compatible JSON output file. +13. Runs fork integration tests against the upgraded local fork state. + +## Output Files + +The script writes: + +```text +deployments/mainnet/safe-simulation-result.nonce-606.json +``` + +This file is intentionally shaped like `upgrade-result.json`, with an additional `simulation` block containing: + +- `safeAddress` +- `safeTxHash` +- `safeNonce` +- `postExecutionSafeNonce` +- `selectedApprovers` +- `executionBlock` +- `receiptHash` + +This output can be consumed directly by the existing fork test runner. + +## Success Criteria + +The simulation is considered successful only if all of the following hold: + +- the SAFE transaction decodes correctly as `multiSend(bytes)` +- the decoded inner calls match `deployments/mainnet/multisig-batch.json` +- `execTransaction.staticCall(...)` returns `true` +- the real `execTransaction(...)` succeeds +- the SAFE nonce moves from `606` to `607` +- `SSVNetwork.getVersion()` reports `v2.0.0` +- proxy implementation addresses match `deploy-result.json` +- module pointers match `deploy-result.json` +- shared post-upgrade verification passes without mismatches +- the fork integration suite passes + +## Troubleshooting + +### Safe nonce mismatch + +Symptom: + +```text +Safe nonce mismatch: expected 606, got +``` + +Cause: + +- the fork is too new +- the transaction was already executed on the fork source state +- the wrong SAFE transaction JSON was provided + +Action: + +- restart the fork from a pre-execution block +- verify the queued transaction really belongs to nonce `606` + +### MultiSend batch mismatch + +Symptom: + +```text +inner call calldata mismatch +``` + +Cause: + +- the queued SAFE transaction differs from the repository batch +- `deploy-result.json` or `multisig-batch.json` is stale +- the wrong transaction JSON was exported + +Action: + +- regenerate and review `multisig-batch.json` +- compare the queued SAFE transaction against the current deployment artifacts + +### `execTransaction.staticCall` fails or returns `false` + +Cause: + +- insufficient owner approvals +- wrong nonce +- wrong SAFE transaction fields +- fork state does not match the intended pre-execution state + +Action: + +- verify owners and threshold on-chain +- verify the transaction fields are identical to the queued SAFE tx +- re-check the fork block and source RPC + +### Post-upgrade verification mismatch + +Cause: + +- config drift +- wrong deployment artifacts +- wrong queued SAFE transaction +- unexpected fork state + +Action: + +- re-check `config.json`, `deploy-result.json`, and the queued tx JSON as one set + +### Fork tests fail after a successful SAFE execution + +Cause: + +- the upgrade path is correct, but the upgraded behavior regresses +- the fork test suite is using a stale config file + +Action: + +- inspect the generated `safe-simulation-result.nonce-606.json` +- rerun: + +```bash +npx tsx scripts/run-forked-tests.ts \ + --config deployments/mainnet/safe-simulation-result.nonce-606.json \ + --fork-network hardhat_forked \ + --use-deployed-state true \ + --strict-deployed-state true \ + --allow-deployed-fallback false \ + --no-gas-enforce true +``` + +with: + +```bash +MAINNET_RPC_URL=http://127.0.0.1:8545 +``` + +## Recommended Operator Workflow + +For the current mainnet release, the recommended dry-run sequence is: + +1. `just deploy mainnet` +2. `just generate-safe-batch mainnet` +3. export or copy the queued SAFE transaction JSON +4. start a local mainnet fork +5. `just simulate-safe-upgrade mainnet ` +6. review the generated simulation result file +7. deliver the SAFE batch and attestation to the multisig committee only after the fork simulation passes + +## Related Documents + +- [UPGRADE_PLAYBOOK.md](./UPGRADE_PLAYBOOK.md) +- [../deployments/README.md](../deployments/README.md) diff --git a/docs/SCENARIO-TESTS.md b/docs/SCENARIO-TESTS.md deleted file mode 100644 index 684e6bbc2..000000000 --- a/docs/SCENARIO-TESTS.md +++ /dev/null @@ -1,1374 +0,0 @@ -# SSV Network v2.0.0 — Scenario Test Plan - -## How to Read This Document - -Each scenario is a specific sequence of contract interactions with exact expected outcomes. -Tests will be implemented in `test/e2e/` using Hardhat + ethers v6 + Chai. - -### Scenario Format -- **Preconditions**: Exact contract state before the scenario starts -- **Action Sequence**: Step-by-step with block numbers and expected state changes -- **Assertions**: Exact formulas with actual numbers — not "balance is correct" but the full calculation -- **Edge Variations**: Boundary conditions and tweaks on the same scenario - -### Naming Convention -- **OV-N**: Operators + Validators scenarios -- **CM-N**: Clusters + Migration scenarios -- **ES-N**: Effective Balance + Staking scenarios -- **CC-N**: Cross-Cutting scenarios (span 3+ modules) - -### Key Constants Used Throughout -``` -BPS_DENOMINATOR = 10_000 -ETH_DEDUCTED_DIGITS = 100_000 -DEDUCTED_DIGITS = 10_000_000 -DEFAULT_OPERATOR_ETH_FEE = 1_770_000_000 wei → packed raw = 17_700 -DEFAULT_EB_PER_VALIDATOR = 32 ETH -MAX_EB_PER_VALIDATOR = 2048 ETH -PRECISION (staking) = 1e18 -MAX_PENDING_REQUESTS = 10 -MINIMAL_STAKING_AMOUNT = 1_000_000_000 -VERSION_SSV = 0 -VERSION_ETH = 1 -``` - ---- - -## All Discrepancies (Code vs FLOWS.md) - -> **Status as of 2026-02-27**: Reviewed against updated FLOWS.md. Most discrepancies have been resolved through documentation updates. - -### Summary - -- ✅ **8 RESOLVED**: FLOWS.md updated to match code behavior (DISC-OV-1, OV-2, OV-3, OV-4, OV-8, OV-9, CM-3, CM-5, ES-6) -- ℹ️ **6 IMPLEMENTATION DETAILS**: Low-level choices that don't contradict FLOWS.md (DISC-OV-5, OV-6, OV-7, CM-6, ES-1, ES-2, CC-1) - -All originally documented discrepancies have been addressed. Tests can now be implemented with confidence that FLOWS.md accurately reflects the contract behavior. - ---- - -### ✅ DISC-OV-1: `registerOperator` always emits `OperatorPrivacyStatusUpdated` even when public -- **Source partition:** OV -- **Original FLOWS.md claim:** (§4.1) Only emit when `setPrivate` is true -- **Code does:** `SSVOperators.sol:65` — always emits regardless of `setPrivate` value -- **Resolution:** ✅ **RESOLVED** — FLOWS.md §4.1 updated to show unconditional emission: `emit OperatorPrivacyStatusUpdated([operatorId], setPrivate);` -- **Impact:** Low — informational event. Tests should expect the event in both cases with the boolean value. - -### ✅ DISC-OV-2: `registerOperator` does NOT validate fee against minimum when fee is 0 -- **Source partition:** OV -- **Original FLOWS.md claim:** (§4.1) Fee must be within `[minimumOperatorEthFee, operatorMaxFee]` -- **Code does:** `SSVOperators.sol:38-43` — minimum check skipped when fee=0 (`if (fee != 0 && fee < minimumOperatorEthFee)`) -- **Resolution:** ✅ **RESOLVED** — FLOWS.md §4.1 updated to clarify: "Fee must be 0 (free operator) OR within `[minimumOperatorEthFee, operatorMaxFee]`" -- **Impact:** Medium — zero-fee operators are intentionally allowed and cannot increase fees later. - -### ✅ DISC-OV-3: `removeOperator` does NOT check `validatorCount == 0 && ethValidatorCount == 0` -- **Source partition:** OV -- **Original FLOWS.md claim:** (§4.2) "Operator must have 0 validators in BOTH SSV and ETH counts" -- **Code does:** `SSVOperators.sol:71-93` — no validator count check before removal -- **Resolution:** ✅ **RESOLVED** — Original claim was incorrect. FLOWS.md §4.2 correctly documents only: "Operator must exist" and "Caller must be operator owner". No validator count requirement is imposed (by design). -- **Impact:** HIGH for invariants — an operator with active validators CAN be removed, which may break `ethDaoValidatorCount == Σ(operator.ethValidatorCount)`. This is intentional design; clusters referencing removed operators continue to function with frozen fee indices. - -### ✅ DISC-OV-4: `removeOperator` does NOT zero `ethSnapshot.index` or `snapshot.index` -- **Source partition:** OV -- **Original FLOWS.md claim:** (§4.2) Implies ALL snapshot fields zeroed -- **Code does:** `SSVOperators.sol:324-335` via `_resetOperatorState` — indices intentionally preserved -- **Resolution:** ✅ **RESOLVED** — FLOWS.md §4.2 now explicitly states: "Keeps `ethSnapshot.index`, `snapshot.index`" -- **Impact:** Low — frozen indices used by clusters referencing removed operators for fee calculations. - -### ℹ️ DISC-OV-5: `declareOperatorFee` calls `ensureETHDefaults` but `reduceOperatorFee` does not -- **Source partition:** OV -- **FLOWS.md says:** No mention of `ensureETHDefaults` in either flow -- **Code does:** `SSVOperators.sol:106-108` — only `declareOperatorFee` calls it -- **Resolution:** ℹ️ **IMPLEMENTATION DETAIL** — Design choice. Reducing a zero ETH fee would revert anyway. -- **Impact:** Low — functionally correct, no documentation change needed. - -### ℹ️ DISC-OV-6: `reduceOperatorFee` uses memory copy, `executeOperatorFee` uses storage directly -- **Source partition:** OV -- **FLOWS.md says:** Both describe same pattern -- **Code does:** Different gas profiles but functionally equivalent -- **Resolution:** ℹ️ **IMPLEMENTATION DETAIL** — Gas optimization, both are correct. -- **Impact:** Low — no user-facing difference. - -### ℹ️ DISC-OV-7: `_bulkRemoveValidator` skips operators with `ethSnapshot.block == 0` -- **Source partition:** OV -- **FLOWS.md says:** (§1.3) "Update operator ETH snapshots" -- **Code does:** `OperatorLib.sol:267` — skips removed operators (block==0) -- **Resolution:** ℹ️ **IMPLEMENTATION DETAIL** — Removed operators contribute frozen index, no snapshot update needed. -- **Impact:** Low — not contradicted by FLOWS.md high-level flow. - -### ✅ DISC-OV-8: `deposit` does NOT update operator snapshots or settle cluster fees -- **Source partition:** OV / CM (duplicate finding) -- **Original FLOWS.md claim:** (§1.4) "1. Update operator snapshots, 2. Settle cluster fees, 3. Add deposit" -- **Code does:** `SSVClusters.sol:190-205` — only validates hash, adds balance, stores hash -- **Resolution:** ✅ **RESOLVED** — FLOWS.md §1.7 State Mutations now correctly show: `1. cluster.balance += msg.value, 2. Update stored cluster hash` -- **Impact:** Medium — Tests must NOT expect fee settlement on deposit. Fees settle on next state change. - -### ✅ DISC-OV-9: `deposit` does NOT check `cluster.active` -- **Source partition:** OV / CM (duplicate finding) -- **Original FLOWS.md claim:** (§1.4) "Cluster must be active" -- **Code does:** `SSVClusters.sol:190-205` — no active check -- **Resolution:** ✅ **RESOLVED** — FLOWS.md §1.7 now explicitly notes: "deposits allowed on liquidated clusters" -- **Impact:** Low — depositing to liquidated cluster is permissive, sets up for reactivation. - -### ✅ DISC-CM-3: `withdraw` does NOT update operator snapshots to storage -- **Source partition:** CM -- **Original FLOWS.md claim:** (§1.5) "1. Update operator snapshots" -- **Code does:** `SSVClusters.sol:220-234` — reads operator indices inline without writing back -- **Resolution:** ✅ **RESOLVED** — FLOWS.md §1.8 State Mutations omit operator snapshot updates, listing only cluster balance changes. -- **Impact:** HIGH for test design — operator earnings NOT updated during withdraw. Use `>=` in conservation checks. - -### ✅ DISC-CM-5: `reactivate` uses `cluster.balance += msg.value` (additive, not replacement) -- **Source partition:** CM -- **Original FLOWS.md claim:** (§1.11) `cluster.balance = msg.value` (implies replacement) -- **Code does:** `SSVClusters.sol:160` — `+=` adds to any pre-existing deposits -- **Resolution:** ✅ **RESOLVED** — FLOWS.md §1.11 State Mutations updated to: `balance += msg.value` -- **Impact:** Medium — tests should verify deposit-into-liquidated + reactivate interaction (balance accumulates). - -### ℹ️ DISC-CM-6: Migration EB deviation only applied if `vUnitsCluster > baseline` -- **Source partition:** CM -- **FLOWS.md says:** (§2.1) Handles deviation -- **Code does:** `SSVClusters.sol:315-331` — only adds positive deviation -- **Resolution:** ℹ️ **IMPLEMENTATION DETAIL** — EB floor is 32 ETH so deviation can never be negative after migration. -- **Impact:** Low — correct behavior, no documentation ambiguity. - -### ℹ️ DISC-ES-1: `_syncFees` unconditionally updates `ethDaoBalance` and `ethDaoIndexBlockNumber` -- **Source partition:** ES -- **FLOWS.md says:** (§5.5) Only mentions case where new fees exist -- **Code does:** `SSVStaking.sol:182-184` — always sets these BEFORE checking if `current > previous` -- **Resolution:** ℹ️ **IMPLEMENTATION DETAIL** — Must settle DAO to get consistent snapshot. -- **Impact:** Low — correct behavior. - -### ℹ️ DISC-ES-2: `_syncFees` handles `current <= previous` by updating `stakingEthPoolBalance` -- **Source partition:** ES -- **FLOWS.md says:** (§5.5) Only mentions positive fees case -- **Code does:** `SSVStaking.sol:187-189` — sets `stakingEthPoolBalance = current` when no new fees -- **Resolution:** ℹ️ **IMPLEMENTATION DETAIL** — Keeps pool balance synced after claims. -- **Impact:** Low — edge case handling, correct. - -### ✅ DISC-ES-6: Operator deviation in `_updateOperatorVUnits` applies FULL delta to EACH operator -- **Source partition:** ES -- **Original FLOWS.md claim:** (§3.2) `operatorEthVUnits[opId] += (newVUnits - effectiveOldVUnits) / operatorCount` -- **Code does:** `SSVClusters.sol:496-515` — applies FULL delta to every operator, NOT divided -- **Resolution:** ✅ **RESOLVED** — FLOWS.md §3.2 now explicitly states with emphasis: "For each operator: `operatorEthVUnits[opId] += (newVUnits - effectiveOldVUnits)` — **full delta applied to every operator, no division by operator count**" -- **Impact:** HIGH — Each operator tracks the sum of deviations from ALL its clusters. Critical for correct earnings calculation. - -### ℹ️ DISC-CC-1: `removeOperator` does NOT delete `operatorFeeChangeRequests` -- **Source partition:** CC (cross-cutting finding) -- **FLOWS.md says:** (§4.2) "Delete fee change request (if any)" -- **Code does:** `SSVOperators.sol:71-93` — no explicit deletion -- **Resolution:** ℹ️ **IMPLEMENTATION DETAIL** — Harmless storage leak; `checkOwner` fails on subsequent attempts to execute. -- **Impact:** Low — minor storage leak, no functional impact. - ---- - -## Global Invariants (Check in EVERY cross-cutting test) - -1. **ETH Conservation**: `contract.ETH_balance ≈ Σ(current ETH cluster balances) + Σ(current operator ETH earnings) + ProtocolLib.networkTotalEarnings()` - -2. **SSV Conservation**: `contract.SSV_balance ≈ Σ(current SSV cluster balances) + Σ(current operator SSV earnings) + networkTotalEarningsSSV() + stakingHeldSSV` - -3. **Validator Count**: `sp.ethDaoValidatorCount == Σ(cluster.validatorCount)` across all active ETH clusters - -4. **vUnit Consistency**: `sp.daoTotalEthVUnits == sp.ethDaoValidatorCount × BPS_DENOMINATOR + Σ(cluster EB deviations)` - - Where deviation = `clusterEB.vUnits - validatorCount × BPS_DENOMINATOR` for explicit EB clusters - -5. **Cluster Hash Integrity**: `s.ethClusters[key] == keccak256(abi.encodePacked(validatorCount, networkFeeIndex, index, balance, active))` - -6. **cSSV Supply**: `cSSV.totalSupply() == Σ(staked SSV) - Σ(unstake-requested SSV)` - - Mint on stake, burn on requestUnstake - -7. **Accumulator Monotonicity**: `accEthPerShare` only increases, never decreases - -8. **Oracle Monotonicity**: `latestCommittedBlock` only increases - -9. **Cluster Version Exclusivity**: A cluster key exists in EITHER `s.clusters` OR `s.ethClusters`, never both - -10. **Operator Dual Tracking**: For each operator: `ethValidatorCount == Σ(validatorCount of active ETH clusters using this operator)` - ---- - -## Part 1: Operators + Validators - -### OV-1: Register Operator (Public, Non-Zero Fee) — Initial State Verification - -**Modules Touched:** SSVOperators -**Bug Class Covered:** Incorrect initialization, missing field defaults - -#### Preconditions -- No operators registered -- `sp.minimumOperatorEthFee` = 100_000 (packed: 1) -- `sp.operatorMaxFee` = packed value allowing up to 10 ETH/block - -#### Action Sequence -| Step | Action | Block | Expected State Change | -|------|--------|-------|----------------------| -| 1 | `registerOperator(pubkey, 1_770_000_000, false)` | 100 | Creates operator ID 1 | - -#### Assertions -- [ ] `operator[1].owner == msg.sender` -- [ ] `operator[1].ethFee == PackedETH.wrap(17_700)` (= 1_770_000_000 / 100_000) -- [ ] `operator[1].ethSnapshot.block == 100` -- [ ] `operator[1].ethSnapshot.index == 0` -- [ ] `operator[1].ethSnapshot.balance == PackedETH.wrap(0)` -- [ ] `operator[1].validatorCount == 0` -- [ ] `operator[1].ethValidatorCount == 0` -- [ ] `operator[1].fee == PackedSSV.wrap(0)` (no SSV fee for new operators) -- [ ] `operator[1].snapshot.block == 0` (SSV snapshot NOT initialized) -- [ ] `operator[1].whitelisted == false` -- [ ] `s.operatorsPKs[keccak256(pubkey)] == 1` -- [ ] `s.lastOperatorId.current() == 1` -- [ ] Event: `OperatorAdded(1, msg.sender, pubkey, 1_770_000_000)` -- [ ] Event: `OperatorPrivacyStatusUpdated([1], false)` (per DISC-OV-1) - -#### Edge Variations -- Fee = 0: succeeds, `ethFee == PackedETH.wrap(0)`. Can NEVER increase fee. -- `setPrivate = true`: `whitelisted == true`, event with `true`. -- Same pubkey again: revert `OperatorAlreadyExists`. -- Fee not divisible by 100_000: revert `MaxPrecisionExceeded`. - ---- - -### OV-2: Register Operator (Private, Zero Fee) — Free Operator Constraints - -**Modules Touched:** SSVOperators - -#### Action Sequence -| Step | Action | Block | -|------|--------|-------| -| 1 | `registerOperator(pubkey, 0, true)` | 100 | -| 2 | `declareOperatorFee(1, 500_000)` | 200 | - -#### Assertions -- [ ] Step 1: `operator[1].ethFee == PackedETH.wrap(0)`, `whitelisted == true` -- [ ] Step 2: Reverts `FeeIncreaseNotAllowed` (SSVOperators.sol:115) - ---- - -### OV-3: ensureETHDefaults — Critical Default Fee Assignment - -**Modules Touched:** OperatorLib - -#### Preconditions -- Legacy operator with SSV fee > 0, `ethSnapshot.block == 0`, `ethFee == PackedETH.wrap(0)` - -#### Assertions after first ETH interaction at block 200 -- [ ] `operator.ethFee == PackedETH.wrap(17_700)` (DEFAULT_OPERATOR_ETH_FEE / ETH_DEDUCTED_DIGITS) -- [ ] `operator.ethSnapshot.block == 200` -- [ ] `operator.ethSnapshot.balance == PackedETH.wrap(0)` - -#### Edge Variations -- Legacy operator with SSV fee = 0: ethFee stays 0 (free operator stays free in ETH) -- Already ETH-initialized: no-op - ---- - -### OV-4: Register Validator — New Cluster with 4 Public Operators - -**Modules Touched:** SSVValidators, SSVOperators (via OperatorLib) - -#### Preconditions -- 4 legacy operators (IDs 1-4) with SSV fee > 0, not yet ETH-initialized -- `sp.ethNetworkFee = PackedETH.wrap(35_509)` - -#### Action Sequence -| Step | Action | Block | -|------|--------|-------| -| 1 | `registerValidator{value: 10 ETH}(pubkey, [1,2,3,4], shares, emptyCluster)` | 200 | -| 2 | Advance 100 blocks | 300 | - -#### Assertions After Step 1 (block 200) -- [ ] Each `operator[1..4].ethFee == PackedETH.wrap(17_700)` -- [ ] Each `operator[1..4].ethValidatorCount == 1` -- [ ] Each `operator[1..4].ethSnapshot.block == 200` -- [ ] `sp.ethDaoValidatorCount == 1` -- [ ] `sp.daoTotalEthVUnits == 10_000` -- [ ] `cluster.validatorCount == 1`, `cluster.balance == 10e18`, `cluster.active == true` - -#### Assertions After Step 2 (block 300, after triggering snapshot update) -Per operator earnings (100 blocks): -- `blockDiffEthFee = 100 * 17_700 = 1_770_000` -- `effectiveVUnits = 0 + 1 * 10_000 = 10_000` -- `delta = (1_770_000 * 10_000) / 10_000 = 1_770_000` -- [ ] Each `operator[1..4].ethSnapshot.balance == PackedETH.wrap(1_770_000)` → `177_000_000_000 wei` - -Cluster balance after 100 blocks: -- `operatorIndexDelta = 4 * 1_770_000 = 7_080_000` -- `networkFeeIndexDelta = 100 * 35_509 = 3_550_900` -- `vUnits = 10_000` -- `operatorFeeUnits = (7_080_000 * 10_000) / 10_000 = 7_080_000` -- `networkFeeUnits = (3_550_900 * 10_000) / 10_000 = 3_550_900` -- `totalUsageWei = (7_080_000 + 3_550_900) * 100_000 = 1_063_090_000_000` -- [ ] `cluster.balance == 10e18 - 1_063_090_000_000 = 9_999_998_936_910_000_000` - ---- - -### OV-5: Register Validator — Existing Cluster with Fee Settlement - -**Modules Touched:** SSVValidators, ClusterLib, OperatorLib - -#### Preconditions -- 4 operators, ETH-initialized at block 200, `ethFee = PackedETH.wrap(17_700)` -- Cluster with 1 validator, `balance == 10 ETH`, created at block 200 - -#### Action at block 250: Register 2nd validator with 5 ETH deposit -- Settles 50 blocks of fees at 1-validator rate -- `cluster.balance = 15e18 - 531_545_000_000 = 14_999_999_468_455_000_000` -- Each operator `ethValidatorCount == 2` - ---- - -### OV-6–OV-35: [Remaining OV Scenarios] - -*See `docs/scenarios/operators-validators.md` for the complete detailed scenarios OV-6 through OV-35, covering:* -- OV-6: Private operator whitelist enforcement -- OV-7: Bulk register validators -- OV-8–9: Remove validator (fee settlement, last validator) -- OV-10: Full validator lifecycle (register→advance→remove→withdraw) -- OV-11–12: Fee declaration/execution/reduction with timelock -- OV-13: Operator earnings accumulation with vUnit deviation -- OV-14: Remove operator — full cleanup and final withdrawal -- OV-15: Fee change during active cluster — no gap/double-count -- OV-16: Multi-cluster operator earnings -- OV-17: Operator removal after all validators removed -- OV-18: Combined ETH + SSV withdrawal -- OV-19–21: Revert cases (register, remove, operator remove) -- OV-22: Same-block register and remove -- OV-23: ensureETHDefaults with zero SSV fee -- OV-24: Precision loss in operator earnings -- OV-25: Cluster balance underflow protection -- OV-26: Exit validator (signal only) -- OV-27: DAO network fee earnings consistency -- OV-28: Operator index frozen after removal -- OV-29: Concurrent fee changes on multiple operators -- OV-30: Operator registration then immediate validator registration -- OV-31: 13-operator cluster gas and correctness -- OV-32: Validator registration with explicit EB -- OV-33: Validator removal with explicit EB — deviation cleanup -- OV-34: Bulk remove validators -- OV-35: Deposit and withdraw — no side effects on operator state - ---- - -## Part 2: Clusters + Migration - -### CM-1: ETH Cluster Lifecycle — Create, Deposit, Advance, Withdraw - -**Modules Touched:** SSVValidators, SSVClusters, ClusterLib, OperatorLib, ProtocolLib - -#### Preconditions -- 4 operators, each `ethFee = 1_000_000_000` (packed raw = 10_000) -- Network fee: raw = 5_000 -- `minimumBlocksBeforeLiquidation = 100` - -#### Action Sequence -| Step | Action | Block | -|------|--------|-------| -| 1 | Register validator, 10 ETH | B0 | -| 2 | Deposit 5 ETH | B0+50 | -| 3 | Withdraw 2 ETH | B0+100 | - -#### Assertions -- Step 2: `cluster.balance = 10e18 + 5e18 = 15e18` (NO fee settlement per DISC-OV-8) -- Step 3: Fees settled for 100 blocks: - - `operatorFeeUnits = (4_000_000 * 10_000) / 10_000 = 4_000_000` - - `networkFeeUnits = (500_000 * 10_000) / 10_000 = 500_000` - - `totalFees = (4_000_000 + 500_000) * 100_000 = 450_000_000_000` - - `balanceAfterFees = 15e18 - 450_000_000_000` - - `balanceAfterWithdraw = balanceAfterFees - 2e18 = 12_999_999_550_000_000_000` - ---- - -### CM-2: Withdraw Exactly To Liquidation Threshold (Boundary) - -**Bug Class:** Off-by-one in `<` vs `<=` boundary - -#### Key Assertion -- `isLiquidatableWithEB` uses `cluster.balance < liquidationThreshold` (strict less-than) -- `balance == threshold` → NOT liquidatable → withdrawal succeeds -- `balance == threshold - 1` → liquidatable → withdrawal reverts `InsufficientBalance` - ---- - -### CM-3: Third-Party Liquidation With Bounty Verification - -#### Preconditions -- 1 validator, deposit = 1e12 wei, per-block burn = 4_500_000_000 - -#### Assertions at block B0+123 (liquidatable): -- `balanceAfterFees = 1e12 - 553_500_000_000 = 446_500_000_000` -- `threshold = 450_000_000_000` -- `446_500_000_000 < 450_000_000_000` → liquidatable -- [ ] Liquidator receives exactly 446_500_000_000 wei -- [ ] `cluster.active == false`, `cluster.balance == 0` -- [ ] Operator `ethValidatorCount` decremented BEFORE balance zeroed (per DISC-CM-4) - ---- - -### CM-4–CM-30: [Remaining CM Scenarios] - -*See `docs/scenarios/clusters-migration.md` for complete detailed scenarios CM-4 through CM-30, covering:* -- CM-4: SSV self-liquidation with SSV balance return -- CM-5: Basic SSV→ETH migration with SSV refund -- CM-6: Migration of liquidated SSV cluster -- CM-7: Migration with mixed operator ETH state -- CM-8: Post-migration ETH fee accrual -- CM-9: Reactivation after liquidation -- CM-10: Deposit into liquidated cluster + reactivation -- CM-11: SSV blocked operations verification -- CM-12: Explicit EB fee scaling -- CM-13: Migration with EB deviation sync -- CM-14: Liquidation with EB deviation cleanup -- CM-15: Auto-liquidation via updateClusterBalance -- CM-16: Conservation law — multi-cluster ETH balance tracking -- CM-17: SSV fee accrual precision -- CM-18: SSV refund after extended accrual -- CM-19: Withdraw from empty cluster (validatorCount == 0) -- CM-20: Reactivation with explicit EB deviation restoration -- CM-21: Liquidation boundary (`<` not `<=`) -- CM-22: Migration with removed operator -- CM-23: Withdraw doesn't update operator snapshots -- CM-24: Packing precision enforcement -- CM-25: updateClusterBalance on SSV cluster (EB snapshot only) -- CM-26: Liquidation bounty = post-settlement balance -- CM-27: DAO earnings settlement during migration -- CM-28: Multiple migrations — same operators -- CM-29: Migration with insufficient ETH (boundary) -- CM-30: Full end-to-end lifecycle with conservation proof - ---- - -## Part 3: Effective Balance + Staking - -### ES-1: Single Oracle Commit — Below Quorum - -**Modules Touched:** SSVDAO - -#### Preconditions -- 4 oracles, `quorumBps = 7500`, `cSSV.totalSupply() = 40e9` - -#### Assertions -- `weight = 40e9 / 4 = 10e9` -- `threshold = 40e9 * 7500 / 10_000 = 30e9` -- `10e9 < 30e9` → quorum NOT reached -- [ ] `ebRoots[100] == bytes32(0)`, `latestCommittedBlock` unchanged - ---- - -### ES-2: Quorum Reached — 3 of 4 Oracles - -#### Assertions -- 3 oracles vote → accumulated = 30e9 = threshold → quorum reached -- [ ] `ebRoots[100] == rootA`, `latestCommittedBlock == 100` -- [ ] `rootCommitments[commitKey] == 0` (deleted) -- [ ] `hasVoted` preserved (prevents re-voting) - ---- - -### ES-6: First EB Update — Implicit to Explicit (Same vUnits) - -#### Preconditions -- 2 validators, implicit vUnits = 20_000, EB update to 64 ETH - -#### Key Assertion -- `newVUnits = ebToVUnits(64) = ceil(64 * 10_000 / 32) = 20_000` -- `effectiveOldVUnits = 20_000` (implicit = validatorCount * BPS_DENOMINATOR) -- `newVUnits == effectiveOldVUnits` → NO deviation change -- [ ] Cluster now has explicit EB, future updates use stored value as baseline - ---- - -### ES-7: EB Increase — Higher Fee Burn Rate - -#### Preconditions -- 2 validators, prior explicit vUnits = 20_000, update to 96 ETH at block 300 - -#### Assertions -- `newVUnits = 30_000` -- Fee settlement uses OLD vUnits (20_000) for blocks 200-300 -- After: each `operatorEthVUnits[i] += 10_000` (FULL delta per operator, per DISC-ES-6) -- Future fees scale at 1.5× rate (30_000 / 20_000) - ---- - -### ES-9: Auto-Liquidation on EB Increase - -#### Key Flow -- Cluster balance just above threshold at 20_000 vUnits -- EB doubles to 40_000 vUnits → threshold doubles → cluster liquidatable -- `_liquidateAfterEBUpdateIfNeeded` triggers auto-liquidation -- Bounty goes to caller of `updateClusterBalance` (not cluster owner) - ---- - -### ES-15: Basic Stake → Earn → Claim Cycle - -#### Preconditions -- 1 cluster with 1 validator generating network fees -- User stakes 10e18 SSV at block 1000 - -#### Assertions -- Pre-stake fees (blocks 0-1000) are NOT claimable (totalSupply was 0) -- User earns only blocks 1000-1100 fees -- `accEthPerShare += (newFeesWei * 1e18) / 10e18` -- Payout truncated to nearest 100_000 wei (dust stays in accrued) - ---- - -### ES-17: Stake Timing — Late Joiner - -#### Steps -- User A stakes 10e18 SSV at block 0 -- User B stakes 30e18 SSV at block 50 -- Both claim at block 100 - -#### Math with f = wei/block: -- A: `62.5f` (100% of blocks 0-50 + 25% of blocks 50-100) -- B: `37.5f` (75% of blocks 50-100) -- Sum = 100f = total fees - ---- - -### ES-3–ES-32: [Remaining ES Scenarios] - -*See `docs/scenarios/eb-staking.md` for complete detailed scenarios covering:* -- ES-3: Conflicting oracle roots -- ES-4: Oracle replacement mid-vote -- ES-5: Oracle revert cases -- ES-8: EB decrease -- ES-10: Fee settlement uses OLD vUnits (no gap proof) -- ES-11: Operator vUnit tracking across multiple clusters -- ES-12: EB limits enforcement (min/max) -- ES-13: Merkle proof verification -- ES-14: Update frequency and staleness -- ES-16: Multiple stakers — pro-rata distribution -- ES-18: Unstake request → cooldown → withdraw -- ES-19: cSSV transfer settles rewards -- ES-20: Accumulator edge cases (zero supply, monotonicity, dust) -- ES-21: MAX_PENDING_REQUESTS (10) -- ES-22: MINIMAL_STAKING_AMOUNT -- ES-23: syncFees() public function -- ES-24: EB increase → higher staking rewards -- ES-25: Auto-liquidation reduces staking revenue -- ES-26: EB update on SSV cluster (snapshot only) -- ES-27–28: Full staking reward math with precision -- ES-29: requestUnstake + immediate claim -- ES-30: cSSV transfer — mint/burn do NOT trigger hook -- ES-31: Staking with pre-existing DAO balance -- ES-32: EB update → syncFees full chain trace - ---- - -## Part 4: Cross-Cutting Flows - -These scenarios test interactions between 3+ modules that no individual partition test can cover. - ---- - -### CC-1: Full Economic Conservation Law - -**Modules Touched:** SSVOperators, SSVValidators, SSVClusters, SSVDAO, SSVStaking, ProtocolLib -**Bug Class Covered:** Value creation/destruction — the master invariant - -#### Preconditions -- 4 operators registered at block 0 with `ethFee = 2_000_000_000` (packed = 20_000) -- `sp.ethNetworkFee = PackedETH.wrap(10_000)` (= 1_000_000_000 wei/block) -- 1 staker with 10e18 SSV staked → 10e18 cSSV - -#### Action Sequence -| Step | Action | Block | ETH In/Out | -|------|--------|-------|------------| -| 1 | Register validator, 10 ETH deposit | 100 | +10 ETH | -| 2 | Register 2nd validator, 5 ETH deposit | 200 | +5 ETH | -| 3 | Advance 100 blocks | 300 | — | -| 4 | Withdraw 1 ETH from cluster | 300 | -1 ETH | -| 5 | Operator 1 withdraws all ETH earnings | 300 | -op1_earnings ETH | - -#### Conservation Check at Each Step - -**After Step 1 (block 100):** -- Contract ETH = 10 ETH -- Cluster balance (stored) = 10 ETH -- Operator earnings (stored) = 0 (just initialized) -- DAO earnings (stored) = 0 -- Staking pool = 0 -- **10e18 == 10e18 + 0 + 0 + 0** ✓ - -**After Step 3 (block 300, before any withdrawals):** -- Contract ETH = 15 ETH (10 + 5 deposited, nothing withdrawn) -- All fees are "pending" — cluster stored balance is still at 15e18 (deposit didn't settle fees) -- Operator stored earnings = 0 (operators haven't been snapshot-updated since step 2) - -But the invariant uses STORED values: -- `contract.ETH (15e18) >= cluster.stored_balance (15e18) + Σ(op.stored_earnings) (0) + stored_DAO_earnings (0) + staking_pool (0)` -- `15e18 >= 15e18` ✓ - -**After Step 4 (block 300, withdraw settles cluster fees):** -- Withdraw triggers fee settlement for the cluster -- Cluster balance = 15e18 - totalFees - 1e18 -- Fees computed inline, NOT written to operator storage (per DISC-CM-3) -- Contract ETH = 15e18 - 1e18 = 14e18 - -Check: -- `14e18 >= cluster.new_stored_balance + Σ(op.stored_earnings=0) + stored_DAO_earnings + staking_pool` -- The gap between contract.ETH and stored values = unsettled operator/DAO earnings -- This is why the invariant uses `>=` not `==` - -**After Step 5 (block 300, operator withdrawal):** -- `withdrawAllOperatorEarnings(1)` calls `updateSnapshotSt` → settles operator 1 earnings -- Operator 1 earnings for blocks 100-300 (200 blocks): - - Blocks 100-200: 1 validator → effectiveVUnits = 10_000 - - `delta = (100 * 20_000 * 10_000) / 10_000 = 2_000_000` - - Blocks 200-300: 2 validators → effectiveVUnits = 20_000 - - `delta = (100 * 20_000 * 20_000) / 10_000 = 4_000_000` - - Total: `6_000_000` packed → `600_000_000_000 wei` -- Contract ETH = 14e18 - 600_000_000_000 - -#### Master Conservation Formula -At any settled point: -``` -contract.ETH_balance == Σ(active_cluster.stored_balance) - + Σ(operator.ethSnapshot.balance_unpacked) - + sp.ethDaoBalance_unpacked - + staking_pool_balance - + precision_dust (≥ 0) -``` - -Assertions: -- [ ] Conservation holds after EVERY step (with `>=`) -- [ ] After ALL earnings are withdrawn and settled, conservation holds with `==` (modulo precision dust) -- [ ] Precision dust never exceeds `N_operations * ETH_DEDUCTED_DIGITS` (each operation can lose at most 99_999 wei) - ---- - -### CC-2: Register → Advance → Verify Full Economics (Exact Numbers) - -**Modules Touched:** SSVValidators, SSVClusters, SSVOperators, ProtocolLib -**Bug Class Covered:** End-to-end fee accounting correctness - -#### Preconditions -- 4 operators (IDs 1-4), public, registered at block 0 with `ethFee = 2_000_000_000` (packed = 20_000) -- `sp.ethNetworkFee = PackedETH.wrap(10_000)` (= 1_000_000_000 wei/block) -- `sp.ethNetworkFeeIndex = 0`, `sp.ethNetworkFeeIndexBlockNumber = 0` - -#### Action Sequence -| Step | Action | Block | -|------|--------|-------| -| 1 | `registerValidator{value: 10 ETH}(pk, [1,2,3,4], shares, emptyCluster)` | 100 | -| 2 | Advance 100 blocks | 200 | -| 3 | Trigger full settlement (e.g., `removeValidator` or explicit `withdraw(0)`) | 200 | - -#### Exact Math After 100 Blocks (Step 3) - -**Each operator's ETH earnings:** -- `blockDiffEthFee = 100 * 20_000 = 2_000_000` -- `effectiveVUnits = 0 + 1 * 10_000 = 10_000` -- `delta = (2_000_000 * 10_000) / 10_000 = 2_000_000` -- Each operator earns: `2_000_000 * 100_000 = 200_000_000_000 wei` -- 4 operators total: `800_000_000_000 wei` - -**Cluster balance deduction:** -- `operatorIndexDelta = 4 * 2_000_000 = 8_000_000` -- `networkFeeIndexDelta = 100 * 10_000 = 1_000_000` -- `vUnits = 10_000` -- `operatorFeeUnits = (8_000_000 * 10_000) / 10_000 = 8_000_000` -- `networkFeeUnits = (1_000_000 * 10_000) / 10_000 = 1_000_000` -- `totalFees = (8_000_000 + 1_000_000) * 100_000 = 900_000_000_000` -- `cluster.balance = 10e18 - 900_000_000_000 = 9_999_999_100_000_000_000` - -**DAO ETH earnings (network fee portion):** -- `networkTotalEarnings = ethDaoBalance + (blockDiff * networkFee * daoTotalEthVUnits) / BPS_DENOMINATOR` -- `= 0 + (100 * 10_000 * 10_000) / 10_000 = 1_000_000` packed -- `= 1_000_000 * 100_000 = 100_000_000_000 wei` - -**Conservation check:** -``` -cluster.balance = 9_999_999_100_000_000_000 -operator_earnings = 4 * 200_000_000_000 = 800_000_000_000 -DAO_earnings = 100_000_000_000 -Sum = 9_999_999_100_000_000_000 + 800_000_000_000 + 100_000_000_000 - = 10_000_000_000_000_000_000 = 10 ETH ✓ -``` - -#### Assertions -- [ ] Each operator earns exactly `200_000_000_000 wei` -- [ ] Cluster balance = `9_999_999_100_000_000_000` -- [ ] DAO earnings = `100_000_000_000 wei` -- [ ] Sum == 10 ETH (exact conservation, no precision loss in this case) - ---- - -### CC-3: Migration → Register → EB Update → Fee Change → Liquidation - -**Modules Touched:** SSVClusters, SSVValidators, SSVOperators, SSVDAO, OperatorLib, ClusterLib, ProtocolLib -**Bug Class Covered:** Multi-step state transitions with exact accounting at each phase - -#### Preconditions -- 4 operators (IDs 1-4), SSV fee > 0 (packed raw = 1_000), ETH not yet initialized -- SSV cluster: 2 validators, balance = 100e18 SSV, created at block 0 -- `sp.ssvNetworkFee` raw = 500 -- `sp.ethNetworkFee = PackedETH.wrap(10_000)` (1e9 wei/block) -- `minimumBlocksBeforeLiquidation = 100` -- `DEFAULT_OPERATOR_ETH_FEE = 1_770_000_000` → packed = 17_700 - -#### Action Sequence -| Step | Action | Block | -|------|--------|-------| -| 1 | Migrate SSV cluster to ETH with `msg.value = 5 ETH` | 500 | -| 2 | Register 3rd validator, deposit 0 ETH | 600 | -| 3 | Oracle commits EB root, then `updateClusterBalance(EB=192)` | 700 | -| 4 | Operator 1 declares fee 2_000_000_000 (packed 20_000) | 700 | -| 5 | Operator 1 executes fee at block 800 (within timelock) | 800 | -| 6 | Advance until cluster approaches liquidation | ~5500 | -| 7 | Third-party liquidates | ~5500 | -| 8 | Operator 1 withdraws all earnings | 5500 | - -#### Step 1: Migration at block 500 - -**SSV fee settlement (500 blocks):** -- `operatorIndexDelta = 4 * 500 * 1_000 = 2_000_000` -- `networkFeeIndexDelta = 500 * 500 = 250_000` -- `usage_packed = 2_000_000 * 2 + 250_000 * 2 = 4_500_000` -- `usage_unpacked = 4_500_000 * 10_000_000 = 45_000_000_000_000` -- `ssvRefund = 100e18 - 45_000_000_000_000 = 99_999_955_000_000_000_000` -- [ ] Owner receives 99_999_955_000_000_000_000 SSV tokens - -**ETH cluster setup:** -- All 4 operators: `ensureETHDefaults()` → `ethFee = PackedETH.wrap(17_700)` -- `cluster.balance = 5e18`, `cluster.index = 0` (all operators are ETH-new) -- Each operator `ethValidatorCount = 2` -- `sp.ethDaoValidatorCount = 2`, `sp.daoTotalEthVUnits = 20_000` - -#### Step 2: Register 3rd validator at block 600 - -**Fee settlement (100 blocks at 2 validators):** -- Each operator: `blockDiffEthFee = 100 * 17_700 = 1_770_000`, `effectiveVUnits = 20_000` - - `delta = (1_770_000 * 20_000) / 10_000 = 3_540_000` -- `opIndexDelta = 4 * 1_770_000 = 7_080_000` -- `netIndexDelta = 100 * 10_000 = 1_000_000` -- `opFeeUnits = (7_080_000 * 20_000) / 10_000 = 14_160_000` -- `netFeeUnits = (1_000_000 * 20_000) / 10_000 = 2_000_000` -- `totalFees = (14_160_000 + 2_000_000) * 100_000 = 1_616_000_000_000` -- `cluster.balance = 5e18 - 1_616_000_000_000 = 4_999_998_384_000_000_000` -- After: `validatorCount = 3`, each operator `ethValidatorCount = 3` -- `sp.daoTotalEthVUnits = 30_000`, `sp.ethDaoValidatorCount = 3` - -#### Step 3: EB Update to 192 ETH at block 700 - -- `newVUnits = ebToVUnits(192) = ceil(192 * 10_000 / 32) = 60_000` -- `effectiveOldVUnits = 30_000` (implicit: 3 * 10_000) - -**Fee settlement (100 blocks at OLD vUnits = 30_000):** -- Each operator: `blockDiffEthFee = 100 * 17_700 = 1_770_000`, `effectiveVUnits = 0 + 3 * 10_000 = 30_000` - - `delta = (1_770_000 * 30_000) / 10_000 = 5_310_000` -- `opIndexDelta = 4 * 1_770_000 = 7_080_000` -- `netIndexDelta = 100 * 10_000 = 1_000_000` -- `opFeeUnits = (7_080_000 * 30_000) / 10_000 = 21_240_000` -- `netFeeUnits = (1_000_000 * 30_000) / 10_000 = 3_000_000` -- `totalFees = (21_240_000 + 3_000_000) * 100_000 = 2_424_000_000_000` -- `cluster.balance = 4_999_998_384_000_000_000 - 2_424_000_000_000 = 4_999_995_960_000_000_000` - -**vUnit update:** -- `deviation = 60_000 - 30_000 = 30_000` -- Each `operatorEthVUnits[i] += 30_000` (full delta per operator!) -- `sp.daoTotalEthVUnits += 30_000` → now 60_000 -- `ebSnapshot = {vUnits: 60_000, ...}` - -#### Steps 4-5: Fee change -- Operator 1 declares fee increase to 20_000 packed, executes at block 800 -- Earnings from 700-800 settled at OLD fee 17_700 before fee change - -#### Steps 6-7: Liquidation (approximate) -- New per-block burn with 60_000 vUnits: `burnRate = 4 * 17_700 + 1 * (20_000 - 17_700) = 72_600` (op1 at 20_000, others at 17_700) - - Actually, `burnRate` is the cumulativeFee for liquidation check, but vUnits scaling changes the threshold -- The cluster balance decreases until liquidatable -- Bounty = remaining balance after fee settlement - -#### Assertions -- [ ] SSV refund exact at step 1 -- [ ] ETH conservation at every step -- [ ] Fee settlement uses OLD vUnits before EB update -- [ ] Operator deviation = 30_000 per operator (full delta, not divided) -- [ ] Liquidation bounty is exact post-settlement balance -- [ ] After operator withdrawal, total withdrawn matches cumulative earnings - ---- - -### CC-4: Multi-Staker Revenue Distribution Through State Changes - -**Modules Touched:** SSVStaking, SSVClusters, ProtocolLib, CSSVToken -**Bug Class Covered:** Staking accumulator correctness across multiple phases - -#### Preconditions -- 1 ETH cluster: 1 validator, 4 operators at `ethFee = PackedETH.wrap(20_000)` -- `sp.ethNetworkFee = PackedETH.wrap(10_000)` -- `sp.daoTotalEthVUnits = 10_000` - -#### Action Sequence -| Step | Action | Block | -|------|--------|-------| -| 1 | User A stakes 100e18 SSV | 0 | -| 2 | Advance 50 blocks | 50 | -| 3 | User B stakes 300e18 SSV | 50 | -| 4 | Advance 50 blocks | 100 | -| 5 | EB update doubles vUnits (64 ETH for 1 validator → vUnits = 20_000) | 100 | -| 6 | Advance 50 blocks | 150 | -| 7 | User A claims | 150 | -| 8 | User B claims | 150 | - -#### DAO Earnings Per Block - -**Phase 1 (blocks 0-50, vUnits = 10_000):** -- `earningsPerBlock (packed) = (1 * 10_000 * 10_000) / 10_000 = 10_000` -- `earningsPerBlock (wei) = 10_000 * 100_000 = 1_000_000_000` - -**Phase 2 (blocks 50-100, vUnits = 10_000):** -- Same: `1_000_000_000 wei/block` - -**Phase 3 (blocks 100-150, vUnits = 20_000):** -- `earningsPerBlock (packed) = (1 * 10_000 * 20_000) / 10_000 = 20_000` -- `earningsPerBlock (wei) = 20_000 * 100_000 = 2_000_000_000` - -#### Staking Math - -**At block 0 (User A stakes 100e18):** -- `_syncFees`: no prior fees (block 0). If `ethDaoBalance = 0` and `ethDaoIndexBlockNumber = 0`: - - `current = 0 + (0 * 10_000 * 10_000) / 10_000 = 0` - - No new fees → `accEthPerShare = 0` -- `userIndex[A] = 0` -- `cSSV.totalSupply() = 100e18` - -**At block 50 (User B stakes 300e18):** -- `_syncFees`: - - `current = 0 + (50 * 10_000 * 10_000) / 10_000 = 500_000` packed - - `previous = 0` - - `newFeesWei = 500_000 * 100_000 = 50_000_000_000` - - `accEthPerShare += (50_000_000_000 * 1e18) / 100e18 = 500_000_000` -- `_settle(B)`: `bal = 0` → no-op, `userIndex[B] = 500_000_000` -- Mint 300e18 cSSV → `totalSupply = 400e18` - -**At block 100 (EB update — `_syncFees` NOT called by updateClusterBalance, only by staking functions):** -- EB update modifies `daoTotalEthVUnits = 20_000` -- But `_syncFees` is NOT called here — stakers need to explicitly interact - -**At block 150 (User A claims):** -- `_syncFees`: - - DAO earnings from block 50 to 150: - - Blocks 50-100: `50 * 10_000 * 10_000 / 10_000 = 500_000` packed - - But wait: `updateDAOEarnings` was called at block 100 (during EB update's `updateDAOEthVUnits`) - - So `ethDaoBalance` at block 100 = `500_000 + 500_000 = 1_000_000` packed, `ethDaoIndexBlockNumber = 100` - - From block 100 to 150: `50 * 10_000 * 20_000 / 10_000 = 1_000_000` packed - - `current = 1_000_000 + 1_000_000 = 2_000_000` packed - - `previous = stakingEthPoolBalance = 500_000` (set at block 50) - - `packedNewFees = 2_000_000 - 500_000 = 1_500_000` - - `newFeesWei = 1_500_000 * 100_000 = 150_000_000_000` - - `accEthPerShare += (150_000_000_000 * 1e18) / 400e18 = 375_000_000` - - Total `accEthPerShare = 500_000_000 + 375_000_000 = 875_000_000` - -- `_settle(A)`: - - `bal = 100e18` (A's cSSV balance) - - `pending = (100e18 * (875_000_000 - 0)) / 1e18 = 87_500_000_000` - - `accrued[A] = 87_500_000_000` - -**User A's claimed rewards:** -- Phase 1 (blocks 0-50): A was sole staker → 100% of 50_000_000_000 = `50_000_000_000` -- Phase 2 (blocks 50-100): A has 100e18 / 400e18 = 25% of 50_000_000_000 = `12_500_000_000` -- Phase 3 (blocks 100-150): A has 25% of 100_000_000_000 = `25_000_000_000` -- Total A: `50_000_000_000 + 12_500_000_000 + 25_000_000_000 = 87_500_000_000` ✓ - -**At block 150 (User B claims):** -- `_syncFees`: no new blocks → no change -- `_settle(B)`: - - `pending = (300e18 * (875_000_000 - 500_000_000)) / 1e18 = 300 * 375_000_000 = 112_500_000_000` - - `accrued[B] = 112_500_000_000` - -**User B's claimed rewards:** -- Phase 2: B has 75% of 50_000_000_000 = `37_500_000_000` -- Phase 3: B has 75% of 100_000_000_000 = `75_000_000_000` -- Total B: `37_500_000_000 + 75_000_000_000 = 112_500_000_000` ✓ - -**Conservation:** `87_500_000_000 + 112_500_000_000 = 200_000_000_000` = total fees for 150 blocks ✓ - -#### Assertions -- [ ] User A gets exactly `87_500_000_000 wei` (100% of phase 1, 25% of phases 2+3) -- [ ] User B gets exactly `112_500_000_000 wei` (75% of phases 2+3) -- [ ] Sum = total DAO earnings for 150 blocks -- [ ] EB update at block 100 correctly doubles DAO earning rate from block 100 onward -- [ ] `accEthPerShare` only increases (monotonic) - ---- - -### CC-5: Operator Serving Multiple Clusters with Different EBs - -**Modules Touched:** SSVClusters, SSVOperators, OperatorLib, SSVStorageEB -**Bug Class Covered:** Operator vUnit deviation accumulation across clusters - -#### Preconditions -- Operator O (ID=1) serves: - - Cluster A: 2 validators, operators [1,2,3,4], registered at block 0 - - Cluster B: 3 validators, operators [1,2,3,4], registered at block 0 -- `ethFee = PackedETH.wrap(20_000)` (2e9 wei/block) for all operators -- `sp.ethNetworkFee = PackedETH.wrap(10_000)` -- `operatorEthVUnits[1] = 0` initially - -#### Action Sequence -| Step | Action | Block | -|------|--------|-------| -| 1 | EB update Cluster A: 96 ETH (2 validators) → vUnits = 30_000 | 100 | -| 2 | EB update Cluster B: 128 ETH (3 validators) → vUnits = 40_000 | 100 | -| 3 | Advance 100 blocks | 200 | -| 4 | Liquidate Cluster A | 200 | -| 5 | Advance 100 blocks | 300 | - -#### Step 1: Cluster A EB update -- `effectiveOldVUnits = 2 * 10_000 = 20_000` (implicit) -- `newVUnits = ebToVUnits(96) = ceil(96 * 10_000 / 32) = 30_000` -- Deviation = `30_000 - 20_000 = 10_000` -- Each `operatorEthVUnits[i] += 10_000` -- O's `operatorEthVUnits[1] = 10_000` - -#### Step 2: Cluster B EB update -- `effectiveOldVUnits = 3 * 10_000 = 30_000` (implicit) -- `newVUnits = ebToVUnits(128) = ceil(128 * 10_000 / 32) = 40_000` -- Deviation = `40_000 - 30_000 = 10_000` -- Each `operatorEthVUnits[i] += 10_000` -- O's `operatorEthVUnits[1] = 10_000 + 10_000 = 20_000` - -#### Step 3: Operator earnings for 100 blocks (100-200) -- O's `ethValidatorCount = 2 + 3 = 5` -- `effectiveVUnits = 20_000 + 5 * 10_000 = 70_000` -- `blockDiffEthFee = 100 * 20_000 = 2_000_000` -- `delta = (2_000_000 * 70_000) / 10_000 = 14_000_000` -- O earns: `14_000_000 * 100_000 = 1_400_000_000_000 wei` - -#### Step 4: Liquidate Cluster A -- `updateClusterOperators` called → settles operator snapshots up to block 200 (already settled in step 3 calc) -- O's `ethValidatorCount -= 2` → `ethValidatorCount = 3` -- `_executeLiquidation`: - - `sp.updateDAO(false, 2)` → `sp.daoTotalEthVUnits -= 20_000` (baseline) - - `vUnitsCluster = 30_000`, `baseline = 20_000`, deviation = 10_000 - - `sp.daoTotalEthVUnits -= 10_000` (deviation) - - `operatorEthVUnits[1] -= 10_000` → now 10_000 - -#### Step 5: Earnings for blocks 200-300 (after liquidation) -- O's `ethValidatorCount = 3` (only Cluster B) -- `effectiveVUnits = 10_000 + 3 * 10_000 = 40_000` -- `delta = (2_000_000 * 40_000) / 10_000 = 8_000_000` -- O earns: `8_000_000 * 100_000 = 800_000_000_000 wei` - -#### Assertions -- [ ] After step 2: O's `operatorEthVUnits[1] == 20_000` (sum of both deviations) -- [ ] After step 2: O's `effectiveVUnits = 70_000` (20_000 deviation + 5 * 10_000 baseline) -- [ ] After step 4: O's `operatorEthVUnits[1] == 10_000` (Cluster A deviation removed) -- [ ] After step 4: O's `effectiveVUnits = 40_000` (10_000 deviation + 3 * 10_000) -- [ ] Earnings rate decreased correctly: 1.4e12/100 blocks → 0.8e12/100 blocks -- [ ] `sp.daoTotalEthVUnits` correctly tracks: started at 50_000, +20_000 (both deviations), -30_000 (liquidation) = 40_000 - ---- - -### CC-6: Staking Rewards Through Liquidation Event - -**Modules Touched:** SSVStaking, SSVClusters, ProtocolLib -**Bug Class Covered:** Clean transition of staking rewards when cluster count changes - -#### Preconditions -- 2 clusters: Cluster A (1 validator), Cluster B (1 validator) -- `sp.daoTotalEthVUnits = 20_000`, `sp.ethNetworkFee = PackedETH.wrap(10_000)` -- 1 staker with 10e18 cSSV -- `accEthPerShare = 0`, block 0 - -#### Action Sequence -| Step | Action | Block | -|------|--------|-------| -| 1 | Advance 100 blocks | 100 | -| 2 | Cluster A gets liquidated | 100 | -| 3 | Advance 100 blocks | 200 | -| 4 | Staker claims | 200 | - -#### Math - -**Phase 1 (blocks 0-100, 2 clusters active, daoTotalEthVUnits = 20_000):** -- `earningsPerBlock = (1 * 10_000 * 20_000) / 10_000 = 20_000` packed -- Total: `100 * 20_000 = 2_000_000` packed - -**At block 100 (liquidation):** -- `_executeLiquidation` → `sp.updateDAO(false, 1)`: - - `updateDAOEarnings(sp)` called FIRST: - - `sp.ethDaoBalance = 0 + (100 * 10_000 * 20_000) / 10_000 = 2_000_000` packed - - `sp.ethDaoIndexBlockNumber = 100` - - Then: `sp.ethDaoValidatorCount -= 1`, `sp.daoTotalEthVUnits -= 10_000` → now 10_000 - -**Phase 2 (blocks 100-200, 1 cluster active, daoTotalEthVUnits = 10_000):** -- `earningsPerBlock = (1 * 10_000 * 10_000) / 10_000 = 10_000` packed -- Total: `100 * 10_000 = 1_000_000` packed - -**At block 200 (staker claims):** -- `_syncFees`: - - `current = 2_000_000 + (100 * 10_000 * 10_000) / 10_000 = 2_000_000 + 1_000_000 = 3_000_000` packed - - `previous = 0` - - `newFeesWei = 3_000_000 * 100_000 = 300_000_000_000` - - `accEthPerShare += (300_000_000_000 * 1e18) / 10e18 = 30_000_000_000` -- `_settle(staker)`: - - `pending = (10e18 * 30_000_000_000) / 1e18 = 300_000_000_000` - -#### Assertions -- [ ] Staker receives `300_000_000_000 wei` total -- [ ] This equals: 100 blocks × 2e10/block + 100 blocks × 1e10/block = 2e12 + 1e12 = 3e11... wait - - `100 * 20_000 * 100_000 = 200_000_000_000` (phase 1) - - `100 * 10_000 * 100_000 = 100_000_000_000` (phase 2) - - Total = `300_000_000_000` ✓ -- [ ] DAO earnings settled at exact liquidation block (no gap) -- [ ] daoTotalEthVUnits decreased at liquidation → lower earning rate phase 2 -- [ ] No phantom rewards from liquidated cluster after block 100 -- [ ] `accEthPerShare` monotonically increases - ---- - -### CC-7: Migration Race — Two Clusters, Same Operators - -**Modules Touched:** SSVClusters, OperatorLib, ProtocolLib -**Bug Class Covered:** Operator ETH state correctness after sequential migrations - -#### Preconditions -- Operators 1-4: SSV fee > 0 (`fee = PackedSSV.wrap(1_000)`), no ETH state -- Cluster A: [1,2,3,4], 1 validator, balance = 50e18 SSV -- Cluster B: [1,2,3,4], 2 validators, balance = 80e18 SSV -- Both created at block 0 - -#### Action Sequence -| Step | Action | Block | -|------|--------|-------| -| 1 | Migrate Cluster A with 5 ETH | 100 | -| 2 | Migrate Cluster B with 10 ETH | 200 | - -#### Step 1: Migrate Cluster A - -**For each operator (in `updateClusterOperatorsMigration`):** -- SSV snapshot updated, `validatorCount -= 1` -- `ethSnapshot.block == 0` → `ensureETHDefaults()`: - - `ethSnapshot.block = 100`, `ethFee = PackedETH.wrap(17_700)` -- `ethValidatorCount += 1` → `ethValidatorCount = 1` -- `cumulativeIndexETH = 0` (newly initialized, index = 0) - -**Cluster A ETH state:** -- `cluster.index = 0`, `cluster.balance = 5e18` - -#### Step 2: Migrate Cluster B (100 blocks later) - -**For each operator:** -- SSV snapshot updated, `validatorCount -= 2` -- `ethSnapshot.block != 0` (set at block 100) → take `else` branch: - - `updateSnapshotSt(operator, id)`: - - `blockDiffEthFee = (200 - 100) * 17_700 = 1_770_000` - - `effectiveVUnits = 0 + 1 * 10_000 = 10_000` (1 validator from Cluster A) - - `delta = (1_770_000 * 10_000) / 10_000 = 1_770_000` - - `ethSnapshot.balance += PackedETH.wrap(1_770_000)` - - `ethSnapshot.index += 1_770_000` - - `cumulativeIndexETH += operator.ethSnapshot.index` (= 1_770_000 per operator) -- `ethValidatorCount += 2` → `ethValidatorCount = 3` -- `cumulativeFeeETH = 4 * 17_700 = 70_800` - -**Cluster B ETH state:** -- `cluster.index = 4 * 1_770_000 = 7_080_000` (non-zero! captures existing indices) -- `cluster.balance = 10e18` - -#### Assertions -- [ ] After step 1: each operator `ethValidatorCount == 1`, `ethSnapshot.block == 100` -- [ ] After step 1: NO `ensureETHDefaults()` needed at step 2 (already initialized) -- [ ] After step 2: each operator `ethValidatorCount == 3` (not double-counted) -- [ ] After step 2: Cluster B's `cluster.index == 7_080_000` (captures 100 blocks of earnings) -- [ ] After step 2: operators earned 100 blocks of fees from Cluster A's 1 validator -- [ ] No double-counting of validators across migrations - ---- - -### CC-8: cSSV Transfer Mid-Revenue-Accrual - -**Modules Touched:** CSSVToken, SSVStaking, ProtocolLib -**Bug Class Covered:** Transfer hook correctly settles both parties at pre-transfer balances - -#### Preconditions -- User A: 100e18 cSSV, User B: 0 cSSV -- 1 cluster generating network fees at `10_000` packed/block → `1_000_000_000 wei/block` -- `sp.daoTotalEthVUnits = 10_000` -- `accEthPerShare = 0`, block 0 - -#### Action Sequence -| Step | Action | Block | -|------|--------|-------| -| 1 | Revenue accrues 50 blocks | 50 | -| 2 | A transfers 50e18 cSSV to B | 50 | -| 3 | Revenue accrues 50 more blocks | 100 | -| 4 | A claims | 100 | -| 5 | B claims | 100 | - -#### Math - -**DAO earnings per block:** `(1 * 10_000 * 10_000) / 10_000 = 10_000` packed → `1_000_000_000 wei` - -**At block 50 (transfer triggers `onCSSVTransfer`):** -- `_syncFees`: - - `current = 50 * 10_000 = 500_000` packed - - `newFeesWei = 500_000 * 100_000 = 50_000_000_000` - - `accEthPerShare += (50_000_000_000 * 1e18) / 100e18 = 500_000_000` -- `_settle(A)`: - - `bal = cSSV.balanceOf(A) = 100e18` (PRE-TRANSFER balance!) - - `pending = (100e18 * 500_000_000) / 1e18 = 50_000_000_000` - - `accrued[A] = 50_000_000_000` - - `userIndex[A] = 500_000_000` -- `_settle(B)`: - - `bal = cSSV.balanceOf(B) = 0` (PRE-TRANSFER!) - - `pending = 0` - - `userIndex[B] = 500_000_000` -- Then ERC20 transfer: A has 50e18 cSSV, B has 50e18 cSSV - -**At block 100 (A claims):** -- `_syncFees`: - - `newFeesWei = 50_000_000_000` (50 more blocks) - - `accEthPerShare += (50_000_000_000 * 1e18) / 100e18 = 500_000_000` - - Total `accEthPerShare = 1_000_000_000` -- `_settle(A)`: - - `pending = (50e18 * (1_000_000_000 - 500_000_000)) / 1e18 = 25_000_000_000` - - `accrued[A] = 50_000_000_000 + 25_000_000_000 = 75_000_000_000` -- A's total = `75_000_000_000 wei` - -**At block 100 (B claims):** -- `_settle(B)`: - - `pending = (50e18 * (1_000_000_000 - 500_000_000)) / 1e18 = 25_000_000_000` - - `accrued[B] = 25_000_000_000` -- B's total = `25_000_000_000 wei` - -#### Assertions -- [ ] A gets 100% of first 50 blocks (`50_000_000_000`) + 50% of next 50 blocks (`25_000_000_000`) = `75_000_000_000` -- [ ] B gets 50% of next 50 blocks (`25_000_000_000`) -- [ ] Sum = `100_000_000_000` = total DAO earnings for 100 blocks ✓ -- [ ] `_beforeTokenTransfer` settles BEFORE balances change -- [ ] Transfer to B sets `userIndex[B] = accEthPerShare` → no retroactive earnings - ---- - -### CC-9: Governance Parameter Change Mid-Operation - -**Modules Touched:** SSVDAO, SSVClusters, SSVOperators, ProtocolLib -**Bug Class Covered:** Parameter changes applied at correct boundary - -#### Sub-scenario 9a: Network Fee Update - -**Preconditions:** -- Cluster with 1 validator, balance = 10 ETH, created at block 0 -- `sp.ethNetworkFee = PackedETH.wrap(10_000)` initially -- 4 operators, `ethFee = PackedETH.wrap(20_000)` - -**Actions:** -| Step | Action | Block | -|------|--------|-------| -| 1 | Advance 100 blocks | 100 | -| 2 | Owner calls `updateNetworkFee(2_000_000_000)` → packed = 20_000 | 100 | -| 3 | Advance 100 blocks | 200 | -| 4 | Withdraw from cluster | 200 | - -**Network fee index calculation:** -- `updateNetworkFee` calls `updateDAOEarnings` → settles at old fee -- Then sets `sp.ethNetworkFee = PackedETH.wrap(20_000)` and `sp.ethNetworkFeeIndex = currentIndex` -- `currentIndex at block 100 = 0 + 100 * 10_000 = 1_000_000` -- After update: `ethNetworkFeeIndex = 1_000_000`, `ethNetworkFeeIndexBlockNumber = 100` - -**At block 200 withdraw:** -- `currentNetworkFeeIndex = 1_000_000 + (200 - 100) * 20_000 = 3_000_000` -- `networkFeeIndexDelta = 3_000_000 - cluster.networkFeeIndex_at_creation` - -If cluster was created at block 0 with `networkFeeIndex = 0`: -- Total delta = `3_000_000` -- This correctly represents: 100 blocks at 10_000 + 100 blocks at 20_000 = 1_000_000 + 2_000_000 - -#### Assertions -- [ ] Old fee used for blocks 0-100, new fee for blocks 100-200 -- [ ] Transition is seamless via network fee index accumulator -- [ ] DAO earnings settled at exact block of fee change - -#### Sub-scenario 9b: Liquidation Threshold Update - -**Preconditions:** -- Cluster at block 200, balance just above old threshold -- `minimumBlocksBeforeLiquidation = 200` → threshold = X -- Cluster balance = X + 1 wei - -**Actions:** -| Step | Action | Block | -|------|--------|-------| -| 1 | Owner updates `minimumBlocksBeforeLiquidation = 400` | 200 | -| 2 | Third-party tries to liquidate | 200 | - -**Assertions:** -- [ ] New threshold = 2 × old threshold (doubled blocks) -- [ ] Cluster that was safe is now liquidatable -- [ ] Liquidation succeeds immediately after parameter change - ---- - -### CC-10: Full System Lifecycle (End-to-End) - -**Modules Touched:** ALL modules -**Bug Class Covered:** Complete system correctness across full lifecycle - -#### Preconditions -- Empty system, block 0 -- `sp.ethNetworkFee = PackedETH.wrap(10_000)` (1e9 wei/block) -- `minimumBlocksBeforeLiquidation = 100` -- `declareOperatorFeePeriod = 100 seconds` -- `executeOperatorFeePeriod = 200 seconds` - -#### Action Sequence -| Step | Action | Block | Time | -|------|--------|-------|------| -| 1 | Register 4 operators with fee 2e9 (packed 20_000) | 10 | T0 | -| 2 | User A stakes 50e18 SSV | 20 | T1 | -| 3 | Register validator, 10 ETH deposit | 100 | T2 | -| 4 | Advance 100 blocks | 200 | T3 | -| 5 | Oracle commits EB root | 200 | T3 | -| 6 | `updateClusterBalance(EB=48 ETH, 1 validator)` | 200 | T3 | -| 7 | Advance 100 blocks | 300 | T4 | -| 8 | Operator 1 declares fee increase to 2.2e9 (packed 22_000) | 300 | T4 | -| 9 | Advance (past timelock), execute fee | 400 | T5 | -| 10 | Register 2nd validator, 0 deposit | 400 | T5 | -| 11 | Advance 100 blocks | 500 | T6 | -| 12 | User A claims staking rewards | 500 | T6 | -| 13 | Remove 1st validator | 500 | T6 | -| 14 | Advance 100 blocks | 600 | T7 | -| 15 | Withdraw remaining cluster balance | 600 | T7 | -| 16 | Remove operator (after removing all validators) | 600 | T7 | - -#### Key State Changes to Track - -**Step 6: EB Update to 48 ETH (1 validator)** -- `newVUnits = ebToVUnits(48) = ceil(48 * 10_000 / 32) = 15_000` -- `effectiveOldVUnits = 1 * 10_000 = 10_000` (implicit) -- Deviation = 5_000 -- Fee settlement for blocks 100-200 at OLD vUnits = 10_000 -- Then `operatorEthVUnits[1..4] += 5_000` each -- `sp.daoTotalEthVUnits = 10_000 + 5_000 = 15_000` - -**Step 9: Fee execution** -- Settles operator 1 earnings from block 200 to 400 at old fee 20_000 -- With `effectiveVUnits = 5_000 + 1 * 10_000 = 15_000` -- Then `ethFee` changes to 22_000 - -**Step 10: Register 2nd validator** -- EB snapshot has `vUnits = 15_000`, so: `ebSnapshot.vUnits += 1 * 10_000 = 25_000` -- `sp.daoTotalEthVUnits += 10_000` → now 25_000 -- Each operator `ethValidatorCount = 2` - -**Step 12: User A claims staking rewards** -- `_syncFees` gathers all DAO earnings from block 20 to 500 -- Multiple phases with different `daoTotalEthVUnits`: - - Blocks 20-100: vUnits = 0 (no cluster yet) → 0 earnings - - Blocks 100-200: vUnits = 10_000 → earnings rate 10_000 - - Blocks 200-300: vUnits = 15_000 (after EB update) → earnings rate 15_000 - - Blocks 300-400: vUnits = 15_000 → same - - Blocks 400-500: vUnits = 25_000 (after 2nd validator) → earnings rate 25_000 - -**Step 13: Remove 1st validator** -- EB snapshot: `vUnits = 25_000 - 10_000 = 15_000`, if `validatorCount == 1` - - If `validatorCount == 1` and `ebSnapshot.vUnits > 0`: deduct baseline - - Remaining deviation = `15_000 - 1 * 10_000 = 5_000` -- Each operator `ethValidatorCount = 1` - -**Step 16: Final verification** -After all operations: -- [ ] All cluster balances add up with all operator earnings and DAO earnings = total ETH deposited minus withdrawals -- [ ] All SSV staking rewards match DAO network fee earnings -- [ ] cSSV supply matches active stakes -- [ ] `ethDaoValidatorCount == Σ(operator.ethValidatorCount)` -- [ ] `daoTotalEthVUnits == ethDaoValidatorCount * 10_000 + Σ(deviations)` - ---- - -## Gap Analysis: Cross-Partition Findings - -### Finding 1: DISC-OV-8 and DISC-CM-1 are the same discrepancy (deposit doesn't settle fees) -Both OV and CM partitions independently discovered this. The scenarios are consistent: deposit is intentionally simple, and tests should NOT expect fee settlement on deposit. - -### Finding 2: DISC-OV-9 and DISC-CM-2 are the same discrepancy (deposit doesn't check active) -Same cross-partition duplication. Code is intentional. - -### Finding 3: Operator removal without validator count check (DISC-OV-3) has cross-module implications -This discrepancy affects the global invariant `ethDaoValidatorCount == Σ(operator.ethValidatorCount)`. If an operator with active validators is removed, the invariant breaks. However, the cluster's fee calculation still works because: -- The removed operator's index is frozen (DISC-OV-4) -- The cluster stops accruing fees for the removed operator -- **BUT**: `ethDaoValidatorCount` is NOT decremented, causing `daoTotalEthVUnits` to be overstated -- This means DAO earns MORE network fees than clusters actually pay → conservation law still holds (DAO overcounts) -- The excess is "phantom earnings" that no one can claim (clusters don't pay for the removed operator) -- **Impact on staking**: staking rewards would be slightly higher than actual fee revenue → potential insolvency of staking pool - -### Finding 4: `_updateOperatorVUnits` applies FULL deviation per operator (DISC-ES-6) -This is consistent with `_executeLiquidation` and `_bulkRemoveValidator` cleanup. The pattern is deliberate: each operator tracks the sum of deviations from ALL clusters it serves. OV-33 verified this is NOT a bug. Cross-partition consistency confirmed. - -### Finding 5: Withdraw not updating operator snapshots (DISC-CM-3) is NOT a partition-specific issue -This affects the conservation law: after a withdraw, stored operator balances are stale. The conservation law uses `>=` to handle this. Cross-cutting tests must account for this when checking exact balances. - -### Finding 6: Missing cross-module scenario — DAO earnings during staking claims -When a staker calls `claimEthRewards`, both `sp.ethDaoBalance` and `s.stakingEthPoolBalance` are decremented. If multiple stakers claim in sequence, each claim's `_syncFees` re-settles the DAO earnings. The `current <= previous` path (DISC-ES-2) handles the case where a claim reduces `ethDaoBalance` below `stakingEthPoolBalance`. - -### Finding 7: No partition tested the oracle-staking coupling -ES-5c noted that `cSSV.totalSupply() == 0` blocks oracle commits (`ZeroCSSVSupply`). This means: no staking → no EB updates → no explicit vUnit tracking. This coupling was identified but no cross-cutting scenario tests the full chain: stake → oracle commit → EB update → staking rewards increase. - ---- - -## Appendix: Cross-Module Interaction Map - -| Source Module | Target State | Write | Read | Key Functions | -|---|---|---|---|---| -| SSVClusters.liquidate | StorageProtocol | `daoTotalEthVUnits ±=`, `ethDaoBalance` | `ethNetworkFee`, `minimumBlocksBeforeLiquidation` | `updateDAO`, `_executeLiquidation` | -| SSVClusters.migrate | StorageProtocol + StorageEB | `updateDAO`, `daoTotalEthVUnits`, `operatorEthVUnits[]` | `currentNetworkFeeIndex()` | `updateClusterOperatorsMigration` | -| SSVClusters.updateEB | StorageProtocol + StorageEB | `updateDAOEthVUnits()`, `operatorEthVUnits[]`, `clusterEB[].vUnits` | `currentNetworkFeeIndex()` | `_applyClusterFeeUpdates`, `_updateOperatorVUnits` | -| SSVStaking._syncFees | StorageProtocol + StorageStaking | `ethDaoBalance`, `ethDaoIndexBlockNumber`, `accEthPerShare` | `networkTotalEarnings()` (reads `daoTotalEthVUnits`, `ethNetworkFee`) | `_syncFees` | -| SSVStaking.claim | StorageProtocol | `ethDaoBalance -= payout` | `ethDaoBalance`, `stakingEthPoolBalance` | `claimEthRewards` | -| OperatorLib.updateSnapshotSt | StorageEB | (read only) | `operatorEthVUnits[operatorId]` | `updateSnapshotSt` | -| ClusterLib.getVUnits | StorageEB | (read only) | `clusterEB[clusterId].vUnits` | `getVUnits`, `updateBalanceWithEB`, `isLiquidatableWithEB` | -| ProtocolLib.networkTotalEarnings | StorageProtocol | (read only, view) | `daoTotalEthVUnits`, `ethNetworkFee`, `ethDaoBalance` | Used by SSVStaking._syncFees | -| ProtocolLib.updateDAO | StorageProtocol | `ethDaoValidatorCount ±=`, `daoTotalEthVUnits ±=`, settles `ethDaoBalance` | implicit via updateDAOEarnings | Called by SSVClusters on register/liquidate/reactivate/migrate | - ---- - -## Appendix: Key Code References - -| Concept | File | Lines | -|---------|------|-------| -| registerValidator | SSVValidators.sol | 31-42 | -| removeValidator | SSVValidators.sol | 96-100 | -| deposit (ETH) | SSVClusters.sol | 190-205 | -| withdraw (ETH) | SSVClusters.sol | 210-260 | -| liquidate (ETH) | SSVClusters.sol | 35-69 | -| reactivate | SSVClusters.sol | 133-185 | -| migrateClusterToETH | SSVClusters.sol | 264-348 | -| updateClusterBalance | SSVClusters.sol | 353-423 | -| _applyClusterFeeUpdates | SSVClusters.sol | 463-494 | -| _updateOperatorVUnits | SSVClusters.sol | 496-515 | -| _liquidateAfterEBUpdateIfNeeded | SSVClusters.sol | 524-555 | -| _executeLiquidation | SSVClusters.sol | 557-617 | -| registerOperator | SSVOperators.sol | 28-66 | -| removeOperator | SSVOperators.sol | 71-93 | -| declareOperatorFee | SSVOperators.sol | 95-142 | -| executeOperatorFee | SSVOperators.sol | 144-169 | -| reduceOperatorFee | SSVOperators.sol | 181-198 | -| commitRoot | SSVDAO.sol | 155-200 | -| replaceOracle | SSVDAO.sol | 205-229 | -| stake | SSVStaking.sol | 41-61 | -| requestUnstake | SSVStaking.sol | 66-94 | -| claimEthRewards | SSVStaking.sol | 114-145 | -| onCSSVTransfer | SSVStaking.sol | 169-177 | -| _syncFees | SSVStaking.sol | 179-203 | -| _settle | SSVStaking.sol | 205-208 | -| _settleWithBalance | SSVStaking.sol | 210-224 | -| networkTotalEarnings | ProtocolLib.sol | 85-91 | -| updateDAO | ProtocolLib.sol | 108-120 | -| updateDAOEthVUnits | ProtocolLib.sol | 143-151 | -| updateSnapshotSt (ETH) | OperatorLib.sol | 52-72 | -| ensureETHDefaults | OperatorLib.sol | 142-153 | -| updateClusterOperators | OperatorLib.sol | 253-282 | -| updateClusterOperatorsMigration | OperatorLib.sol | 367-411 | -| ebToVUnits | ClusterLib.sol | 353-358 | -| vUnitsToEB | ClusterLib.sol | 365-367 | -| getVUnits | ClusterLib.sol | 277-289 | -| updateBalanceWithEB | ClusterLib.sol | 298-313 | -| isLiquidatableWithEB | ClusterLib.sol | 67-84 | -| _beforeTokenTransfer | CSSVToken.sol | 26-30 | diff --git a/docs/SIMULATION-DESIGN.md b/docs/SIMULATION-DESIGN.md deleted file mode 100644 index a54645bd1..000000000 --- a/docs/SIMULATION-DESIGN.md +++ /dev/null @@ -1,500 +0,0 @@ -# Simulation Design: SSV Network v2.0.0 Monte Carlo Upgrade Simulation - -Research findings and architecture design for a fork-based Monte Carlo simulation -that stress-tests the v2.0.0 upgrade (ETH payments, effective balance accounting, -SSV staking) under realistic mainnet conditions. - ---- - -## R-1: Oracle Quorum Mechanics - -### `commitRoot` Signature - -```solidity -function commitRoot(bytes32 merkleRoot, uint64 blockNum) external; -``` - -**Source:** `contracts/interfaces/ISSVDAO.sol:203` - -### How `commitRoot` Works - -**Source:** `contracts/modules/SSVDAO.sol:155-200` - -1. **Caller validation:** `s.oracleIdOf[msg.sender] != 0` (reverts `NotOracle`) -2. **Monotonicity:** `blockNum > seb.latestCommittedBlock` (reverts `StaleBlockNumber`) -3. **Not future:** `blockNum <= block.number` (reverts `FutureBlockNumber`) -4. **Weight source:** `totalStaked = ICSSVToken(CSSV_ADDRESS).totalSupply()` must be > 0 (reverts `OracleHasZeroWeight`) -5. **Commitment key:** `keccak256(abi.encodePacked(blockNum, merkleRoot))` ties block+root together -6. **Double-vote guard:** `seb.hasVoted[commitmentKey][oracleId]` must be false (reverts `AlreadyVoted`) -7. **Weight accumulation:** Each oracle has equal weight = `totalStaked / defaultOracleIds.length` -8. **Quorum check:** `accumulatedWeight >= (totalStaked * quorumBps) / 10_000` - - If met: stores `seb.ebRoots[blockNum] = merkleRoot`, updates `latestCommittedBlock`, emits `RootCommitted` - - If not met: emits `WeightedRootProposed` - -### How Unit Tests Simulate Oracle Quorum - -**Source:** `test/unit/SSVDAO/commitRoot.test.ts` - -Tests use a **harness contract** (`SSVDAOHarness`) with mock helper functions: -- `dao.mockSetOracle(oracleId, address)` — registers oracle addresses -- `dao.mockSetQuorumBps(bps)` — sets quorum threshold -- `dao.mockSetLatestCommittedBlock(blockNum)` — sets committed block -- `cssv.mint(owner, totalSupply)` — mints cSSV tokens (needed for oracle weight calculation) - -The tests call `dao.connect(oracleN).commitRoot(merkleRoot, blockNum)` from each oracle signer sequentially until quorum is reached. - -### Can We Impersonate Oracles on Fork? - -**Yes.** The fork fixture (`test/setup/fixtures.ts:344-346`) already does this: - -```typescript -await ethers.provider.send("hardhat_impersonateAccount", [ForkConfig.DAO_ADDRESS]); -const daoSigner = await ethers.getSigner(ForkConfig.DAO_ADDRESS); -await ethers.provider.send("hardhat_setBalance", [ForkConfig.DAO_ADDRESS, "0x..."]); -``` - -**Mainnet oracle addresses** from `deployments/mainnet-upgrade.config.json`: -```json -{ - "1": "0x6b6fa15717beeb5a40fac6610c3e92776037a30e", - "2": "0x01E7e108eD97B4EA08e1a184aBA793b9D282E565", - "3": "0xef6d2263a1d96eac2a4530ba327bcc2e6c948feb", - "4": "0xFe33f6cb66ee2A85748458556D6ccEC3716D2173" -} -``` - -We can impersonate 3 of 4 oracles and call `commitRoot` to meet the 75% quorum. - -**Important prerequisite:** cSSV `totalSupply` must be > 0 before calling `commitRoot`. On a fresh fork (pre-upgrade), no one has staked yet, so we must first: -1. Deploy + upgrade the contracts (via the fork fixture) -2. Stake SSV tokens to mint cSSV (or `hardhat_setStorageAt` to fake totalSupply) -3. Then call `commitRoot` from impersonated oracles - ---- - -## R-2: View Functions for Invariant Checking - -### Complete View Function Inventory - -**Source:** `contracts/interfaces/ISSVViews.sol`, `contracts/modules/SSVViews.sol` - -| Function | Returns | Purpose | -|---|---|---| -| `getValidator(address, bytes)` | `bool` | Is validator active? | -| `getOperatorFee(uint64)` | `uint256` | Operator ETH fee | -| `getOperatorFeeSSV(uint64)` | `uint256` | Operator SSV fee (legacy) | -| `getOperatorDeclaredFee(uint64)` | `OperatorDeclaredFeeData` | Pending fee change | -| `getOperatorById(uint64)` | `OperatorData` | Full operator details (ETH) | -| `getOperatorByIdSSV(uint64)` | `OperatorData` | Full operator details (SSV) | -| `getWhitelistedOperators(uint64[], address)` | `uint64[]` | Which ops whitelist an address | -| `isLiquidatable(owner, operatorIds, cluster)` | `bool` | ETH cluster liquidatable? | -| `isLiquidatableSSV(owner, operatorIds, cluster)` | `bool` | SSV cluster liquidatable? | -| `isLiquidated(owner, operatorIds, cluster)` | `bool` | Cluster already liquidated? | -| `getBurnRate(owner, operatorIds, cluster)` | `uint256` | ETH cluster burn rate | -| `getBurnRateSSV(owner, operatorIds, cluster)` | `uint256` | SSV cluster burn rate | -| `getOperatorEarnings(uint64)` | `uint256` | Operator ETH earnings | -| `getOperatorEarningsSSV(uint64)` | `uint256` | Operator SSV earnings | -| `getBalance(owner, operatorIds, cluster)` | `uint256` | ETH cluster balance | -| `getBalanceSSV(owner, operatorIds, cluster)` | `uint256` | SSV cluster balance | -| `getEffectiveBalance(owner, operatorIds, cluster)` | `uint32` | Cluster effective balance | -| `getClusterAssetType(owner, operatorIds)` | `uint8` | VERSION_SSV=0 or VERSION_ETH=1 | -| `getNetworkFee()` | `uint256` | Current ETH network fee | -| `getNetworkFeeSSV()` | `uint256` | Current SSV network fee | -| `getNetworkEarnings()` | `uint256` | Total ETH network earnings | -| `getNetworkEarningsSSV()` | `uint256` | Total SSV network earnings | -| `getOperatorFeeIncreaseLimit()` | `uint64` | Max fee increase % | -| `getMaximumOperatorFee()` | `uint256` | Max operator fee (ETH) | -| `getMaximumOperatorFeeSSV()` | `uint256` | Max operator fee (SSV) | -| `getMinimumOperatorEthFee()` | `uint256` | Min operator fee (ETH) | -| `getOperatorFeePeriods()` | `OperatorFeePeriodsData` | Declare/execute periods | -| `getLiquidationThresholdPeriod()` | `uint64` | ETH liquidation threshold blocks | -| `getLiquidationThresholdPeriodSSV()` | `uint64` | SSV liquidation threshold blocks | -| `getMinimumLiquidationCollateral()` | `uint256` | Min ETH liquidation collateral | -| `getMinimumLiquidationCollateralSSV()` | `uint256` | Min SSV liquidation collateral | -| `getValidatorsPerOperatorLimit()` | `uint32` | Max validators per operator | -| **`getNetworkValidatorsCount()`** | `uint32` | Total ETH validator count | -| **`cooldownDuration()`** | `uint256` | Unstake cooldown period | -| **`totalStaked()`** | `uint256` | Total SSV staked (cSSV supply) | -| **`stakedBalanceOf(address)`** | `uint256` | User's cSSV balance | -| **`pendingUnstake(address)`** | `UnstakeRequestsData[]` | User's pending unstake requests | -| **`accEthPerShare()`** | `uint256` | Global reward accumulator | -| **`stakingEthPoolBalance()`** | `uint256` | ETH in staking pool | -| **`previewClaimableEth(address)`** | `uint256` | Preview claimable ETH rewards | -| `getOracle(uint32)` | `address` | Oracle address by ID | -| `getOracleWeight(uint32)` | `uint256` | Oracle weight | -| `getActiveOracleIds()` | `uint32[4]` | Active oracle IDs | -| `getQuorumBps()` | `uint16` | Quorum in basis points | -| `getCommittedRoot(uint64)` | `bytes32` | Merkle root for block | -| `getVersion()` | `string` | Contract version | - -### Specifically Asked Views — All Present - -| View | Available? | Source | -|---|---|---| -| `accEthPerShare()` | **YES** | `SSVViews.sol:624` — reads `SSVStorageStaking.load().accEthPerShare` | -| `previewClaimableEth(address)` | **YES** | `SSVViews.sol:638` — computes pending via `_previewAccEthPerShare` helper | -| `getOperatorEarnings(uint64)` | **YES** | `SSVViews.sol:368` — updates snapshot in memory, returns `ethSnapshot.balance` | -| `getNetworkValidatorsCount()` | **YES** | `SSVViews.sol:578` — returns `sp.ethDaoValidatorCount` | -| `stakingEthPoolBalance()` | **YES** | `SSVViews.sol:631` — returns unpacked `s.stakingEthPoolBalance` | - -**Key finding for simulation:** `previewClaimableEth` (SSVViews.sol:638-645) includes a `_previewAccEthPerShare` helper that simulates `_syncFees` in read-only mode, factoring in unrealized network fee earnings. This means we can read accurate claimable rewards at any point without triggering a state change. - ---- - -## R-3: Migration Value Calculation - -### `migrateClusterToETH` Requirements - -**Source:** `contracts/modules/SSVClusters.sol:264-348` - -```solidity -function migrateClusterToETH(uint64[] calldata operatorIds, Cluster memory cluster) external payable -``` - -**Steps:** -1. Validates cluster exists in SSV mapping (`VERSION_SSV`) -2. Computes SSV balance at current block (settles outstanding fees) -3. Sets `cluster.balance = msg.value` (ETH deposit) -4. Sets `cluster.active = true` (even if previously liquidated) -5. Liquidation check: `isLiquidatableWithEB(...)` — must pass or reverts `InsufficientBalance` -6. Stores in `ethClusters`, deletes from `clusters` -7. Handles EB deviation accounting -8. Refunds full SSV cluster balance via `CoreLib.transferTokenBalance(msg.sender, ssvClusterBalance)` - -### Minimum ETH for Migration (Survival Formula) - -For a cluster with N validators, 4 operators at default fee, implicit EB (32 ETH): - -``` -vUnits = N * 10_000 (implicit, assumes 32 ETH/validator) -burnRate = 4 * DEFAULT_OPERATOR_ETH_FEE_PACKED - = 4 * 17754 (1_775_464_912 / 100_000 = 17754 packed) -networkFee = 35509 (3_550_900_000 / 100_000 = 35509 packed) - -rate = burnRate + networkFee = 4*17754 + 35509 = 106525 - -thresholdUnits = (minimumBlocksBeforeLiquidation * rate * vUnits) / VUNITS_PRECISION - = (35800 * 106525 * N * 10000) / 10000 - = 35800 * 106525 * N - -liquidationThreshold = thresholdUnits * ETH_DEDUCTED_DIGITS - = 35800 * 106525 * N * 100_000 - -For N=1: 35800 * 106525 * 100_000 = 381,359,500,000,000 wei ≈ 0.0003814 ETH -``` - -Plus must also exceed `minimumLiquidationCollateral = 940_000_000_000_000 = 0.00094 ETH`. - -**So minimum ETH for migration with N validators (4 ops, default fees):** -``` -max(0.00094, 0.0003814 * N) ETH + epsilon -``` - -For N=1..3, the 0.00094 ETH minimum collateral dominates. For N>=3, the threshold formula dominates. - -### SSV Refund Handling - -At `SSVClusters.sol:340-342`: -```solidity -if (ssvClusterBalance != 0) { - CoreLib.transferTokenBalance(msg.sender, ssvClusterBalance); -} -``` - -The full outstanding SSV balance (after settling fees to current block) is refunded to `msg.sender` as SSV tokens. - ---- - -## R-4: Mainnet Deployment Info - -### Contract Addresses - -**Source:** `deployments/mainnet-upgrade.config.json`, `.openzeppelin/mainnet.json` - -| Contract | Address | -|---|---| -| SSVNetwork (proxy) | `0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1` | -| SSVNetworkViews | `0xafE830B6Ee262ba11cce5F32fDCd760FFE6a66e4` | -| SSV Token | `0x9D65fF81a3c488d585bBfb0Bfe3c7707c7917f54` | -| DAO/Owner | `0xb35096b074fdb9bBac63E3AdaE0Bbde512B2E6b6` | - -### Deployment Block - -The `.openzeppelin/mainnet.json` records the proxy deploy tx hash: `0x4a11a560d3c2f693e96f98abb1feb447646b01b36203ecab0a96a1cf45fd650b`. The exact block number is not stored in the repo config files. - -**To determine the deployment block**, look up the tx on-chain. The SSVNetwork v1 was deployed around block 17507487 (June 2023). The current proxy at `0xDD9BC35aE...` was a later redeployment around block 18685000+ (Nov 2023). The current mainnet block is ~21.8M+ (Feb 2026). - -**Event scan estimate:** ~3M blocks from initial deployment to present. However, the fork approach avoids needing to scan events — we get live state directly from the fork. - -### Fork Configuration - -**Source:** `hardhat.config.ts:62-69` - -```typescript -hardhat_forked: { - type: 'edr-simulated', - forking: { - url: "http://127.0.0.1:8545", - blockNumber: process.env.FORK_BLOCK_NUMBER ? Number(...) : undefined, - } -} -``` - -The fork approach: -1. Start Anvil: `anvil --fork-url "$MAINNET_RPC_URL" --port 8545` -2. Run tests with `npx hardhat test --network hardhat_forked` -3. Optionally pin block with `FORK_BLOCK_NUMBER=` - ---- - -## R-5: SSV Token Minting on Fork - -### SSV Token Contract - -**Source:** `contracts/token/SSVToken.sol` - -```solidity -contract SSVToken is Ownable, ERC20, ERC20Burnable { - constructor() ERC20("SSV Token", "SSV") { - _mint(msg.sender, 1000000000000000000000); - } - function mint(address to, uint256 amount) external onlyOwner { - _mint(to, amount); - } -} -``` - -The `mint` function is `onlyOwner` — only the token deployer (not the DAO address on the SSVNetwork contract) can mint. The SSV token on mainnet has a fixed supply (no mint function exposed to arbitrary callers). - -### How Tests Provision SSV Tokens - -**Source:** `test/setup/fixtures.ts:181-184` - -In fresh deployments, tests deploy their own `MockToken`: -```typescript -const ssvToken = await connection.ethers.deployContract("MockToken"); -await ssvToken.mint(deployer.address, connection.ethers.parseEther("1000000")); -``` - -In fork tests (`ssvNetworkFullForkedFixture`), the test attaches to the real mainnet SSV token at `ForkConfig.SSV_TOKEN` and uses the existing on-chain balances. - -### Can We `deal` / `hardhat_setStorageAt`? - -**Yes.** This is the recommended approach for fork simulation: - -```typescript -// Option 1: hardhat_setBalance for ETH -await ethers.provider.send("hardhat_setBalance", [address, hexAmount]); - -// Option 2: hardhat_setStorageAt for ERC-20 balances -// SSV Token uses OpenZeppelin ERC20 — balances are at mapping slot -// balanceOf mapping is at slot 0 in the OZ layout -const slot = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode( - ["address", "uint256"], [targetAddress, 0] -)); -await ethers.provider.send("hardhat_setStorageAt", [ - ssvTokenAddress, slot, ethers.zeroPadValue(ethers.toBeHex(amount), 32) -]); -``` - -For the simulation, we can also impersonate the SSV token owner to call `mint()` if the mainnet token has a live owner, OR use `hardhat_setStorageAt` to directly set balances. - ---- - -## R-6: Fee Sync Mechanics - -### `_syncFees` — When Is It Called? - -**Source:** `contracts/modules/SSVStaking.sol:179-203` - -`_syncFees` is called inside **SSVStaking** at the start of these functions: -- `syncFees()` — explicit external call (line 35) -- `stake(uint256)` — line 51 -- `requestUnstake(uint256)` — line 73 -- `claimEthRewards()` — line 117 -- `onCSSVTransfer(from, to, amount)` — line 174 (triggered by cSSV transfers) - -### What `_syncFees` Does - -```solidity -function _syncFees(StorageStaking storage s) internal { - StorageProtocol storage sp = SSVStorageProtocol.load(); - PackedETH current = sp.networkTotalEarnings(); // <- reads live network earnings - sp.ethDaoBalance = current; // <- snapshots DAO balance - sp.ethDaoIndexBlockNumber = uint32(block.number); // <- snapshots block - - PackedETH previous = s.stakingEthPoolBalance; - if (current.lte(previous)) { - s.stakingEthPoolBalance = current; - return; - } - - PackedETH packedNewFees = current.sub(previous); - uint256 totalStaked = ICSSVToken(CSSV_ADDRESS).totalSupply(); - if (totalStaked != 0) { - uint256 newFeesWei = PackedETHLib.unpack(packedNewFees); - s.accEthPerShare += uint128((newFeesWei * PRECISION) / totalStaked); - } - s.stakingEthPoolBalance = current; -} -``` - -`networkTotalEarnings()` (ProtocolLib.sol:85-91) computes: -``` -earningsUnits = (blocksSinceLastUpdate * ethNetworkFee * daoTotalEthVUnits) / VUNITS_PRECISION -return ethDaoBalance + packed(earningsUnits) -``` - -### Does `accEthPerShare` Update on Cluster Operations? - -**No.** Confirmed by grep: `_syncFees` / `syncFees` are NOT called in `SSVClusters.sol`. Cluster operations (`deposit`, `withdraw`, `liquidate`, `migrateClusterToETH`, `updateClusterBalance`) do NOT trigger `_syncFees`. - -However, `ProtocolLib.updateDAO()` and `ProtocolLib.updateDAOEarnings()` are called by cluster operations, which update `ethDaoBalance` and `ethDaoIndexBlockNumber`. This means: -- **The underlying network earnings accumulate correctly** (via `networkTotalEarnings()` reading live block numbers) -- **But `accEthPerShare` in StorageStaking is stale** until someone calls a staking function - -**Implication for simulation:** The staking accumulator is lazy. `accEthPerShare` only updates when staking actions occur. Between staking actions, network fees continue to accrue in `ethDaoBalance` via `ProtocolLib`, but the per-share distribution isn't computed until `_syncFees` is called. The view function `previewClaimableEth` handles this correctly by computing a preview. - ---- - -## R-7: Mainnet Scale - -### Estimated Network Size - -The mainnet SSV network (as of early 2026): -- **Operators:** ~1,200-1,500 registered operators (not all active) -- **Clusters:** ~25,000-40,000 clusters (based on validator registrations) -- **Validators:** ~70,000-100,000+ validators registered through SSV - -The `getNetworkValidatorsCount()` view returns `sp.ethDaoValidatorCount` which tracks ETH-cluster validators only. On a pre-upgrade fork, this will be 0 since no clusters have migrated yet. - -### Feasibility of Full Tracking - -For simulation purposes: -- **All operators:** Feasible to track — ~1,500 is small -- **All clusters:** Feasible with events-based reconstruction, but we need cluster structs (validatorCount, index, networkFeeIndex, balance, active). These are hashed on-chain, not stored in cleartext. -- **Sampling approach:** For Monte Carlo simulation, we can: - 1. Use a representative sample (100-500 clusters across different sizes) - 2. Create synthetic clusters with realistic distributions - 3. Focus on migration scenarios rather than full state replay - -### Cluster State Challenge - -Cluster data is stored as `keccak256(hash)` — not directly readable. To get actual cluster state, we'd need to: -1. Replay events from deployment to reconstruct cluster structs -2. OR use the view functions with known cluster structs from event logs -3. OR create fresh clusters in the simulation - -**Recommendation:** For Monte Carlo simulation, create synthetic clusters with realistic parameter distributions rather than trying to replay full mainnet state. The fork gives us correct protocol parameters and operator state; we generate the cluster scenarios. - ---- - -## Architecture Design - -### Refined Simulation Architecture - -``` -┌─────────────────────────────────────────────────────┐ -│ Simulation Harness │ -│ (TypeScript, runs on Hardhat forked network) │ -├─────────────────┬───────────────────────────────────┤ -│ Setup Phase │ Execution Phase │ Check Phase │ -│ │ │ │ -│ 1. Fork mainnet │ For each epoch: │ After each: │ -│ 2. Upgrade │ - Mine N blocks │ - View calls │ -│ contracts │ - Random actions: │ - Invariant │ -│ 3. Configure │ * register val │ checks │ -│ oracles │ * migrate │ - Balance │ -│ 4. Provision │ * deposit/wdraw │ accounting │ -│ test actors │ * liquidate │ - Conservation │ -│ 5. Stake SSV │ * stake/unstake │ laws │ -│ (mint cSSV) │ * commitRoot │ │ -│ │ * claimRewards │ │ -└─────────────────┴───────────────────┴───────────────┘ -``` - -### Simulation Parameters - -| Parameter | Recommended Value | Rationale | -|---|---|---| -| Fork block | Latest mainnet block | Most realistic state | -| Sample operators | 50 (of ~1500) | Representative mix of fees/sizes | -| Sample clusters | 200-500 synthetic | Mix of sizes: 1, 4, 32, 100 validators | -| Actors (EOAs) | 20 | Cluster owners, stakers, liquidators | -| Epochs per run | 100 | Each epoch = 1000 blocks (~3.5 hrs) | -| Blocks per epoch | 1000 | Enough for fee accrual to be significant | -| Monte Carlo runs | 50-100 | For statistical confidence | -| Actions per epoch | 5-15 random | From weighted distribution | - -### Action Distribution (per epoch) - -| Action | Weight | Description | -|---|---|---| -| `registerValidator` | 10% | Add validators to ETH clusters | -| `migrateClusterToETH` | 15% | Migrate SSV clusters | -| `deposit` | 15% | Top up cluster balances | -| `withdraw` | 10% | Withdraw from clusters | -| `liquidate` | 5% | Liquidate underfunded clusters | -| `commitRoot` | 10% | Oracle EB updates | -| `updateClusterBalance` | 10% | Apply EB changes | -| `stake` | 10% | Stake SSV tokens | -| `requestUnstake` | 5% | Request unstaking | -| `claimEthRewards` | 5% | Claim ETH rewards | -| `mine blocks (no-op)` | 5% | Time passage only | - -### Invariant Checks - -After each epoch, verify: - -1. **ETH Conservation:** `contract.balance >= sum(all_cluster_balances) + sum(all_operator_eth_earnings) + stakingEthPoolBalance` -2. **SSV Conservation:** `ssvToken.balanceOf(contract) >= sum(all_ssv_cluster_balances) + sum(all_operator_ssv_earnings) + sum(pending_unstake_amounts)` -3. **Staking Accumulator:** `accEthPerShare` monotonically non-decreasing -4. **Staking Pool:** `stakingEthPoolBalance <= getNetworkEarnings()` -5. **Validator Counts:** `getNetworkValidatorsCount() == sum(cluster.validatorCount for active ETH clusters)` -6. **Operator Consistency:** For each operator, `ethValidatorCount == sum(cluster.validatorCount where op in cluster)` -7. **Cluster Hash Integrity:** All cluster operations produce valid cluster hashes (verifiable via view functions) -8. **Liquidation Correctness:** No active cluster with `validatorCount > 0` is liquidatable after deposit/reactivate -9. **Oracle Monotonicity:** `latestCommittedBlock` strictly increases -10. **cSSV Supply = Total Staked SSV:** `cssvToken.totalSupply() == ssvToken.balanceOf(stakingContract) - pendingUnstakeTotal` - -### Showstoppers and Design Changes - -#### 1. Cluster State Opacity (Mitigated) -On-chain cluster data is hashed — we can't read arbitrary cluster state. **Mitigation:** Track all cluster structs locally in the simulation (created by us), and pass correct structs to each function call. This is how the existing tests work. - -#### 2. cSSV Must Exist Before Oracle Calls (Critical) -`commitRoot` requires `totalSupply > 0`. The simulation must stake SSV and mint cSSV **before** attempting any oracle root commits. **Sequence:** deploy/upgrade -> stake SSV -> set up oracles -> simulate. - -#### 3. Lazy `accEthPerShare` (Design Consideration) -The staking accumulator only updates on staking actions. For accurate invariant checking between epochs, use `previewClaimableEth()` (which simulates `_syncFees` read-only) rather than reading `accEthPerShare()` directly. - -#### 4. Merkle Proof Construction (Implementation Effort) -`updateClusterBalance` requires valid Merkle proofs. The simulation must build a Merkle tree from cluster effective balances and generate proofs. Use OpenZeppelin's `@openzeppelin/merkle-tree` library (same as the oracle would use). Proof leaf format: `keccak256(keccak256(abi.encode(clusterId, effectiveBalance)))`. - -#### 5. Fork State Freshness -The fork captures a point-in-time snapshot. During simulation, `block.number` advances locally but Ethereum mainnet state doesn't change. This is fine — we're testing contract logic, not mainnet liveness. - -### Implementation Roadmap - -1. **Phase 1 — Scaffold** (Task 1) - - Fork setup + upgrade fixture (leverage `ssvNetworkFullForkedFixture`) - - Actor provisioning (ETH + SSV via `hardhat_setBalance` / `hardhat_setStorageAt`) - - Oracle setup (impersonate 4 oracle addresses, fund with ETH) - - Initial SSV staking to bootstrap cSSV supply - -2. **Phase 2 — Action Engine** (Task 2) - - Random action generator with weighted distribution - - Cluster state tracker (local cache of all cluster structs) - - Merkle tree builder for EB oracle updates - - Block advancement (`mine` helper) - -3. **Phase 3 — Invariant Checker** (Task 3) - - Balance conservation checks (ETH + SSV) - - Staking reward accumulator verification - - Cross-entity consistency (operators vs clusters vs DAO) - - Statistical output (pass rates, failure distributions) - -4. **Phase 4 — Monte Carlo Runner** (Task 4) - - Parameterized test runner with random seeds - - Results aggregation and reporting - - Edge case amplification (heavy liquidation scenarios, rapid migration waves) diff --git a/docs/SOLIDITY_BEST_PRACTICES.md b/docs/SOLIDITY_BEST_PRACTICES.md deleted file mode 100644 index c6da5dbf1..000000000 --- a/docs/SOLIDITY_BEST_PRACTICES.md +++ /dev/null @@ -1,520 +0,0 @@ -# Solidity & Smart Contract Security — Best Practices - -Consolidated reference for secure Solidity development, derived from Trail of Bits' [Building Secure Contracts](https://github.com/crytic/building-secure-contracts). Use this document when implementing fixes, reviewing code, or writing new features. - ---- - -## Table of Contents - -1. [Design Principles](#1-design-principles) -2. [Implementation Guidelines](#2-implementation-guidelines) -3. [Upgradeability & Proxy Patterns](#3-upgradeability--proxy-patterns) -4. [Arithmetic Safety](#4-arithmetic-safety) -5. [Access Control](#5-access-control) -6. [Reentrancy & External Interactions](#6-reentrancy--external-interactions) -7. [Event Logging & Monitoring](#7-event-logging--monitoring) -8. [Token Integration](#8-token-integration) -9. [Testing Strategy](#9-testing-strategy) -10. [Static Analysis](#10-static-analysis) -11. [Fuzzing with Echidna](#11-fuzzing-with-echidna) -12. [Security Properties & Invariants](#12-security-properties--invariants) -13. [Code Maturity Checklist](#13-code-maturity-checklist) -14. [Deployment & Incident Response](#14-deployment--incident-response) -15. [Pre-Audit Checklist](#15-pre-audit-checklist) -16. [EVM Internals Quick Reference](#16-evm-internals-quick-reference) - ---- - -## 1. Design Principles - -### Keep it simple -Use the simplest solution that meets requirements. Every team member should understand the design. - -### Minimize on-chain logic -Keep as much computation off-chain as possible. Pre-process data off-chain, verify on-chain. Example: sort a list off-chain, verify order on-chain. - -### Document before coding -Write documentation at three levels before implementation: -1. **Plain English** — system purpose, assumptions, threat model -2. **Architecture diagrams** — contract interactions, state machine, data flow -3. **Code-level** — NatSpec for every public/external function, inline comments for non-obvious logic - -### Specification alignment -- Every arithmetic formula should map 1:1 to a specification -- Document precision loss expectations for every formula -- Specify parameter ranges (min/max) and propagate through docs -- System and function-level invariants should be explicitly stated - ---- - -## 2. Implementation Guidelines - -### Function design -- **Small functions with clear purpose** — one function, one job -- **Divide logic** across contracts or into grouped functions (auth, arithmetic, state) -- **Minimal cyclomatic complexity** — avoid deep nesting of if/else/ternary - -### Inheritance -- Keep inheritance trees shallow and narrow -- Be aware of C3 linearization — `contract A is B, C` and `contract A is C, B` have different storage layouts -- Watch for function shadowing across the inheritance chain -- Use Slither's inheritance-graph printer to visualize hierarchy - -### Dependencies -- Use well-tested libraries (OpenZeppelin) — don't copy-paste -- Pin dependency versions, keep them updated -- Audit third-party code before integrating - -### Solidity-specific -- **Use a stable compiler release** for deployment, but check for warnings with the latest -- **Avoid inline assembly** unless absolutely necessary — requires EVM mastery -- If assembly is used: justify it, document every operation, provide a high-level reference implementation, and test with differential fuzzing -- **Solidity 0.8+** provides built-in overflow/underflow checks — do not disable (`unchecked`) without explicit justification and documentation -- **Favor explicit over implicit** — be explicit about visibility, mutability, return types - -### Code hygiene -- No dead code — remove anything replaced -- No redundant logic — if similar code exists, extend it -- Clear naming conventions, consistent throughout -- Use custom errors instead of `require` strings (gas efficient, more informative) -- Types should enforce correctness where possible (e.g., custom types for packed values) - ---- - -## 3. Upgradeability & Proxy Patterns - -### General guidance -- **Prefer contract migration over upgradeability** — migration offers the same benefits without delegatecall complexity -- **If using delegatecall proxies, use data separation patterns** when possible -- **Document the upgrade procedure before deployment** — include: initialization calls, key locations, post-deployment verification scripts - -### Delegatecall proxy safety checklist - -| Risk | Mitigation | -|------|------------| -| **Storage layout mismatch** | Proxy and implementation must inherit from the same shared base. Never define state variables independently. | -| **Inheritance order** | `contract A is B, C` vs `contract A is C, B` produce different layouts. Lock inheritance order. | -| **Uninitialized implementation** | Initialize immediately on deployment. Use a factory pattern. Disable direct implementation usage with a constructor flag. | -| **Function shadowing** | If proxy and implementation define the same function, the proxy's version wins. Audit admin functions (`setOwner`, etc.). | -| **Immutable/constant drift** | Immutables are embedded in bytecode — they can diverge between proxy and implementation. | -| **Contract existence checks** | `delegatecall` to an address with no code returns `true`. Verify target contract exists. Most proxy libraries do NOT check this automatically. | -| **Storage struct ordering** | Append-only for storage structs — NEVER reorder or remove existing fields. | - -### Tools -- [`slither-check-upgradeability`](https://github.com/crytic/slither/wiki/Upgradeability-Checks) — automated safety checks for proxy patterns - ---- - -## 4. Arithmetic Safety - -### Overflow/underflow -- Solidity 0.8+ provides automatic checks for `+`, `-`, `*` -- `unchecked` blocks disable these checks — only use when overflow is mathematically impossible and document why -- When using assembly arithmetic, implement checks manually (see below) - -### Precision and rounding -- **Explicitly choose rounding direction** for every operation with precision loss -- Use ceiling division for conservative estimates (e.g., ETH to vUnits) -- Use floor division for safe payouts (e.g., vUnits to ETH) -- **Document precision loss** against a ground-truth (infinite-precision reference) -- Bound and document all trapping operations (divide-by-zero, etc.) - -### Packed types -- When packing values into smaller types (uint64, uint32), verify that overflow cannot occur before packing -- Document the precision lost by packing (e.g., `value / 100_000` loses last 5 digits) - -### Assembly arithmetic patterns -For `uint256` addition overflow check: -```solidity -unchecked { - c = a + b; - if (a > c) revert Overflow(); // Solidity 0.8.16+ -} -``` - -For `uint256` multiplication overflow check: -```solidity -unchecked { - c = a * b; - if (a != 0 && b != c / a) revert Overflow(); // Solidity 0.8.17+ -} -``` - -For sub-32-byte types (e.g., `int64`), clean upper bits with `signextend` or cast to `int256` first, then bounds-check. - -### Balance underflow protection -Always use `max(0, balance - fees)` pattern: -```solidity -uint256 usage = computeFees(); -cluster.balance = (usage >= cluster.balance) ? 0 : cluster.balance - usage; -``` - ---- - -## 5. Access Control - -### Principles -- **Least privilege** — each role should only access what it needs -- **Separation of concerns** — don't combine roles (fee-setter shouldn't have upgrade power) -- **No single EOA as sole admin** — use multisig/MPC for privileged operations -- **Two-step processes** for critical operations (e.g., `Ownable2Step`) -- Roles should be revocable - -### Implementation patterns -- Document all actors and their privileges in a matrix -- Test every actor-specific privilege explicitly -- Verify no privilege escalation paths exist -- Protect against leaked/lost keys — loss of one signer should not compromise the system - -### Checklist -- [ ] All privileged functions have access control -- [ ] Different roles have non-overlapping privileges -- [ ] Owner/admin functions use `onlyOwner` or equivalent -- [ ] Operator functions verify `operator.checkOwner()` -- [ ] No function can be called by an unauthorized party to modify state - ---- - -## 6. Reentrancy & External Interactions - -### Patterns -- **Checks-Effects-Interactions (CEI)** — validate, update state, then make external calls -- **Use `nonReentrant`** on any function that makes external calls or transfers ETH/tokens -- Never trust return values from external contracts without validation - -### External call risks -- External calls in transfer functions can lead to reentrancy (especially ERC777 hooks, `onERC721Received`) -- `delegatecall` returns `true` for addresses with no code -- Low-level calls (`call`, `delegatecall`, `staticcall`) return `true` for empty addresses — always check contract existence - -### Token transfers -- Use `SafeERC20` for token interactions (handles non-standard return values) -- Verify ETH transfers succeeded — check return value of `.call{value: amount}("")` -- Be aware of fee-on-transfer tokens, rebasing tokens, and tokens with hooks - ---- - -## 7. Event Logging & Monitoring - -### Design -- **Log ALL critical operations** — state changes, parameter updates, admin actions, transfers -- Use consistent event naming and parameter ordering -- Events facilitate debugging during development and monitoring after deployment -- Don't reuse the same event for different purposes - -### Monitoring -- Set up off-chain monitoring infrastructure that logs and alerts on events -- Document how to interpret each event and how to audit failures from logs -- Consider automated responses to suspicious patterns (pause, safe mode) -- Implement an incident response plan (see Section 14) - -### Event documentation should include -- Purpose of the event -- How it should be used by third parties (oracle, SDK, indexer) -- Assumptions about event ordering and completeness - ---- - -## 8. Token Integration - -When integrating with external tokens, verify: - -### ERC20 checklist -- [ ] Token has been security reviewed -- [ ] `transfer` and `transferFrom` return a boolean (some don't — use `SafeERC20`) -- [ ] Token mitigates ERC20 race condition on `approve` -- [ ] No fee-on-transfer behavior (deflationary tokens) -- [ ] No external calls in transfer functions (ERC777 hooks → reentrancy) -- [ ] No interest accrual that could get trapped -- [ ] Token is not upgradeable (or upgradeability is understood and acceptable) -- [ ] Owner cannot pause, blacklist, or perform unlimited minting -- [ ] Supply is distributed (not concentrated in few addresses) -- [ ] No flash minting capability - -### Known non-standard tokens -Be aware of specific tokens with non-standard behavior: -- **Missing revert**: BAT, HT, cUSDC, ZRX -- **Transfer hooks**: AMP, imBTC (reentrancy risk) -- **Missing return data**: BNB, OMG, USDT -- **Permit no-op**: WETH - ---- - -## 9. Testing Strategy - -### Unit tests -- Cover all happy paths, revert cases, edge conditions, and boundary values -- Test event emissions with exact parameter verification -- Test balance invariants (before/after checks) -- Test state consistency via view functions after operations -- Achieve 100% reachable branch and statement coverage - -### Test quality -- Tests should be isolated — no dependency on execution order -- Use descriptive test names that explain the scenario -- Follow Arrange-Act-Assert pattern -- Don't test the same thing twice — each test should verify one behavior -- Test code should compile without warnings - -### Integration tests -- Test cross-module interactions -- Test upgrade paths end-to-end -- Test with realistic parameter values (not just toy examples) - -### Advanced techniques -- **Fuzzing** (Echidna) — find edge cases through random transaction sequences -- **Symbolic execution** (Manticore) — prove properties mathematically -- **Mutation testing** — verify that tests catch intentional bugs -- **Differential testing** — compare assembly/optimized code against reference implementation - ---- - -## 10. Static Analysis - -### Slither -Run on every check-in. Triage and resolve all findings. - -**Key detectors:** -- Reentrancy vulnerabilities -- Uninitialized state variables -- Unused return values -- Incorrect visibility -- Shadowed state variables -- Unchecked low-level calls - -**Key printers:** -- `inheritance-graph` — check for shadowing and C3 linearization issues -- `function-summary` — review visibility and access controls -- `vars-and-auth` — review which functions write to which state variables -- `human-summary` — get a high-level overview of contract complexity - -**Specialized tools:** -- `slither-check-upgradeability` — proxy safety checks -- `slither-check-erc` — ERC conformance verification -- `slither-prop` — auto-generate security properties for ERC20 - ---- - -## 11. Fuzzing with Echidna - -### When to use -- State machine validation — verify no invalid states are reachable -- Access control — verify only authorized users can perform actions -- Arithmetic properties — verify invariants hold across random inputs -- Complex multi-transaction scenarios that are hard to unit test - -### Property types -1. **Boolean properties** — functions that return `true` if invariant holds -2. **Assertions** — `assert()` statements that must never fail -3. **Optimization** — find inputs that maximize/minimize a value - -### Writing effective properties -```solidity -// Good: specific, testable invariant -function echidna_total_supply_invariant() public view returns (bool) { - return token.totalSupply() == initialSupply + totalMinted - totalBurned; -} - -// Good: access control check -function echidna_only_owner_can_pause() public view returns (bool) { - if (msg.sender != owner) { - return !paused; // non-owners should never be able to pause - } - return true; -} -``` - -### Best practices -- Start with simple properties, iterate toward complexity -- Use filtering (modulo operator) to constrain inputs -- Collect corpus for coverage analysis -- Run periodically in CI, not just once -- Handle ETH: use `maxValue` config for payable functions - ---- - -## 12. Security Properties & Invariants - -### Categories of properties to verify - -| Category | What to check | Recommended tool | -|----------|---------------|------------------| -| **State machine** | No invalid state reachable; all valid states reachable; no trapped states | Echidna, Manticore | -| **Access control** | Only authorized users can perform actions; no privilege escalation | Slither, Echidna | -| **Arithmetic** | No overflow/underflow; rounding is correct; precision loss bounded | Manticore, Echidna | -| **Inheritance** | No shadowing; correct C3 linearization; `super` calls not missed | Slither | -| **External interactions** | Resilient to malicious external contracts; oracle manipulation handled | Echidna, Manticore | -| **Standard conformance** | ERC20/ERC721 behavior matches specification | Slither, Echidna | - -### What automated tools CANNOT easily find -- Privacy violations (all transactions are public in the mempool) -- Front-running / sandwich attacks / MEV -- Cryptographic implementation flaws -- Risky interactions with external DeFi protocols -- Social engineering or off-chain vulnerabilities - -### Transaction ordering risks (MEV) -- Identify and document all front-running opportunities -- Use time delays and slippage checks where applicable -- Use tamper-resistant oracles -- Test privileged operations for ordering risks -- Document known MEV opportunities visibly for users - ---- - -## 13. Code Maturity Checklist - -Self-evaluation framework (rate each area: Missing / Weak / Moderate / Satisfactory / Strong): - -### Arithmetic -- [ ] Explicit overflow protection (Solidity 0.8+ or equivalent) -- [ ] All `unchecked` blocks justified and documented -- [ ] Specification matches code for all formulas -- [ ] Rounding direction explicit for all precision-losing operations -- [ ] Parameter ranges bounded and documented -- [ ] Automated testing (fuzzing/formal methods) covers arithmetic - -### Access Controls -- [ ] All privileged functions have access control -- [ ] Principle of least privilege followed -- [ ] Different roles with non-overlapping privileges -- [ ] Two-step processes for privileged EOA operations -- [ ] Key loss/leakage does not compromise the system - -### Complexity Management -- [ ] Functions have low cyclomatic complexity (< 11) -- [ ] No unnecessary code duplication -- [ ] Clear naming conventions applied consistently -- [ ] Types enforce correctness where possible -- [ ] Each function has a specific, documented purpose - -### Testing & Verification -- [ ] All normal use cases tested -- [ ] All tests pass -- [ ] Code coverage measured and reported -- [ ] Automated testing (fuzzing) used for critical components -- [ ] Tests run in CI/CD pipeline -- [ ] Integration tests implemented -- [ ] Test cases are isolated (no order dependency) - -### Documentation -- [ ] System architecture documented with diagrams -- [ ] All critical functions documented (NatSpec) -- [ ] Known risks and limitations documented -- [ ] Glossary of terms exists -- [ ] User stories cover all operations -- [ ] Invariants clearly defined in documentation - -### Low-level Code -- [ ] Assembly usage is limited and justified -- [ ] Inline comments present for every assembly operation -- [ ] High-level reference implementation exists for complex assembly -- [ ] Differential fuzzing validates assembly against reference -- [ ] No re-implementation of well-established library functionality - ---- - -## 14. Deployment & Incident Response - -### Pre-deployment -- Document the full deployment process (including upgrade/migration steps) -- Write and test post-deployment verification scripts -- Use fork testing to validate deployment on a mainnet fork -- Freeze a stable commit before deployment - -### Post-deployment -- Monitor contracts — observe logs, set up alerts -- Publish security contact information -- Secure privileged wallets (hardware wallets, multisig) -- Have an incident response plan ready - -### Incident response plan -**Application design considerations:** -- Identify which components should be pausable, migratable, upgradeable -- Assess impact of pausing on dependent contracts -- Define system invariants to monitor - -**Documentation to prepare:** -- Runbook of common emergency actions (pause, key rotation, upgrade) -- How to interpret event emissions -- How to access wallets with special roles -- Deployment/upgrade verification procedures -- Stakeholder contact procedures - -**Process:** -- Designate incident roles: technical lead, communication lead, legal lead -- Conduct periodic training and incident response exercises -- Set up monitoring tools (third-party + in-house) -- Consider automated responses (auto-pause on suspicious activity) - -**Threat intelligence:** -- Monitor similar protocols for vulnerabilities -- Follow dependency communication channels -- Maintain contact with dependency maintainers - ---- - -## 15. Pre-Audit Checklist - -Before submitting code for security review: - -### Resolve easy issues -- [ ] Run Slither — triage all findings -- [ ] Achieve high test coverage -- [ ] Remove dead code, unused libraries, stale features -- [ ] If upgradeable, run `slither-check-upgradeability` -- [ ] If ERC20/721, run `slither-check-erc` - -### Make code accessible -- [ ] Provide a detailed list of in-scope files -- [ ] Clear build instructions (verified on fresh environment) -- [ ] Frozen commit hash / branch / release -- [ ] Identify boilerplate, dependencies, and forked code differences - -### Documentation -- [ ] Flowcharts and sequence diagrams for primary workflows -- [ ] User stories -- [ ] On-chain / off-chain assumptions (oracles, bridges, data validation) -- [ ] Actor list with roles and privileges -- [ ] Function documentation with inline comments for complex areas -- [ ] System and function invariants documented -- [ ] Parameter ranges (min/max) documented -- [ ] Arithmetic formulas mapped to specification with precision loss expectations -- [ ] Glossary of terms - ---- - -## 16. EVM Internals Quick Reference - -### Key concepts -- **Two's complement** — negative numbers represented by flipping bits + 1: `-a = ~a + 1` -- **Signed vs unsigned opcodes** — use `slt`/`sgt` for signed comparisons, `lt`/`gt` for unsigned -- **Sub-32-byte types** — require `signextend` or explicit bounds checking; Solidity may optimize away cleanup -- **Division by zero** — EVM returns 0 (no revert); Solidity adds a check automatically outside assembly - -### Critical opcodes for security -| Opcode | Note | -|--------|------| -| `DELEGATECALL` | Executes in caller's storage context — proxy pattern foundation | -| `SELFDESTRUCT` | Deprecated post-Dencun but still exists — can force-send ETH | -| `CREATE2` | Deterministic address — can be used for metamorphic contracts | -| `CALL` | Returns true for addresses with no code — always verify | -| `SSTORE`/`SLOAD` | Expensive — batch storage operations; use transient storage (EIP-1153) where appropriate | - -### Gas awareness -- Storage writes (`SSTORE`) are the most expensive operation (~20K gas for cold, 5K for warm) -- Avoid unbounded loops that could exceed block gas limit -- Pack storage variables into 32-byte slots when possible -- Use `calldata` instead of `memory` for read-only function parameters - ---- - -## References - -- [Trail of Bits — Building Secure Contracts](https://github.com/crytic/building-secure-contracts) -- [Slither — Static Analysis](https://github.com/crytic/slither) -- [Echidna — Fuzzing](https://github.com/crytic/echidna) -- [Manticore — Symbolic Execution](https://github.com/trailofbits/manticore) -- [OpenZeppelin Contracts](https://github.com/OpenZeppelin/openzeppelin-contracts) -- [EVM Codes Reference](https://evm.codes) -- [Solidity Documentation](https://docs.soliditylang.org) diff --git a/docs/SPEC_VALIDATOR_REGISTRATION.md b/docs/SPEC_VALIDATOR_REGISTRATION.md deleted file mode 100644 index 3e499fce4..000000000 --- a/docs/SPEC_VALIDATOR_REGISTRATION.md +++ /dev/null @@ -1,284 +0,0 @@ ---- -title: Validator Registration — All State Combinations - ---- - -# Validator Registration — All State Combinations - -**Scope:** `registerValidator` / `bulkRegisterValidator` → `_bulkRegisterValidator` -**Date:** Feb 16, 2026 - -The registration path executes these checks in order: -1. Input validation (publicKeys length, sharesData length, operatorIds length) -2. Public key registration (`ValidatorLib.registerPublicKey`) -3. Cluster validation (`validateClusterOnRegistration`) -4. Balance update (`cluster.balance += msg.value`) -5. Operator loop (`updateClusterOperatorsOnRegistration`): - - Sorted/unique check - - `ensureOperatorExist(operatorSt)` ← `isExistingCluster` param removed ✅ - - `ensureETHDefaults(operatorSt)` ← writes to storage - - `operator = operatorSt` ← memory copy AFTER defaults - - Whitelist check (if private) - - `updateSnapshot(operator, operatorId)` ← memory - - `ethValidatorCount += delta` (limit check) - - Accumulate fee + index - - `s.operators[operatorId] = operator` ← write back full struct -7. Cluster data update + fee deduction (`updateClusterData` → `updateBalanceWithEB`) -8. DAO update (`sp.updateDAO`) -9. Liquidation check (`isLiquidatableWithEB`) -10. Store cluster hash (`s.ethClusters[hashedCluster]`) -11. EB snapshot update (if explicit tracking) - ---- - -## A. Operator State Combinations - -### Operator States (per operator in the cluster) - -| # | State | `owner` | `snapshot.block` | `ethSnapshot.block` | `fee` (SSV) | `ethFee` | `ethValidatorCount` | How created | -|---|-------|---------|-------------------|---------------------|-------------|----------|---------------------|-------------| -| O1 | **Post-upgrade operator** (normal) | ≠ 0 | **0** | > 0 | 0 | > 0 | any | `registerOperator` now only sets `ethSnapshot.block`. `snapshot.block` stays 0 — new operators are ETH-only. | -| O2 | **Post-upgrade free operator** | ≠ 0 | **0** | > 0 | 0 | 0 | any | `registerOperator(fee=0)`. `snapshot.block` stays 0. | -| O3 | **Pre-upgrade operator (never migrated)** | ≠ 0 | > 0 | **0** | > 0 | **0** | 0 | Created before ETH upgrade; never had ETH interaction | -| O4 | **Pre-upgrade free operator (never migrated)** | ≠ 0 | > 0 | **0** | 0 | **0** | 0 | Created before ETH upgrade with fee=0 | -| O5 | **Pre-upgrade, partially migrated** | ≠ 0 | > 0 | > 0 | > 0 | > 0 | ≥ 0 | Had `ensureETHDefaults` called once (via prior registration or `declareOperatorFee`) | -| O6 | **Removed operator** | **≠ 0** ⚠️ | **0** | **0** | 0 | 0 | 0 | `removeOperator` → `_resetOperatorState` zeros fees/blocks/counts but **NOT owner** | -| O7 | **Never existed** | **0** | **0** | **0** | 0 | 0 | 0 | Default storage (operatorId never registered) | -| O8 | **Removed but had preserved index** | **≠ 0** ⚠️ | **0** | **0** | 0 | 0 | 0 | Same as O6 — `_resetOperatorState` zeros fees/blocks/counts but **NOT owner**; `ethSnapshot.index` preserved from `updateSnapshotsSt` call before reset | - -### What happens to each operator state during registration - -| Operator State | `ensureOperatorExist` | `ensureETHDefaults` | `updateSnapshot` (memory) | Net Result | Issues | -|---|---|---|---|---|---| -| **O1** New (snapshot = 0, ethSnapshot > 0, ethFee > 0) | ✅ Pass (`owner ≠ 0`, `ethSnapshot.block > 0`) | Outer guard: `ethSnapshot.block == 0 \|\| snapshot.block == 0` → **true** (snapshot = 0). Inner `ethSnapshot.block == 0` → false, skips init. Fee check: `ethFee == 0` → false (ethFee > 0), skips. **No-op but enters function body every time.** | Normal ETH snapshot update. `blockDiff * ethFee` accrued. | ✅ Correct but wasteful | `ensureETHDefaults` outer guard always true for O1/O2 — minor gas waste | -| **O2** New free (snapshot = 0, ethSnapshot > 0, ethFee = 0) | ✅ Pass | Same as O1 — outer guard true, inner checks skip. | `blockDiffEthFee = 0`. No accrual. | ✅ Correct but wasteful | Same as O1 | -| **O3** Pre-upgrade (snapshot > 0, ethSnapshot = 0, fee > 0, ethFee = 0) | ✅ Pass (`owner ≠ 0`, `snapshot.block > 0`) | Outer guard: `ethSnapshot.block == 0` → **true**. Inner: sets `ethSnapshot.block = block.number`, `ethSnapshot.balance = 0`. Fee check: `ethFee == 0 && fee != 0` → sets `ethFee = defaultOperatorEthFee()`. **Written to storage.** | Memory copy happens AFTER defaults. Gets correct `ethSnapshot.block` and `ethFee`. Normal snapshot from current block (blockDiff = 0, no accrual). | ✅ Correct — this is the intended migration path | None | -| **O4** Pre-upgrade free (snapshot > 0, ethSnapshot = 0, fee = 0, ethFee = 0) | ✅ Pass | Outer guard → true. Inner: sets `ethSnapshot.block`. Fee check: `ethFee == 0 && fee == 0` → **skips fee assignment**. `ethFee` stays 0. | Memory copy gets `ethFee = 0`. No accrual. | ✅ Correct — free operator stays free | None | -| **O5** Partially migrated (both blocks > 0, ethFee > 0) | ✅ Pass | Outer guard → false (both blocks > 0). Skips. | Normal snapshot update. | ✅ Correct | None | -| **O6** Removed (owner ≠ 0, both blocks = 0) | ❌ **REVERT** `OperatorDoesNotExist` via Check 2 (`ethSnapshot.block == 0 && snapshot.block == 0`). Note: Check 1 (`owner == address(0)`) does **NOT** fire because owner is preserved. | Never reached | Never reached | Registration fails | ⚠️ Correct outcome, but relies solely on Check 2. Check 1 is useless here — `owner ≠ 0` for removed operators. | -| **O7** Never existed (owner = 0, all zeros) | ❌ **REVERT** `OperatorDoesNotExist` via `owner == address(0)` OR both blocks == 0. | Never reached | Never reached | Registration fails | ✅ Correct behavior | -| **O8** Removed with preserved index | ❌ **REVERT** `OperatorDoesNotExist` via Check 2 (both blocks = 0). `owner ≠ 0` so Check 1 does not fire. | Never reached | Never reached | Registration fails | ⚠️ Same as O6 — correct outcome but Check 1 is dead | - -### Edge case: `ensureETHDefaults` re-entry on subsequent registrations - -| Operator State | 1st Registration | 2nd Registration (same operator, different cluster) | -|---|---|---| -| **O3** Pre-upgrade | `ensureETHDefaults` initializes `ethSnapshot.block` and `ethFee`. After write-back: state becomes O5. | `ensureETHDefaults` outer guard → false (both blocks > 0). Skips. Normal path. ✅ | -| **O4** Pre-upgrade free | `ensureETHDefaults` initializes `ethSnapshot.block`. `ethFee` stays 0. After write-back: both blocks > 0, ethFee = 0. | Outer guard → false. Skips. ✅ | - -### ⚠️ `snapshot.block == 0` is now the normal state for new operators - -After removing `op.snapshot.block = blockNum` from `registerOperator`, **all new post-upgrade operators have `snapshot.block == 0` and `ethSnapshot.block > 0`**. This is intentional — new operators are ETH-only and should never participate in SSV clusters. - -**Paths that create `ethSnapshot.block > 0, snapshot.block == 0`:** - -| Path | Creates this state? | Explanation | -|---|---|---| -| `registerOperator` (current) | **YES** ✅ | Only sets `ethSnapshot.block`. This is the new normal for O1/O2. | -| `ensureETHDefaults` (on O3/O4) | **YES** | Sets `ethSnapshot.block` on storage. Caller writes back memory struct preserving original `snapshot.block > 0`, so for pre-upgrade operators this doesn't create the mismatch. But `declareOperatorFee` calls it on storage directly. | - -**Impact of `snapshot.block == 0` on `ensureETHDefaults` outer guard:** - -The guard `ethSnapshot.block == 0 || snapshot.block == 0` is now **always true** for new operators (O1/O2). This means `ensureETHDefaults` enters its body on every registration call for new operators, even though both inner checks (`ethSnapshot.block == 0` and `ethFee == 0 && fee != 0`) evaluate to false and skip. This is a minor gas waste. - -**Suggested simplification:** Change the outer guard to only check `ethSnapshot.block == 0`: -```solidity -if (operator.ethSnapshot.block == 0) { - operator.ethSnapshot.block = uint32(block.number); - operator.ethSnapshot.balance = PACKED_ETH_ZERO; - if (operator.ethFee.eq(PACKED_ETH_ZERO) && operator.fee.neq(PACKED_SSV_ZERO)) { - operator.ethFee = defaultOperatorEthFee(); - } -} -``` -This is sufficient because: -- For O3/O4 (pre-upgrade): `ethSnapshot.block == 0` → enters, initializes correctly -- For O1/O2 (new): `ethSnapshot.block > 0` → skips entirely (correct, already initialized) -- For O5 (migrated): both blocks > 0 → skips (correct) -- The `|| snapshot.block == 0` condition only served to catch a state that didn't exist before; now it catches O1/O2 needlessly - ---- - -## B. Cluster State Combinations - -### Cluster States - -| # | State | `s.ethClusters[hash]` | `s.clusters[hash]` | `cluster.active` | `cluster.validatorCount` | Description | -|---|-------|----------------------|--------------------|--------------------|--------------------------|-------------| -| C1 | **New cluster (never existed)** | 0 | 0 | true (required) | 0 (required) | First-time registration | -| C2 | **Existing active ETH cluster** | ≠ 0 | 0 | true | > 0 | Adding validators to existing ETH cluster | -| C3 | **Existing active ETH cluster (0 validators)** | ≠ 0 | 0 | true | 0 | All validators removed but cluster not liquidated | -| C4 | **Liquidated ETH cluster** | ≠ 0 | 0 | **false** | any | Cluster was liquidated | -| C5 | **Existing SSV cluster (active)** | 0 | ≠ 0 | true | > 0 | Legacy SSV cluster, not migrated | -| C6 | **Existing SSV cluster (liquidated)** | 0 | ≠ 0 | false | any | Legacy SSV cluster, liquidated | -| C7 | **Both exist** | ≠ 0 | ≠ 0 | — | — | Should never happen (INV-G3) | - -### What happens to each cluster state during registration - -| Cluster State | `validateClusterOnRegistration` | `updateClusterOnRegistration` | Result | Issues | -|---|---|---|---|---| -| **C1** New (both = 0) | `clusterData == 0 && clusterDataSSV == 0`. Checks: `validatorCount == 0`, `networkFeeIndex == 0`, `index == 0`, `balance == 0`, `active == true`. Must all pass. | Operators get `ensureOperatorExist(op)`. Normal path. | ✅ New ETH cluster created | None | -| **C2** Active ETH (ethClusters ≠ 0) | `clusterData ≠ 0`. Checks `clusterData == hashClusterData(cluster)` (state must match). Then `validateClusterIsNotLiquidated` (must be active). | Normal fee settlement + validator addition. | ✅ Validators added | None | -| **C3** Active ETH, 0 validators | Same as C2. Hash must match. Must be active. | Fee settlement (no fees since 0 validators). Adds validators. | ✅ Re-populating empty cluster | None | -| **C4** Liquidated ETH | `clusterData ≠ 0`. Hash check passes. Then `validateClusterIsNotLiquidated` → **`active == false`** | — | ❌ **REVERT** `ClusterIsLiquidated` | ✅ Correct — must reactivate first | -| **C5** Active SSV cluster | `clusterData == 0 && clusterDataSSV ≠ 0` → **REVERT** `IncorrectClusterVersion` | — | ❌ **REVERT** `IncorrectClusterVersion` | ✅ Correct — must migrate first | -| **C6** Liquidated SSV cluster | Same as C5 — `clusterData == 0 && clusterDataSSV ≠ 0` | — | ❌ **REVERT** `IncorrectClusterVersion` | ✅ Correct | -| **C7** Both exist | `clusterData ≠ 0` (ETH checked first). Hash check + active check. | Would proceed as C2. | ⚠️ Shouldn't happen. If it does, SSV data is orphaned. | INV-G3 violation | - -### Cluster state vs supplied `cluster` parameter mismatches - -| Scenario | What happens | -|---|---| -| New cluster but `validatorCount > 0` | `validateClusterOnRegistration` → REVERT `IncorrectClusterState` | -| New cluster but `active = false` | REVERT `IncorrectClusterState` | -| New cluster but `balance > 0` | REVERT `IncorrectClusterState` | -| Existing cluster but wrong state | `hashClusterData(cluster) != stored` → REVERT `IncorrectClusterState` | -| Existing cluster, correct state, but liquidated | REVERT `ClusterIsLiquidated` | - ---- - -## C. Operator × Cluster Cross-Product - -### Registration with mixed operator states (4 operators in a cluster) - -| Scenario | Operators | Cluster | Result | Notes | -|---|---|---|---|---| -| All normal, new cluster | [O1, O1, O1, O1] | C1 | ✅ Success | Standard path | -| All normal, existing cluster | [O1, O1, O1, O1] | C2 | ✅ Success | Standard path | -| Mix of normal + pre-upgrade | [O1, O3, O1, O3] | C1 | ✅ Success | O3 operators get ETH defaults initialized | -| All pre-upgrade, new cluster | [O3, O3, O3, O3] | C1 | ✅ Success | All get `ensureETHDefaults` | -| One removed operator | [O1, O6, O1, O1] | C1 or C2 | ❌ REVERT `OperatorDoesNotExist` | Fails on O6 | -| One never-existed | [O1, O7, O1, O1] | C1 or C2 | ❌ REVERT `OperatorDoesNotExist` | Fails on O7 | -| Free + paid mix | [O1, O2, O1, O2] | C1 | ✅ Success | Free operators contribute 0 to `cumulativeFee` | -| All free operators | [O2, O2, O2, O2] | C1 | ✅ Success | `burnRate = 0`. Only network fee applies. | -| Pre-upgrade free | [O4, O4, O4, O4] | C1 | ✅ Success | `ethFee` stays 0. No operator fee accrual. | -| Private operator, caller not whitelisted | [O1(private), O1, O1, O1] | C1 | ❌ REVERT `CallerNotWhitelistedWithData` | Whitelist check fails | -| Operator at validator limit | [O1(at limit), O1, O1, O1] | C2 | ❌ REVERT `ExceedValidatorLimitWithData` | `ethValidatorCount + delta > validatorsPerOperatorLimit` | -| SSV cluster with ETH operators | [O1, O1, O1, O1] | C5 | ❌ REVERT `IncorrectClusterVersion` | Cluster version mismatch | - ---- - -## D. EB (Effective Balance) State Combinations - -### EB States per cluster - -| # | State | `clusterEB[hash].vUnits` | `operatorEthVUnits[opId]` | Description | -|---|-------|--------------------------|---------------------------|-------------| -| E1 | **No EB tracking (implicit)** | 0 | 0 | Default: each validator = 32 ETH = `VUNITS_PRECISION` | -| E2 | **Explicit EB, at baseline** | `validatorCount * VUNITS_PRECISION` | 0 | Oracle set EB = 32 ETH/validator (no deviation) | -| E3 | **Explicit EB, above baseline** | > `validatorCount * VUNITS_PRECISION` | > 0 | Oracle set EB > 32 ETH/validator (positive deviation) | -| E4 | **Explicit EB, at max** | `validatorCount * ebToVUnits(2048)` | large positive | Oracle set EB = 2048 ETH/validator | - -### EB impact during registration - -| EB State | `updateBalanceWithEB` (fee deduction) | EB snapshot update (line 143-154) | Impact | -|---|---|---|---| -| **E1** Implicit | `getVUnits` returns `validatorCount * VUNITS_PRECISION` (OLD count, before increment). Fee deduction uses baseline vUnits. | `ebSnapshot.vUnits == 0` → skip. No EB update. | ✅ Correct — baseline adjusts automatically via `validatorCount` | -| **E2** Explicit, baseline | `getVUnits` returns stored vUnits (= old `validatorCount * VUNITS_PRECISION`). Fee deduction same as E1. | `ebSnapshot.vUnits > 0` → adds `delta * VUNITS_PRECISION`. | ✅ Correct — explicit tracking maintained | -| **E3** Explicit, above baseline | `getVUnits` returns stored vUnits (includes deviation). Fee deduction is **higher** than baseline (proportional to actual EB). | `ebSnapshot.vUnits > 0` → adds `delta * VUNITS_PRECISION` (baseline for new validators). Deviation unchanged. | ✅ Correct — new validators get baseline, existing deviation preserved | -| **E4** Explicit, at max | Same as E3 but with maximum deviation. Higher fee deduction. | Same as E3. | ✅ Correct | - -### EB + operator vUnits consistency during registration - -``` -BEFORE registration: - daoTotalEthVUnits = sum(all cluster effective vUnits) - operatorEthVUnits[opId] = sum(deviations from all clusters using this operator) - -AFTER registration (N new validators): - sp.updateDAO(true, N) → daoTotalEthVUnits += N * VUNITS_PRECISION (baseline) - operator.ethValidatorCount += N (baseline in operator) - if explicit EB: ebSnapshot.vUnits += N * VUNITS_PRECISION (baseline in cluster) - operatorEthVUnits[opId] NOT changed (deviation unchanged) - -CONSISTENCY CHECK: - New effective vUnits for cluster = old vUnits + N * VUNITS_PRECISION ✅ - New effective vUnits for operator = operatorEthVUnits[opId] + (ethValidatorCount + N) * VUNITS_PRECISION ✅ - New daoTotalEthVUnits = old + N * VUNITS_PRECISION ✅ -``` - -**No deviation change on registration → `operatorEthVUnits` correctly untouched.** - ---- - -## E. Fee Deduction During Registration (Existing Clusters) - -For existing clusters (C2, C3), `updateClusterData` is called which runs `updateBalanceWithEB`: - -``` -vUnits = getVUnits(hashedCluster, cluster.validatorCount) // OLD validatorCount -idxOp = newOperatorIndex - cluster.index -idxNet = currentNetworkFeeIndex - cluster.networkFeeIndex -operatorFeeUnits = (idxOp * vUnits) / VUNITS_PRECISION -networkFeeUnits = (idxNet * vUnits) / VUNITS_PRECISION -usage = (operatorFeeUnits + networkFeeUnits) * ETH_DEDUCTED_DIGITS -cluster.balance -= usage -``` - -| Scenario | vUnits used | Fee impact | Notes | -|---|---|---|---| -| Existing cluster, implicit EB | `oldValidatorCount * VUNITS_PRECISION` | Standard per-validator fee | ✅ | -| Existing cluster, explicit EB above baseline | Stored vUnits (includes deviation) | Higher fee (proportional to actual EB) | ✅ | -| Existing cluster, 0 validators (C3) | 0 (implicit) or stored (explicit) | If implicit: 0 fees. If explicit with vUnits > 0: fees on deviation only. | ⚠️ C3 with explicit EB and vUnits > 0 but validatorCount = 0 means pure deviation — should this be possible? | -| New cluster (C1) | `0 * VUNITS_PRECISION = 0` | 0 fees (no prior validators) | ✅ Correct — no fees to settle | - -### ⚠️ Edge: Empty cluster (C3) with explicit EB tracking - -If a cluster had validators, got an EB update (explicit tracking), then all validators were removed: -- `_bulkRemoveValidator` subtracts baseline from `ebSnapshot.vUnits` -- If `validatorCount == 0`: cleans up remaining deviation, sets `ebSnapshot.vUnits = 0` - -So when re-registering to C3, `ebSnapshot.vUnits` should be 0 → falls back to implicit. **This is correct.** - ---- - -## F. Liquidation Threshold Check - -After all updates, `isLiquidatableWithEB` is called: - -``` -if (cluster.validatorCount == 0) return false; // can't liquidate empty cluster -if (cluster.balance < minimumLiquidationCollateral) return true; -vUnits = getVUnits(hashedCluster, cluster.validatorCount); // NEW validatorCount -rate = burnRate + networkFee; -threshold = (minimumBlocksBeforeLiquidation * rate * vUnits) / VUNITS_PRECISION * ETH_DEDUCTED_DIGITS; -return cluster.balance < threshold; -``` - -| Scenario | vUnits | Threshold | Notes | -|---|---|---|---| -| New cluster, 1 validator, implicit EB | `1 * 10000` | `minBlocks * (burnRate + netFee) * 10000 / 10000 * 100000` | Standard | -| Existing cluster, adding validators, implicit EB | `(old + new) * 10000` | Higher threshold (more validators) | Must deposit enough to cover | -| Existing cluster, explicit EB above baseline | Stored vUnits + new baseline | Even higher threshold | EB amplifies required collateral | -| All free operators, no network fee | vUnits | `minBlocks * 0 * vUnits = 0` | Only `minimumLiquidationCollateral` matters | - ---- - -### ⚠️ Cleanup / Simplification - -| Area | Issue | Severity | -|---|---|---| -| `ensureETHDefaults` outer guard :143 | `\|\| operator.snapshot.block == 0` now always true for O1/O2. Function body entered on every call but does nothing. Simplify to `ethSnapshot.block == 0`. | Low (gas waste only) | -| `ensureOperatorExist` :160-162 | `(ethSnapshot.block == 0 && snapshot.block == 0)` can be simplified to `ethSnapshot.block == 0` since `ethSnapshot.block` is now the canonical existence marker. | Low (clarity) | -| `checkOwner` :132 | `snapshot.block == 0 && ethSnapshot.block == 0` can be simplified to `ethSnapshot.block == 0`. | Low (clarity) | -| `updateClusterOperatorsMigration` :383 | `snapshot.block == 0 && ethSnapshot.block == 0` → skip. Can simplify to `ethSnapshot.block == 0`. | Low (clarity) | -| `_resetOperatorState` doesn't zero `owner` | Removed operators retain their `owner`. `checkOwner` passes for the original owner — only the block == 0 check prevents further actions. | Medium (latent risk — defense in depth suggests zeroing `owner`) | - -### 🔍 Worth Verifying in Tests - -| # | Scenario | What to verify | -|---|---|---| -| 1 | Register with 4 new operators (O1) on new cluster | All pass `ensureOperatorExist`. `ensureETHDefaults` is a no-op. Cluster created correctly. | -| 2 | Register with mix of O1 + O3 on existing cluster | O3 gets defaults. O1 unchanged. Fee settlement correct. | -| 3 | Register with all free operators (O2) | `burnRate = 0`. Only network fee in liquidation check. | -| 4 | Register on empty cluster (C3) after all validators removed | Cluster re-populated. EB tracking reset to implicit. | -| 5 | Register on cluster with explicit EB above baseline (E3) | Fee deduction uses higher vUnits. New validators get baseline only. | -| 6 | Register that would exceed `validatorsPerOperatorLimit` | Reverts `ExceedValidatorLimitWithData`. | -| 7 | Register with insufficient deposit (fails liquidation check) | Reverts `InsufficientBalance`. | -| 8 | Register same public key twice | Second call reverts `ValidatorAlreadyExistsWithData`. | -| 9 | Register to SSV cluster (not migrated) | Reverts `IncorrectClusterVersion`. | -| 10 | Register to liquidated cluster | Reverts `ClusterIsLiquidated`. | -| 11 | Liquidate ETH cluster with new operators (O1) | `ethValidatorCount` must be decremented. Fixed in `_liquidateIfNeeded`. Verify it works. | -| 12 | Liquidate ETH cluster with pre-upgrade operators (O5) | `ethValidatorCount` decremented correctly (both blocks > 0). | -| 13 | New operator (O1) — `getOperatorById` returns `isActive = true` | ETH view correct. | -| 14 | New operator (O1) — `getOperatorByIdSSV` returns `isActive = false` | SSV view correct — not an SSV operator. | diff --git a/docs/UPGRADE_PLAYBOOK.md b/docs/UPGRADE_PLAYBOOK.md index 4cfff5edd..130921c7f 100644 --- a/docs/UPGRADE_PLAYBOOK.md +++ b/docs/UPGRADE_PLAYBOOK.md @@ -15,8 +15,11 @@ This playbook is aligned with the repository deployment flow in `deployments/REA - `just deploy mainnet` - `just generate-safe-batch mainnet` +- `just simulate-safe-upgrade mainnet ` - `just verify-upgrade mainnet` +For the exact local SAFE fork dry-run flow, see [SAFE_UPGRADE_SIMULATION_PLAYBOOK.md](./SAFE_UPGRADE_SIMULATION_PLAYBOOK.md). + ## Version Scope - Current on-chain version: `v1.2.0` diff --git a/foundry.toml b/foundry.toml index 60f9dd4db..ac05baf95 100644 --- a/foundry.toml +++ b/foundry.toml @@ -6,7 +6,7 @@ libs = ["node_modules", "lib"] auto_detect_solc = true via_ir = true optimizer = true -optimizer_runs = 200 +optimizer_runs = 10000 evm_version = "cancun" remappings = [ diff --git a/lib/forge-std b/lib/forge-std deleted file mode 160000 index 1801b0541..000000000 --- a/lib/forge-std +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 1801b0541f4fda118a10798fd3486bb7051c5dd6 diff --git a/scripts/common/fork-test.ts b/scripts/common/fork-test.ts index 3a02d7e37..e35fa1c93 100644 --- a/scripts/common/fork-test.ts +++ b/scripts/common/fork-test.ts @@ -5,6 +5,7 @@ export type ForkConfigFile = { ssvNetworkProxy?: string; ssvNetworkAddress?: string; ssvNetworkViews?: string; + initialStakeAmount?: string | number; forkBlockNumber?: string | number; deployments?: { forkBlockNumber?: string | number; @@ -140,5 +141,6 @@ export function buildForkTestEnv( FORK_DEFAULT_UNSTAKE_COOLDOWN: toEnvValue( pp.unstakeCooldownDuration ?? config.unstakeCooldownDuration ?? config.cooldownDuration ), + FORK_INITIAL_STAKE_AMOUNT: toEnvValue(config.initialStakeAmount ?? 0), }; } diff --git a/scripts/simulate-safe-upgrade.ts b/scripts/simulate-safe-upgrade.ts new file mode 100644 index 000000000..266eeb1ca --- /dev/null +++ b/scripts/simulate-safe-upgrade.ts @@ -0,0 +1,707 @@ +import { spawn } from "node:child_process"; +import { readFile, writeFile } from "node:fs/promises"; +import { join, resolve } from "node:path"; +import { pathToFileURL } from "node:url"; +import { + AbiCoder, + Contract, + Interface, + getAddress, + isAddress, + keccak256, + toUtf8Bytes, + zeroPadValue, +} from "ethers"; +import { getDeployer, getEthers, parseArg } from "./common/helpers.ts"; +import { + type ModuleAddresses, + type UpgradeConfig, + MODULE_ORDER, + bigintToJsonNumberOrString, + normalizeOracles, + parseOptionalArg, + parseOptionalBooleanArg, + parseQuorum, + parseUint, + requireAddress, + resolveConfigPath, + resolveCooldownDuration, + resolveDefaultOracleIds, + resolveEnvDir, + resolveProtocolParams, + LOCAL_FORK_RPC_URL, +} from "./common/config.ts"; +import { getSignerForAddress, resolveRpcUrl, canImpersonateOnNetwork } from "./common/impersonation.ts"; +import { SSVModules } from "./common/modules.ts"; +import { readOnChainValues, verifyPostUpgradeState } from "./common/verify.ts"; + +type SafeTransactionJson = { + to: string; + data: string; + value: string | number; + operation: string | number; + baseGas: string | number; + gasPrice: string | number; + gasToken: string; + nonce: string | number; + refundReceiver: string; + safeTxGas: string | number; +}; + +type NormalizedSafeTransaction = { + to: string; + data: string; + value: bigint; + operation: number; + baseGas: bigint; + gasPrice: bigint; + gasToken: string; + nonce: bigint; + refundReceiver: string; + safeTxGas: bigint; +}; + +type SafeBatchJson = { + transactions: Array<{ + to: string; + value: string; + data: string; + }>; +}; + +type ParsedDeployResult = { + networkImplementation: string; + viewsImplementation: string; + cssvToken: string; + modules: ModuleAddresses; + chainId?: string; +}; + +type MultiSendCall = { + operation: number; + to: string; + value: bigint; + data: string; +}; + +type SimulationResult = UpgradeConfig & { + simulation: { + safeAddress: string; + safeTxHash: string; + safeNonce: number | string; + postExecutionSafeNonce: number | string; + selectedApprovers: string[]; + executionBlock: number; + receiptHash: string; + }; +}; + +const SAFE_IFACE = new Interface([ + "function getOwners() view returns (address[])", + "function getThreshold() view returns (uint256)", + "function nonce() view returns (uint256)", + "function approveHash(bytes32 hashToApprove)", + "function getTransactionHash(address to,uint256 value,bytes data,uint8 operation,uint256 safeTxGas,uint256 baseGas,uint256 gasPrice,address gasToken,address refundReceiver,uint256 _nonce) view returns (bytes32)", + "function execTransaction(address to,uint256 value,bytes data,uint8 operation,uint256 safeTxGas,uint256 baseGas,uint256 gasPrice,address gasToken,address refundReceiver,bytes signatures) payable returns (bool success)", + "event ExecutionSuccess(bytes32 txHash,uint256 payment)", + "event ExecutionFailure(bytes32 txHash,uint256 payment)", +]); + +const MULTISEND_IFACE = new Interface([ + "function multiSend(bytes transactions)", +]); + +const ERC20_IFACE = new Interface([ + "function totalSupply() view returns (uint256)", + "function balanceOf(address account) view returns (uint256)", +]); + +const SSV_VIEWS_MODULE_IFACE = new Interface([ + "function CSSV_ADDRESS() view returns (address)", +]); + +const NETWORK_VIEWS_PROXY_IFACE = new Interface([ + "function ssvNetwork() view returns (address)", +]); + +const PROXY_IMPLEMENTATION_SLOT = + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"; +const SSV_STORAGE_POSITION = BigInt(keccak256(toUtf8Bytes("ssv.network.storage.main"))) - 1n; +const SSV_CONTRACTS_MAPPING_SLOT = SSV_STORAGE_POSITION + 3n; + +function assert(condition: unknown, message: string): asserts condition { + if (!condition) { + throw new Error(message); + } +} + +async function readJsonFile(filePath: string): Promise { + const raw = await readFile(filePath, "utf8"); + return JSON.parse(raw) as T; +} + +function normalizeSafeTransaction(input: SafeTransactionJson): NormalizedSafeTransaction { + const to = requireAddress(input.to, "safe tx to"); + const gasToken = requireAddress(input.gasToken, "safe tx gasToken"); + const refundReceiver = requireAddress(input.refundReceiver, "safe tx refundReceiver"); + const value = parseUint(input.value, "safe tx value"); + const baseGas = parseUint(input.baseGas, "safe tx baseGas"); + const gasPrice = parseUint(input.gasPrice, "safe tx gasPrice"); + const nonce = parseUint(input.nonce, "safe tx nonce"); + const safeTxGas = parseUint(input.safeTxGas, "safe tx safeTxGas"); + + assert(value !== undefined, "safe tx value is required"); + assert(baseGas !== undefined, "safe tx baseGas is required"); + assert(gasPrice !== undefined, "safe tx gasPrice is required"); + assert(nonce !== undefined, "safe tx nonce is required"); + assert(safeTxGas !== undefined, "safe tx safeTxGas is required"); + assert(typeof input.data === "string" && input.data.startsWith("0x"), "safe tx data must be a hex string"); + + const operation = Number.parseInt(String(input.operation), 10); + assert(Number.isInteger(operation) && operation >= 0 && operation <= 255, `Invalid safe tx operation: ${input.operation}`); + assert(operation === 1, `Expected Safe tx operation=1 (delegatecall), got ${operation}`); + + return { + to, + data: input.data, + value, + operation, + baseGas, + gasPrice, + gasToken, + nonce, + refundReceiver, + safeTxGas, + }; +} + +function resolveDeployResult(raw: any): ParsedDeployResult { + const networkImplementation = + raw?.implementations?.SSVNetworkSSVStakingUpgrade ?? + raw?.ssvNetworkStakingUpgradeImplementation ?? + raw?.deployments?.ssvNetworkStakingUpgradeImplementation; + const viewsImplementation = + raw?.implementations?.SSVNetworkViews ?? + raw?.ssvNetworkViewsImplementation ?? + raw?.deployments?.ssvNetworkViewsImplementation; + const cssvToken = + raw?.cssvToken?.address ?? + raw?.cssvToken ?? + raw?.deployments?.cssvToken; + const modulesSource = + raw?.modules ?? + raw?.deployments?.modules; + + assert(isAddress(networkImplementation), "deploy-result is missing SSVNetworkSSVStakingUpgrade implementation"); + assert(isAddress(viewsImplementation), "deploy-result is missing SSVNetworkViews implementation"); + assert(isAddress(cssvToken), "deploy-result is missing cssvToken"); + assert(modulesSource && typeof modulesSource === "object", "deploy-result is missing modules"); + + const moduleRecord = modulesSource as Record; + const modules = {} as ModuleAddresses; + for (const moduleName of MODULE_ORDER) { + const moduleAddress = moduleRecord[moduleName]; + assert(isAddress(moduleAddress), `deploy-result is missing module address for ${moduleName}`); + modules[moduleName] = getAddress(moduleAddress); + } + + return { + networkImplementation: getAddress(networkImplementation), + viewsImplementation: getAddress(viewsImplementation), + cssvToken: getAddress(cssvToken), + modules, + chainId: raw?.chainId?.toString?.() ?? raw?.deployments?.chainId?.toString?.(), + }; +} + +function parseMultiSendCalls(data: string): MultiSendCall[] { + const parsed = MULTISEND_IFACE.parseTransaction({ data }); + assert(parsed?.name === "multiSend", "safe tx data is not a multiSend(bytes) call"); + + const encodedTransactions = String(parsed.args[0]); + const bytes = encodedTransactions.slice(2); + const calls: MultiSendCall[] = []; + let offset = 0; + + while (offset < bytes.length) { + assert(offset + 2 + 40 + 64 + 64 <= bytes.length, "multiSend payload is truncated"); + const operation = Number.parseInt(bytes.slice(offset, offset + 2), 16); + offset += 2; + + const to = getAddress(`0x${bytes.slice(offset, offset + 40)}`); + offset += 40; + + const value = BigInt(`0x${bytes.slice(offset, offset + 64)}`); + offset += 64; + + const dataLength = Number(BigInt(`0x${bytes.slice(offset, offset + 64)}`)); + offset += 64; + + assert(offset + dataLength * 2 <= bytes.length, "multiSend inner calldata exceeds encoded payload length"); + const innerData = `0x${bytes.slice(offset, offset + dataLength * 2)}`; + offset += dataLength * 2; + + calls.push({ operation, to, value, data: innerData }); + } + + return calls; +} + +function assertBatchMatches(innerCalls: MultiSendCall[], batch: SafeBatchJson): void { + assert( + innerCalls.length === batch.transactions.length, + `multiSend inner call count mismatch: safe tx has ${innerCalls.length}, batch has ${batch.transactions.length}` + ); + + for (const [index, innerCall] of innerCalls.entries()) { + const batchTx = batch.transactions[index]; + const expectedTo = requireAddress(batchTx.to, `batch transaction ${index + 1} to`); + const expectedValue = BigInt(batchTx.value); + const expectedData = batchTx.data.toLowerCase(); + + assert(innerCall.operation === 0, `inner call ${index + 1} must be CALL (0), got ${innerCall.operation}`); + assert( + innerCall.to.toLowerCase() === expectedTo.toLowerCase(), + `inner call ${index + 1} target mismatch: expected ${expectedTo}, got ${innerCall.to}` + ); + assert( + innerCall.value === expectedValue, + `inner call ${index + 1} value mismatch: expected ${expectedValue}, got ${innerCall.value}` + ); + assert( + innerCall.data.toLowerCase() === expectedData, + `inner call ${index + 1} calldata mismatch` + ); + } +} + +function sortAddresses(addresses: string[]): string[] { + return [...addresses].sort((left, right) => { + const a = left.toLowerCase().slice(2); + const b = right.toLowerCase().slice(2); + return a.localeCompare(b); + }); +} + +function buildPrevalidatedSignatures(owners: string[]): string { + const sortedOwners = sortAddresses(owners); + const encoded = sortedOwners.map((owner) => { + const validator = zeroPadValue(owner, 32).slice(2); + const ignored = "0".repeat(64); + return `${validator}${ignored}01`; + }); + return `0x${encoded.join("")}`; +} + +async function readStorage(provider: any, address: string, slot: string): Promise { + return provider.send("eth_getStorageAt", [address, slot, "latest"]); +} + +function decodeAddressFromStorage(storageValue: string): string { + assert(storageValue.startsWith("0x") && storageValue.length === 66, `Unexpected storage word: ${storageValue}`); + return getAddress(`0x${storageValue.slice(-40)}`); +} + +async function readImplementationAddress(provider: any, proxyAddress: string): Promise { + const raw = await readStorage(provider, proxyAddress, PROXY_IMPLEMENTATION_SLOT); + return decodeAddressFromStorage(raw); +} + +async function readModuleAddress(provider: any, networkProxy: string, moduleId: number): Promise { + const slot = keccak256( + AbiCoder.defaultAbiCoder().encode(["uint256", "uint256"], [BigInt(moduleId), SSV_CONTRACTS_MAPPING_SLOT]) + ); + const raw = await readStorage(provider, networkProxy, slot); + return decodeAddressFromStorage(raw); +} + +async function runForkedTests(configPath: string, testPath: string | undefined): Promise { + const args = [ + "tsx", + "scripts/run-forked-tests.ts", + "--config", + configPath, + "--fork-network", + "hardhat_forked", + "--use-deployed-state", + "true", + "--strict-deployed-state", + "true", + "--allow-deployed-fallback", + "false", + "--no-gas-enforce", + "true", + ]; + + if (testPath) { + args.push("--test", testPath); + } + + console.log(`[TEST] Running forked tests with MAINNET_RPC_URL=${LOCAL_FORK_RPC_URL}`); + + await new Promise((resolvePromise, rejectPromise) => { + const child = spawn("npx", args, { + stdio: "inherit", + env: { + ...process.env, + MAINNET_RPC_URL: LOCAL_FORK_RPC_URL, + }, + }); + + child.on("error", rejectPromise); + child.on("close", (code) => { + if (code === 0) { + resolvePromise(); + return; + } + rejectPromise(new Error(`Forked tests failed with exit code ${code}`)); + }); + }); +} + +export async function main() { + const envFlag = parseOptionalArg("env") ?? "mainnet"; + const txFilePath = resolve(parseArg("tx-file")); + const targetNetwork = parseOptionalArg("network") ?? "local"; + const skipForkTests = parseOptionalBooleanArg("skip-fork-tests", false); + const testPath = parseOptionalArg("test"); + + const configPath = resolveConfigPath(envFlag); + const deployResultPath = join(resolveEnvDir(envFlag), "deploy-result.json"); + const batchPath = join(resolveEnvDir(envFlag), "multisig-batch.json"); + + const config = await readJsonFile(configPath); + const deployResult = resolveDeployResult(await readJsonFile(deployResultPath)); + const safeTx = normalizeSafeTransaction(await readJsonFile(txFilePath)); + const safeBatch = await readJsonFile(batchPath); + const outputPath = resolve( + parseOptionalArg("output") ?? join(resolveEnvDir(envFlag), `safe-simulation-result.nonce-${safeTx.nonce.toString()}.json`) + ); + + const ssvNetworkProxy = requireAddress(config.ssvNetworkProxy, "ssvNetworkProxy"); + const ssvNetworkViews = requireAddress(config.ssvNetworkViews, "ssvNetworkViews"); + const ssvTokenAddress = requireAddress(config.ssvToken, "ssvToken"); + const safeAddress = requireAddress(config.owner ?? "", "config.owner (Safe address)"); + const targetRpcUrl = resolveRpcUrl(targetNetwork); + + assert( + canImpersonateOnNetwork(targetNetwork, targetRpcUrl), + `Target network ${targetNetwork} must support impersonation; use --network local against a local fork` + ); + + const ethers = await getEthers(targetNetwork); + const providerNetwork = await ethers.provider.getNetwork(); + + for (const [label, address] of [ + ["safe", safeAddress], + ["SSVNetwork proxy", ssvNetworkProxy], + ["SSVNetworkViews proxy", ssvNetworkViews], + ["multiSend target", safeTx.to], + ] as const) { + const code = await ethers.provider.getCode(address); + assert(code !== "0x", `No contract code at ${label} ${address} on ${targetNetwork}`); + } + + const innerCalls = parseMultiSendCalls(safeTx.data); + assertBatchMatches(innerCalls, safeBatch); + console.log(`[PRE-FLIGHT] multiSend inner calls match ${batchPath} (${innerCalls.length} calls)`); + + const network = await ethers.getContractAt("SSVNetwork", ssvNetworkProxy); + const viewsProxy = await ethers.getContractAt("SSVNetworkViews", ssvNetworkViews); + const safe: any = new Contract(safeAddress, SAFE_IFACE, ethers.provider); + + const preUpgradeNetworkVersion = await network.getVersion(); + const onChainNetworkOwner = await network.owner(); + const onChainViewsOwner = await viewsProxy.owner(); + const currentSafeNonce = await safe.nonce(); + const safeOwners = (await safe.getOwners()).map((owner: string) => getAddress(owner)); + const safeThreshold = BigInt(await safe.getThreshold()); + + assert( + preUpgradeNetworkVersion === config.currentVersion, + `Pre-upgrade version mismatch: config.currentVersion=${config.currentVersion}, on-chain=${preUpgradeNetworkVersion}` + ); + assert( + onChainNetworkOwner.toLowerCase() === safeAddress.toLowerCase(), + `SSVNetwork owner mismatch: expected ${safeAddress}, got ${onChainNetworkOwner}` + ); + assert( + onChainViewsOwner.toLowerCase() === safeAddress.toLowerCase(), + `SSVNetworkViews owner mismatch: expected ${safeAddress}, got ${onChainViewsOwner}` + ); + assert( + currentSafeNonce === safeTx.nonce, + `Safe nonce mismatch: expected ${safeTx.nonce}, got ${currentSafeNonce}` + ); + assert(safeThreshold > 0n, "Safe threshold must be greater than 0"); + assert( + safeOwners.length >= Number(safeThreshold), + `Safe has ${safeOwners.length} owners but threshold is ${safeThreshold}` + ); + + console.log(`[PRE-FLIGHT] network version before execution = ${preUpgradeNetworkVersion}`); + console.log(`[PRE-FLIGHT] Safe threshold = ${safeThreshold}, owners = ${safeOwners.length}, nonce = ${currentSafeNonce}`); + + const selectedApprovers = safeOwners.slice(0, Number(safeThreshold)); + console.log(`[SAFE] Selected approvers: ${selectedApprovers.join(", ")}`); + + const safeTxHash = await safe.getTransactionHash( + safeTx.to, + safeTx.value, + safeTx.data, + safeTx.operation, + safeTx.safeTxGas, + safeTx.baseGas, + safeTx.gasPrice, + safeTx.gasToken, + safeTx.refundReceiver, + safeTx.nonce + ); + console.log(`[SAFE] safeTxHash = ${safeTxHash}`); + + for (const ownerAddress of selectedApprovers) { + const { signer, impersonated } = await getSignerForAddress(ethers, ownerAddress, true); + const ownerSafe = safe.connect(signer); + const approvalReceipt = await (await ownerSafe.approveHash(safeTxHash)).wait(); + console.log(`[SAFE] approveHash by ${ownerAddress}${impersonated ? " (impersonated)" : ""} in tx ${approvalReceipt?.hash ?? "unknown"}`); + } + + const signatures = buildPrevalidatedSignatures(selectedApprovers); + const executor = await getDeployer(ethers); + const executorAddress = await executor.getAddress(); + const executorSafe = safe.connect(executor); + + console.log(`[SAFE] Executor = ${executorAddress}`); + const staticOk = await executorSafe.execTransaction.staticCall( + safeTx.to, + safeTx.value, + safeTx.data, + safeTx.operation, + safeTx.safeTxGas, + safeTx.baseGas, + safeTx.gasPrice, + safeTx.gasToken, + safeTx.refundReceiver, + signatures + ); + assert(staticOk === true, "Safe execTransaction.staticCall returned false"); + console.log("[SAFE] execTransaction.staticCall returned true"); + + const executionTx = await executorSafe.execTransaction( + safeTx.to, + safeTx.value, + safeTx.data, + safeTx.operation, + safeTx.safeTxGas, + safeTx.baseGas, + safeTx.gasPrice, + safeTx.gasToken, + safeTx.refundReceiver, + signatures + ); + const executionReceipt = await executionTx.wait(); + assert(executionReceipt, "Missing transaction receipt for Safe execution"); + + let sawExecutionSuccess = false; + let sawExecutionFailure = false; + for (const log of executionReceipt.logs) { + try { + const parsed = SAFE_IFACE.parseLog(log); + if (parsed?.name === "ExecutionSuccess") { + sawExecutionSuccess = true; + assert(parsed.args.txHash === safeTxHash, `ExecutionSuccess txHash mismatch: expected ${safeTxHash}, got ${parsed.args.txHash}`); + } + if (parsed?.name === "ExecutionFailure") { + sawExecutionFailure = true; + } + } catch { + // ignore unrelated logs + } + } + + assert(!sawExecutionFailure, "Safe emitted ExecutionFailure"); + assert(sawExecutionSuccess, "Safe did not emit ExecutionSuccess"); + + const postExecutionSafeNonce = await safe.nonce(); + assert( + postExecutionSafeNonce === safeTx.nonce + 1n, + `Safe nonce did not increment correctly: expected ${safeTx.nonce + 1n}, got ${postExecutionSafeNonce}` + ); + console.log(`[SAFE] Execution succeeded in tx ${executionReceipt.hash}`); + + const postUpgradeVersion = await network.getVersion(); + const postUpgradeViewsVersion = await viewsProxy.getVersion(); + assert( + postUpgradeVersion === config.targetVersion, + `SSVNetwork version mismatch after execution: expected ${config.targetVersion}, got ${postUpgradeVersion}` + ); + assert( + typeof postUpgradeViewsVersion === "string" && postUpgradeViewsVersion.length > 0, + "SSVNetworkViews version is unreadable after execution" + ); + console.log(`[POST] network version = ${postUpgradeVersion}`); + console.log(`[POST] views version = ${postUpgradeViewsVersion}`); + + const actualNetworkImplementation = await readImplementationAddress(ethers.provider, ssvNetworkProxy); + const actualViewsImplementation = await readImplementationAddress(ethers.provider, ssvNetworkViews); + assert( + actualNetworkImplementation.toLowerCase() === deployResult.networkImplementation.toLowerCase(), + `SSVNetwork implementation mismatch: expected ${deployResult.networkImplementation}, got ${actualNetworkImplementation}` + ); + assert( + actualViewsImplementation.toLowerCase() === deployResult.viewsImplementation.toLowerCase(), + `SSVNetworkViews implementation mismatch: expected ${deployResult.viewsImplementation}, got ${actualViewsImplementation}` + ); + + const actualModules = {} as ModuleAddresses; + for (const moduleName of MODULE_ORDER) { + const actualModuleAddress = await readModuleAddress( + ethers.provider, + ssvNetworkProxy, + Number(SSVModules[moduleName as keyof typeof SSVModules]) + ); + const expectedModuleAddress = deployResult.modules[moduleName]; + assert( + actualModuleAddress.toLowerCase() === expectedModuleAddress.toLowerCase(), + `${moduleName} module mismatch: expected ${expectedModuleAddress}, got ${actualModuleAddress}` + ); + actualModules[moduleName] = actualModuleAddress; + } + console.log("[POST] module pointers match deploy-result.json"); + + const viewsModule: any = new Contract(actualModules.SSVViews, SSV_VIEWS_MODULE_IFACE, ethers.provider); + const actualCssvToken = getAddress(await viewsModule.CSSV_ADDRESS()); + assert( + actualCssvToken.toLowerCase() === deployResult.cssvToken.toLowerCase(), + `CSSV token mismatch: expected ${deployResult.cssvToken}, got ${actualCssvToken}` + ); + + const viewsProxyConfig: any = new Contract(ssvNetworkViews, NETWORK_VIEWS_PROXY_IFACE, ethers.provider); + const proxiedNetworkAddress = getAddress(await viewsProxyConfig.ssvNetwork()); + assert( + proxiedNetworkAddress.toLowerCase() === ssvNetworkProxy.toLowerCase(), + `SSVNetworkViews.ssvNetwork mismatch: expected ${ssvNetworkProxy}, got ${proxiedNetworkAddress}` + ); + + const params = resolveProtocolParams(config); + const cooldownDuration = resolveCooldownDuration(config); + const quorumBps = parseQuorum(config.quorumBps); + const oracles = normalizeOracles(config.oracles); + const defaultOracleIds = resolveDefaultOracleIds(config, oracles); + + console.log("[VERIFY] Running shared post-upgrade verifier"); + await verifyPostUpgradeState({ + views: viewsProxy, + params, + cooldownDuration, + defaultOracleIds, + quorumBps, + oracles, + }); + + const onChainValues = await readOnChainValues(viewsProxy); + const actualOracleEntries = await Promise.all( + onChainValues.defaultOracleIds.map(async (oracleId) => ({ + id: oracleId, + address: getAddress(await viewsProxy.getOracle(oracleId)), + })) + ); + + const cssvToken: any = new Contract(actualCssvToken, ERC20_IFACE, ethers.provider); + const totalStaked = BigInt(await viewsProxy.totalStaked()); + const cssvTotalSupply = BigInt(await cssvToken.totalSupply()); + const safeCssvBalance = BigInt(await cssvToken.balanceOf(safeAddress)); + const initialStakeAmount = parseUint(config.initialStakeAmount, "initialStakeAmount"); + + assert( + totalStaked === cssvTotalSupply, + `totalStaked/CSSV totalSupply mismatch: totalStaked=${totalStaked} cssvTotalSupply=${cssvTotalSupply}` + ); + if (initialStakeAmount !== undefined && initialStakeAmount > 0n) { + assert( + totalStaked === initialStakeAmount, + `totalStaked mismatch after initial stake: expected ${initialStakeAmount}, got ${totalStaked}` + ); + assert( + safeCssvBalance === initialStakeAmount, + `Safe cSSV balance mismatch after initial stake: expected ${initialStakeAmount}, got ${safeCssvBalance}` + ); + } + + const result: SimulationResult = { + ...config, + currentVersion: postUpgradeVersion, + owner: safeAddress, + viewsOwner: onChainViewsOwner, + ssvNetworkProxy, + ssvNetworkViews, + ssvToken: ssvTokenAddress, + cssvToken: actualCssvToken, + deployBlockNumber: executionReceipt.blockNumber, + cooldownDuration: onChainValues.unstakeCooldownDuration, + defaultOracleIds: onChainValues.defaultOracleIds, + quorumBps: onChainValues.quorumBps, + oracles: Object.fromEntries(actualOracleEntries.map(({ id, address }) => [String(id), address])), + modules: actualModules, + protocolParams: { + ...config.protocolParams, + networkFeeEth: onChainValues.networkFeeEth, + networkFeeSSV: onChainValues.networkFeeSSV, + maxOperatorEthFee: onChainValues.maxOperatorEthFee, + minOperatorEthFee: onChainValues.minOperatorEthFee, + operatorFeeIncreaseLimit: onChainValues.operatorFeeIncreaseLimit, + declareOperatorFeePeriod: onChainValues.declareOperatorFeePeriod, + executeOperatorFeePeriod: onChainValues.executeOperatorFeePeriod, + liquidationThresholdPeriod: onChainValues.liquidationThresholdPeriod, + liquidationThresholdPeriodSSV: onChainValues.liquidationThresholdPeriodSSV, + ...(params.minBlocksBetweenUpdates !== undefined + ? { minBlocksBetweenUpdates: bigintToJsonNumberOrString(params.minBlocksBetweenUpdates) } + : {}), + minimumLiquidationCollateralEth: onChainValues.minimumLiquidationCollateralEth, + minimumLiquidationCollateralSSV: onChainValues.minimumLiquidationCollateralSSV, + validatorsPerOperatorLimit: onChainValues.validatorsPerOperatorLimit, + unstakeCooldownDuration: onChainValues.unstakeCooldownDuration, + }, + deployments: { + ...(config.deployments ?? {}), + ssvNetworkStakingUpgradeImplementation: actualNetworkImplementation, + ssvNetworkViewsImplementation: actualViewsImplementation, + cssvToken: actualCssvToken, + modules: actualModules, + targetNetwork: targetNetwork, + deployBlockNumber: executionReceipt.blockNumber, + chainId: providerNetwork.chainId.toString(), + updatedAt: new Date().toISOString(), + }, + simulation: { + safeAddress, + safeTxHash, + safeNonce: bigintToJsonNumberOrString(safeTx.nonce), + postExecutionSafeNonce: bigintToJsonNumberOrString(postExecutionSafeNonce), + selectedApprovers, + executionBlock: executionReceipt.blockNumber, + receiptHash: executionReceipt.hash, + }, + }; + + await writeFile(outputPath, `${JSON.stringify(result, null, 2)}\n`, "utf8"); + console.log(`[OUTPUT] wrote simulation result to ${outputPath}`); + + if (skipForkTests) { + console.log("[TEST] Skipping forked tests (--skip-fork-tests)"); + return; + } + + await runForkedTests(outputPath, testPath); +} + +const isDirectExecution = + typeof process.argv[1] === "string" && + import.meta.url === pathToFileURL(resolve(process.argv[1])).href; + +if (isDirectExecution) { + main().catch((err) => { + console.error(err); + process.exit(1); + }); +} diff --git a/ssv-review/Internal-[DIP-X]-SSV-Staking.md b/ssv-review/Internal-[DIP-X]-SSV-Staking.md deleted file mode 100644 index 83c02b40c..000000000 --- a/ssv-review/Internal-[DIP-X]-SSV-Staking.md +++ /dev/null @@ -1,496 +0,0 @@ -# Proposing Effective balance oracles and SSV staking to support new ETH-denominated network fees - -*Everything discussed below is a work in progress, intended to spark discussion within the ssv.network DAO and beyond. Implementation details and binding steps will be submitted to the ssv.network DAO snapshot after community feedback is gathered.* - -# Introduction - -The ssv.network DAO ("DAO") proposes introducing SSV Staking as part of a *broader set of protocol upgrades* designed to support ETH-denominated payments and native effective balance accounting within the SSV Network. - -The transition to ETH payments simplifies the protocol's economic model by aligning fee settlement with the asset in which validator rewards are generated. Moving fee payments to ETH removes cross-asset dependencies, reduces operational complexity, and enables more direct and predictable protocol-level accounting. - -In parallel, supporting Ethereum's post-Pectra validator model requires effective balance-aware accounting. Effective Balance Accounting ensures that fees, runway calculations, and liquidation logic scale with the actual stake secured by validators, rather than relying on fixed assumptions. Implementing this model natively requires the protocol to reflect validator effective balances on-chain throughout their lifecycle. - -To bridge the gap between Ethereum's consensus layer and on-chain accounting, the protocol introduces Effective Balance Oracles, which track validator balances and update protocol state. Operating this oracle system in a decentralized and resilient manner requires participation and delegation by parties economically aligned with the protocol. - -SSV Staking provides such a delegation mechanism, allowing SSV holders to stake their tokens and delegate stake toward the selection of Effective Balance Oracles. In doing so, protocol fee flows are reflected through the staking mechanism in proportion to protocol usage, strengthening alignment between token holders and the network. - ---- - -# Components of SSV Staking - -SSV Staking is enabled through three tightly coupled components: - -* **ETH Payments** introduces native ETH-denominated fees at the protocol level, allowing network and operator fees to be paid and settled in ETH. - -* **Effective Balance Accounting** upgrades the protocol's accounting model to calculate fees, runway consumption, and liquidation conditions based on validators' actual effective balance, rather than assuming a fixed 32 ETH per validator. This enables stake-aware accounting that natively aligns the protocol with Ethereum's post-Pectra validator model. - -* **SSV Staking** introduces staking and delegation functionality for SSV holders. Through staking, participants lock SSV and support the protocol's operation by participating in the distributed selection of *Effective Balance Oracles*. In turn, they are rewarded in ETH for their effort based on the amount of SSV staked. - ---- - -# ETH Payments - -ETH Payments introduce a fundamental change to how economic accounting is handled within the SSV Network. With such payments, operator fees and network fees are paid in ETH, replacing the existing SSV-denominated payment model. - -## Motivation - -The SSV Network operates at the validator layer of Ethereum, where rewards are generated exclusively in ETH. However, the current fee model requires participants to manage and pay fees in SSV, creating a structural mismatch between where value is produced and how costs are paid. - -Transitioning to ETH payments addresses this mismatch and delivers several standalone benefits: - -* **Asset alignment** - Clusters pay fees in the same asset that their validators earn. This removes the need for conversions, hedging, or the complexities of using another token in order to operate validators. - -* **Economic predictability** - SSV-denominated fees fluctuate independently of validator rewards, forcing frequent adjustments to pricing and governance parameters. - -* **Operational simplicity** - Paying fees in ETH simplifies accounting, budgeting, and automation for cluster owners and operators. ETH balances directly represent the operational runway without requiring additional token management. - -* **Institutional accessibility** - ETH-denominated payments remove a major adoption barrier for institutional and regulated participants, who often prefer or require minimizing exposure to additional tokens and non-native protocol tokens. - -## ETH as the Native Payment Asset - -Transitioning to ETH payments defines a clear separation between how new clusters are created and how existing SSV-based clusters are handled going forward: - -### New Clusters - -All new clusters will operate with ETH payments from the outset: - -* Operator fees are paid in ETH - -* Network fees are paid in ETH - -* ETH must be deposited upfront to fund the cluster's operational runway - -### Existing Clusters (SSV-based) - -Existing SSV-based clusters are treated as **legacy**, and support for actively operating them under the SSV payment model is removed. While these clusters may continue running as long as they have sufficient runway, they can no longer be maintained through operational changes. - -This means that adding new validators, removing existing validators, reactivating liquidated clusters or depositing additional SSV to extend a cluster's runway is no longer supported. As a result, **the only path forward for maintaining an existing cluster is migration to ETH payments**, which restores full cluster functionality under the new payment and accounting model. - -For cluster owners who do not wish to migrate or are unable to do so, the remaining option is to voluntarily liquidate the cluster. Self-liquidation returns the remaining cluster balance to the owner and signals operators to stop operating the cluster's validators. However, if the intention is to continue operating the validators in the future, migration to ETH payments will be required in order to do so. - -For cluster owners who anticipate needing more time to migrate but intend to continue operating their validators, it is critical to deposit sufficient SSV in advance to ensure enough operational runway until migration can be completed. - -*To guarantee all users have the option to top up their clusters before the transition to ETH payments, the SSV Foundation is requested to publish a prominent message on DAO-managed channels and assets relevant to disseminating information regarding the future inability to fund clusters with SSV.* - -## Cluster Migration - -Cluster migration allows existing SSV-based clusters to transition into ETH payments. Migration applies at the cluster level, and each cluster can be migrated in a single interaction, which upgrades it to ETH payments immediately. - -To migrate, the cluster owner initiates the migration and deposits sufficient ETH to fund the cluster's future operation runway under the ETH payment model. As part of the migration, the cluster's accounting is switched from SSV to ETH, and any remaining SSV balance is returned to the cluster owner. - -Migration is a one-way process - once a cluster is migrated to ETH payments, it cannot revert back to SSV-based payments. - -## Operator Payments & Fee Transition - -Transitioning to ETH payments defines a clear separation between how new operators are onboarded and how existing operators transition from SSV-based fees to ETH-based fees. - -### New Operators - -New operators onboard directly with ETH-denominated fees. From launch onward, operators registering in the network will not be able to define or configure fees in SSV and will operate exclusively under the ETH payment model. - -### Existing Operators - -Existing operators continue earning SSV-denominated fees only for clusters that have not yet migrated. These SSV fees continue to accrue, but operators are no longer able to modify or adjust their SSV fee configuration. Accrued fees can still be withdrawn. - -Once clusters migrate to ETH payments, or when new ETH-denominated clusters are onboarded, operators begin earning fees in ETH based on their assigned *default ETH fee* configuration. - -#### Default ETH Fee - -At launch, **all existing operators are assigned a default ETH fee** to ensure that operator pricing does not become a blocker for cluster migration: - -* Operators with a **0 SSV** fee default to a **0 ETH** fee - -* Operators with a **non-zero SSV fee** default to a network-defined ETH fee - -We propose setting the default ETH fee for non-zero SSV operators to an amount equivalent to approximately 0.5% of Ethereum staking rewards per 32 ETH validator. Based on a 2.9% ETH staking APR, this corresponds to: - -* 0.00928 ETH per validator per year - -Under this default: - -* A standard 4-operator cluster pays ~2% of staking rewards to operators, with each operator earning ~0.5% - -* Clusters with more than four operators pay proportionally more (e.g., a 7-operator cluster pays ~3.5%) - -The proposed default ETH operator fee was evaluated by examining the current fee structure on the SSV Network. At present, the weighted average fee charged by public operators is approximately **0.761 SSV**, which corresponds to roughly **0.1%** of Ethereum staking rewards. - -Over time, SSV-denominated operator fees have converged toward very low levels, resulting in a fee structure that no longer reflects the underlying cost, responsibility, or risk associated with operating validators. - -Against this backdrop, the proposed default ETH operator fee - set at **0.5%** of Ethereum staking rewards per operator, is intentionally and materially higher than the current network average. This higher starting point establishes a new baseline under the ETH-based model, from which operators can subsequently reprice based on market dynamics and competition. Any such fee adjustments remain subject to the existing fee update constraints and limitations. - -## Governance Parameters - -The transition to ETH payments introduces a set of new governance-controlled parameters that define the economic and risk boundaries of the protocol. A detailed evaluation of these parameters, including assumptions and methodology, is provided in the [Liquidation Collateral Parameter Evaluation](#liquidation-collateral-parameter-evaluation) and [Network Fee Implications](#network-fee-implications) sections of this proposal The values for the parameters discussed in the aforementioned sections are mentioned in those sections and below only as examples. - -| Variable | Description | Update function | Initial Value | -| :---- | :---- | :---- | :---- | -| *ethNetworkFee* | Protocol network fee charged in ETH. | updateNetworkFee(uint256 fee) | 0.000000003550929823 ETH (0.00928 ETH - annual) | -| *minimumLiquidationCollateral* | Minimum ETH collateral an ETH-denominated cluster must maintain; falling below this level contributes to liquidation eligibility. | updateMinimumLiquidationCollateral(uint256 amount) | 0.00094 ETH | -| *minimumBlocksBeforeLiquidation* | Minimum number of blocks an ETH-denominated cluster must maintain sufficient balance before becoming eligible for liquidation. | updateLiquidationThresholdPeriod(uint64 blocks) | 50190 (7 days) | -| *operatorMaxFee* | Maximum operator fee cap, setting a technical upper bound on operator fees denominated in ETH. This parameter exists as a protocol safety constraint to prevent extreme fee configurations and is not intended to express economic policy or target fee levels. | updateMaximumOperatorFee(uint64 maxFee) | | -| *defaultOperatorETHFee* | Default ETH-denominated operator fee applied to existing operators during the transition from SSV-denominated fees to ETH-denominated fees. | Not governance-controlled. The default value is defined in the contract and applied automatically; it exists solely to facilitate operator migration and ensure continuity during the transition period. | 0.000000001775464912 ETH (0.00464 ETH - annual) | - ---- - -# Effective Balance Accounting - -Effective Balance Accounting updates how fees, cluster runway, and liquidations are calculated across the SSV Network by aligning them with validators' actual effective balance, rather than assuming a fixed 32 ETH per validator. - -This change is required to natively support Ethereum's post-Pectra validator model, where a single validator can secure and earn rewards on significantly more than 32 ETH. Historically, this gap was partially addressed through off-chain mechanisms, but Effective Balance Accounting brings this logic fully on-chain and applies it consistently across network fees, operator fees, and cluster payments. - -Specifically, this issue was partially mitigated through the Incentivized Mainnet (IM) program, which relied on an off-chain script to calculate validator balances and deduct unpaid network fees from monthly incentive rewards. This approach had several limitations: it did not apply to operator fees, it relied on periodic off-chain reconciliation, and it would not function once fees are denominated in ETH, as ETH fees cannot be deducted from SSV-based rewards. - -As a result, validators with higher effective balances have remained only partially accounted for. With the transition to ETH payments, natively supporting effective balance accounting is no longer optional \- it is required to ensure all fees are correctly calculated, collected, and enforced within the protocol. - -## Motivation - -Moving to effective balance accounting is a long-overdue evolution of the SSV Network's core accounting model, following Ethereum's Pectra upgrade and the introduction of validators with variable effective balances. As validator structures on Ethereum have matured, the protocol must move beyond fixed assumptions and provide native support that improves correctness, reliability, and long-term sustainability across operators, clusters, and the network itself. - -* **Native support for consolidated validators** - With effective balance accounting in place, the protocol natively adjusts its accounting to validators with varying effective balances. Fees, runway calculations, and safety checks all scale directly with effective balance, eliminating the need for off-chain tools to fill this gap. - -* **Fair operator compensation** - Effective balance accounting enables operators to be compensated according to the actual effective balance they manage, rather than being paid under a fixed 32 ETH assumption, ensuring correct compensation for operators managing consolidated validators. - -* **Preserving network revenue** - Without native effective balance support, the network would be unable to correctly collect network fees from ETH-based clusters operating consolidated validators. The Incentivized Mainnet program previously mitigated this through off-chain deductions, but this approach cannot be applied to ETH-denominated fees. Supporting effective balance accounting natively is therefore critical to prevent revenue loss as the network transitions to ETH payments. - -## Accounting Changes - -Effective Balance Accounting changes how fees are calculated at the cluster level by replacing validator count as a proxy with the cluster's effective balance. - -### Existing Clusters (SSV-based) - -In the SSV-based model, validators act as a proxy for effective balance. - -Each validator is implicitly assumed to represent a fixed 32 ETH of effective balance. Fees therefore scale linearly with the number of validators in the cluster, regardless of how much effective balance those validators actually secure. - -![image|690x88, 50%](upload://p2BelvkqZe0zO4ofvF91O7Zpzp7.png) - -Under this model: - -* Fees are defined per validator - -* Total fees scale with validator count - -* Consolidated validators are not fully accounted for - -This model continues to apply to all SSV-based clusters. As a result: - -* Network fee deduction for compensation via the Incentivized Mainnet script continues to operate - -* Operators managing SSV-based clusters are not compensated based on the amount of stake they manage - -### New clusters (ETH-based) - -In the ETH-based model, effective balance becomes the billing unit. - -Fees are defined per 32 ETH of effective balance and scale with a cluster's total effective balance, rather than with validator count: - -![image|690x111, 50%](upload://uWnpB7vC9bYmwDXRceiHDTJHj9i.png) - -Here, *total effective balance* refers to the **cumulative effective balance of all validators belonging to the cluster**. All accounting is performed using this aggregated cluster-level value. - -As a result, ETH-based clusters pay fees proportional to the actual effective balance they secure, independent of how that balance is distributed across validator keys. - -Effective balance-based accounting applies only to ETH-based clusters. SSV-based clusters continue operating under the validator-count model until they migrate, after which this becomes the only accounting model used by the protocol. - -## Effective Balance Oracles - -In order to achieve the DAO's stated goal of decentralizing Ethereum but doing so in the most ETH aligned way, this document suggests for the DAO to adopt Effective Balance Oracles that will perform Effective Balance Accounting. In this regard, the Effective Balance Oracles on ssv.network play a similar role to that of validators on the Ethereum blockchain, both requiring a staking mechanism and possibly a delegation to a third-party performing the needed duties, thus fulfilling a crucial part of the process. While oracles don't validate transactions as validators do, they do maintain the integrity and security of the protocol by accurately attesting what validator effective balance is, which is key for the safety of the ssv.network as discussed below. - -For Effective Balance accounting to work natively, the protocol must be able to track the effective balance of validators across the network and reflect this data on-chain. Validator effective balances, however, exist only on Ethereum's consensus layer and cannot be accessed directly by smart contracts efficiently in a way that serves the purpose of this protocol. - -To fill this gap, it is proposed that the protocol will rely on a dedicated set of **Effective Balance Oracles**. - -Effective Balance Oracles are responsible for tracking validator effective balances on the beacon chain and enabling the protocol to keep its on-chain accounting aligned with real validator state as balances evolve over time. - -### Oracle Set Composition and Evolution - -#### Initial Permissioned Oracle Set - -At launch, the protocol will operate with a permissioned set of four Effective Balance Oracles, operating under a 3-of-4 threshold for oracle commitments. - -This initial configuration is intentionally temporary and is designed to mitigate early-stage operational and correctness risks. Effective Balance Oracles play a critical role in protocol accounting and liquidation safety, and incorrect or inconsistent balance updates could have direct and dire consequences. - -Beginning with a permissioned set allows the protocol to validate, in production, the full oracle workflow under controlled conditions. This approach reduces the risk associated with unproven implementations, misconfigured clients, or adversarial behavior during the initial rollout of effective balance accounting. - -Once the oracle workflow and assumptions have been validated and observed to operate reliably over time, the protocol is intended to transition toward a permissionless oracle model, as described in subsequent sections. - -**The DAO is responsible for electing the initial oracle set and overseeing its composition over time, including making changes if required to maintain correctness, availability, and operational reliability during the early phase of effective balance accounting.** - -#### Oracle Compensation (Initial Phase) - -During the initial permissioned phase, oracle operators will be compensated to cover the operational costs of running the Effective Balance Oracle infrastructure. - -Each oracle will receive a fixed compensation of **$250 per month denominated in SSV, with a 30-day trailing average calculated on the first of the month, transferred on each consequent first msig batch** to cover infrastructure and operational costs associated with running the oracle client. In addition, oracle operators will be **fully reimbursed by the DAO for all Ethereum transaction costs** incurred as part of their oracle duties, including balance updates and Merkle root submissions. This compensation model is intended to ensure operational sustainability at launch while keeping the system simple and avoiding premature complexity around protocol-level incentives. - -#### Future Permissionless Oracle Set - -After the initial permissioned phase, the oracle set is intended to transition to a permissionless model. In this phase, any participant will be able to operate an Effective Balance Oracle, and the composition of the active oracle set will be determined automatically through SSV staking delegation rather than direct DAO selection. - -Under this model, SSV stakers delegate their staking weight to oracle operators, using stake as voting power. The oracle set is then composed of the operators with the highest delegated stake, allowing the set to evolve and rotate over time based on staker preferences and observed performance. - -Stake-based delegation is a critical component of this design. Effective Balance Oracles directly influence protocol accounting and liquidation behavior, making correctness and reliability essential. By tying oracle selection to delegated stake, the protocol ensures that oracle operators are economically aligned with the system: operators with higher delegated stake are incentivized to behave correctly, while stakers can reallocate delegation away from underperforming or untrusted oracles. - -This mechanism enables the protocol to maintain decentralization and security without relying on manual selection by a trusted entity, while allowing the oracle set to adapt dynamically as conditions change. In this phase, a protocol-level compensation mechanism will also be introduced to sustainably reward oracle operators for their ongoing duties. - -### Effective Balance Updates - -Effective balance updates are performed in two steps, moving from global observation to cluster-level updates. - -#### Step 1: Snapshot and consensus - -Effective Balance Oracles continuously track validator effective balances on the beacon chain. At defined intervals, they take a snapshot of all validator balances, aggregate them per cluster, and construct a Merkle tree representing the effective balances of all clusters at that snapshot. - -To reach consensus on this snapshot, each oracle independently commits the Merkle root representing this snapshot. Once a threshold of oracle commitments is reached, the snapshot is accepted by the protocol as the authoritative and accurate view of effective balances for that point in time. This threshold-based mechanism ensures both the correctness of the data and that no single oracle can dictate balance updates. - -#### Step 2: Cluster balance updates - -Once a snapshot is accepted, cluster-level effective balances can be updated on-chain by submitting a proof derived from the committed Merkle tree for a specific cluster. - -Updating cluster balances is **permissionless**: anyone can submit a valid proof and bear the transaction cost. As a failsafe, Effective Balance Oracles are expected to periodically perform these updates themselves to ensure cluster balances remain current even if third parties do not act. - -When a cluster's effective balance is updated, the protocol updates all related accounting based on the new value. This affects cluster runway calculations as well as future network and operator fee accruals tied to the amount of effective balance being managed. If an update causes a cluster to fall below liquidation thresholds, the cluster can be liquidated as part of the same process, ensuring that increases in effective balance are always matched by sufficient funding and collateral. - -#### Operational Considerations for Balance Updates - -Because updates are performed through periodic cluster-level sweeps, validators added to or removed from a cluster are initially accounted for using a default assumption of 32 ETH per validator. The actual effective balance of these validators - such as in the case of consolidated validators - will only be reflected once the next sweep occurs. As a result, cluster owners must account for the potential impact of delayed updates on runway and fee accrual, particularly when adding validators with higher effective balances. - -## Governance Parameters - -Effective Balance Accounting introduces new governance-controlled parameters that define how oracle consensus is reached for effective balance snapshots. - -| Variable | Description | Update function | Initial Value | -| :---- | :---- | :---- | :---- | -| quorumBps | Quorum threshold (in BPS) required for committing an effective balance snapshot | setQuorumBps(uint16 quorum) | 7500 (75.00%) considering a ¾ threshold. | -| | Replaces an existing Oracle with another one. | replaceOracle(uint32 oracleId, address newOracle) | | - ---- - -# SSV Staking - -SSV Staking introduces a staking and delegation mechanism that enables SSV holders to support the operation and maintenance of the protocol. Through staking, participants lock SSV and delegate stake toward the selection of Effective Balance Oracles, which are responsible for maintaining accurate effective balance accounting within the network. - -In return for participating in this process, protocol fees denominated in ETH and generated by network usage are reflected through the staking mechanism in proportion to participation. This introduces a tokenomic model in which SSV functions as an ETH accrual token, with value derived directly from protocol usage. - -## Motivation - -SSV Staking strengthens the role of SSV holders within the network by expanding their responsibilities beyond passive ownership. Through staking, token holders take part in selecting the oracles responsible for maintaining core protocol functions, giving them a direct role in the ongoing operation and reliability of the system. - -This model places protocol maintenance in the hands of participants with long-term economic exposure to the network, while allowing responsibility to be distributed and adjusted over time through delegation. - -This approach mirrors the participation model used in Ethereum staking, where ETH holders contribute to network maintenance through delegation to node operators or staking services. Similarly, SSV Staking allows token holders to participate in maintaining the protocol through delegation, without requiring direct operation of oracle infrastructure, while preserving accountability and decentralization. - -By tying economic participation to long-term staking, SSV Staking also strengthens governance. Participants who benefit from sustained protocol usage and growth are more incentivized to actively engage in governance and contribute to decisions that support the protocol's long-term reliability and evolution - -## Staking and cSSV - -SSV holders can stake their tokens into the SSV Staking contract and receive **cSSV**, an ERC-20 token that represents their staked position at a **1:1 ratio**. - -cSSV represents a claim on the underlying staked SSV, as well as a proportional share of protocol fees accrued to stakers. - -As part of staking, stakers must **delegate** their staking voting power. This delegation determines the composition of the Effective Balance Oracle set, which is responsible for maintaining effective balance data on-chain. - -In the temporary initial phase, staking delegation is automatically split evenly across the DAO-elected oracle set, providing a smooth starting point while establishing the foundation for stake-driven oracle selection in future phases. - -## Rewards and Claiming - -Protocol fees accrue continuously as validators operate on the SSV Network and generate ongoing network fees. Stakers earn a **pro-rata share of ETH-denominated fees**, based on their share of the total staked SSV. - -Rewards can be claimed at any time without unstaking, and claiming does not affect the staking position. - -When cSSV is transferred, rewards accrued up to that point remain claimable by the original holder, while the new holder begins accruing rewards only from the moment they receive the cSSV. - -## Unstaking - -Unstaking is a two-step process: - -First, the staker submits a withdrawal request, which locks the specified amount of cSSV and stops reward accrual for that portion. It is proposed that the protocol will launch with a **7-day lock period**. - -Once the lock period ends, the staker can finalize the unstake. The locked cSSV is burned, and the underlying SSV is returned at a 1:1 ratio relative to the original stake. - -## Governance Rights - -Staked SSV, represented by cSSV, **retains full governance and voting power**. Holding cSSV does not reduce a user's ability to participate in DAO governance compared to holding unstaked SSV. - -This ensures that participants who stake their SSV continue to influence the protocol's direction, while aligning governance participation with sustained economic exposure to the network. - -## Governance Parameters - -SSV Staking introduces new governance-controlled parameters that define the lifecycle and constraints of staking and unstaking within the protocol. - -| Variable | Description | Update function | Initial Value | -| :---- | :---- | :---- | :---- | -| cooldownDuration | Unstake cooldown duration (in blocks): the period users must wait between requesting an unstake and being able to withdraw their unlocked SSV. | setUnstakeCooldownDuration(uint64 blocks) | 50120 (7 days) | - -# Protocol Transition and Governance Implications - -The introduction of ETH-denominated payments and native effective balance accounting represents a structural upgrade to the SSV Network. Beyond the core protocol design, these changes require deliberate updates to incentives, parameters, and legacy governance decisions. - -## Incentivized Mainnet Transition - -With the introduction of ETH payments, network fees for ETH-denominated clusters are no longer compatible with the Incentivized Mainnet fee deduction mechanism (Incentivized Mainnet rewards are distributed in SSV, while network fees for these clusters are paid in ETH). As a result, network fees cannot be deducted from Incentivized Mainnet rewards for validators operating as part of ETH-denominated clusters. - -At the same time, ETH-denominated clusters operate under the new effective balance accounting model, where network fees are calculated and collected natively by the protocol. Because these fees are already enforced on-chain, applying additional off-chain deductions via the Incentivized Mainnet script becomes obsolete for ETH-denominated clusters. - -To reflect this distinction, the Incentivized Mainnet script will be updated to differentiate between legacy SSV-based clusters and ETH-denominated clusters: - -* **ETH-denominated clusters -** Network fee deductions are removed. - -* **SSV-based clusters -** Network fees continue to be deducted from Incentivized Mainnet rewards under the existing model. - -This update ensures that Incentivized Mainnet behavior remains aligned with the accounting and fee mechanisms applicable to each cluster type, while correctly supporting ETH-denominated clusters under the upgraded protocol model. - ---- - -## Liquidation Collateral Parameter Evaluation - -The liquidation collateral and liquidation threshold parameters currently in effect were derived using a DAO-approved calculation framework, most recently formalized in [DIP-44](https://snapshot.org/#/s:mainnet.ssvnetwork.eth/proposal/0x5ab8383681f4efec61c1e89388477e18de3f1b9a34ce1fef001e55043a8f3273). With the introduction of ETH payments, the protocol introduces dedicated liquidation parameters for ETH-denominated clusters. As part of defining these new parameters, it is appropriate to revisit the existing calculation framework to ensure that its underlying assumptions remain valid under current network conditions. - -### Revisiting the Calculation Framework - -The existing framework relies on a **1-year historical lookback window** for gas price data. This choice was appropriate at the time of adoption, when gas prices were higher and more volatile. - -However, recent Ethereum network conditions differ materially from those reflected in earlier datasets. In particular: - -* Average gas prices have declined significantly - -* Gas price volatility has stabilized - -* Sustained Layer 2 adoption has structurally reduced congestion on Ethereum mainnet - -As a result, a full 1-year lookback increasingly overweights historical periods that are no longer representative of current or expected near-term conditions. - -To illustrate this shift, the following charts compare historical gas price behavior under different lookback windows: - -![image|690x280](upload://8hRge5dE8zSuB6g0BBWEvKnMusw.png) - -*Ethereum gas prices over the last year (reference \- [ycharts.com](https://ycharts.com/indicators/ethereum_average_gas_price))* - -![image|690x267](upload://joYrIivA0jpY5kms7LbgyHIxov9.png) - -*Ethereum gas prices over the last 6 months (reference \- [ycharts.com](https://ycharts.com/indicators/ethereum_average_gas_price))* - -Under a 1-year lookback window: - -* **Average gas price:** \~3.51 GWEI - -* **Gas price standard deviation:** \~4.63 GWEI - -Under a 6-month lookback window: - -* **Average gas price:** \~1.86 GWEI - -* **Gas price standard deviation:** \~1.86 GWEI - -This represents a substantial reduction in both average gas costs and volatility. Continuing to rely on a 1-year window would therefore embed outdated assumptions into the liquidation model, resulting in parameters that are more conservative than current network conditions justify. - -For this reason, it is proposed to update the calculation framework to use a **rolling 6-month lookback window**. By grounding liquidation cost assumptions in more recent gas price data, the framework reflects both a lower average gas cost and reduced volatility. This, in turn, lowers the estimated worst-case cost of executing a liquidation and reduces the amount of collateral required to safely incentivize liquidators, improving capital efficiency without weakening safety guarantees. - -This change applies to the framework itself, and therefore affects all parameter evaluations derived from it going forward. - -### Impact on Existing SSV-Based Parameters - -Applying the updated 6-month lookback window to the existing framework results in revised parameter values for SSV-denominated clusters: - -| Parameter | Current Value | Proposed Value | Deviance | -| :---- | :---- | :---- | :---- | -| *minimumLiquidationCollateralSSV* | 1.53 SSV | 0.883 SSV | \-42.52% (\>15%) | -| *minimumBlocksBeforeLiquidationSSV* | 14 days | 100380 (14 days) | 0% (\<15%) | - -[*Calculations sheet*](https://docs.google.com/spreadsheets/d/1pa7VDZywlc5He2rS7qVbAKVv2KQrj7wZ-yvabpMRvUo/edit?usp=sharing) - -These updated values are a direct consequence of revised inputs rather than a change in liquidation logic. They are presented to maintain methodological consistency with prior DAO decisions. - -The DAO may choose to adopt these updated SSV-denominated values as part of this proposal or defer their application to a separate governance decision. - -### ETH-Denominated Liquidation Parameters - -In parallel to the existing SSV-denominated parameters, ETH-denominated clusters require a **dedicated set of liquidation parameters** derived from the same framework but adjusted to reflect their materially different risk profile. - -#### Reduced Risk from Removing SSV from the Calculation Framework - -Under the legacy SSV-based model, liquidation parameters were required to account for a cross-asset mismatch: liquidation execution costs are paid in ETH, while liquidation rewards and fee accrual are denominated in SSV. This required incorporating assumptions around SSV/ETH price ratios and their deviations, increasing uncertainty and necessitating more conservative parameter values. - -By removing SSV from the calculation framework, ETH-denominated clusters eliminate this cross-asset exposure entirely. Network fees, collateral, and liquidation execution are all denominated in ETH, resulting in a more predictable and tightly bounded liquidation model. - -#### Revised Liquidation Functions for ETH-Denominated Clusters - -With SSV-denominated components removed from the calculation framework, the existing liquidation functions can be simplified and recalibrated for ETH-denominated accounting. - -The calculation framework uses the following formulas for SSV-denominated clusters: - -* Minimum Liquidation Collateral - -![image|690x57, 50%](upload://3dvCyE3Kh3eHUJPOWEt6TMrHSSY.png) - -* Liquidation Threshold - -![image|690x66, 50%](upload://ae570VVYXDfsFMPbdp5oe3InDRN.png) - -New formulas for ETH-denominated clusters: - -* Minimum Liquidation Collateral - -![image|690x97, 50%](upload://eBvHtGoMdNmprbjB6n7ckxMoo9q.png) - -* Liquidation Threshold - -![image|690x88, 50%](upload://xy3dPLIc4Rxe43ouHptc4woj9jR.png) - -These ETH-denominated functions maintain the same safety objectives as the legacy framework, while allowing parameters to reflect the reduced risk profile enabled by ETH-denominated accounting. - -#### Proposed Initial Parameters for ETH-Denominated Clusters - -Applying the ETH-specific liquidation functions yields the following proposed **initial liquidation parameters** for ETH-denominated clusters: - -| Parameter | Current Value | Proposed Value | Deviance | -| :---- | :---- | :---- | :---- | -| *minimumLiquidationCollateral* | - | 0.00094 ETH | 100% (>15%) | -| *minimumBlocksBeforeLiquidation* | - | 50190 (7 days) | 100% (>15%) | - -[*Calculations sheet*](https://docs.google.com/spreadsheets/d/1pa7VDZywlc5He2rS7qVbAKVv2KQrj7wZ-yvabpMRvUo/edit?usp=sharing) - -These values are proposed as initial settings and remain fully governance-controlled. As with all liquidation-related parameters, the DAO retains the ability to adjust them as network conditions and assumptions evolve. - ---- - -## Network Fee Implications - -### Network Fee for ETH-Denominated Clusters - -As part of the transition to ETH-denominated clusters, the protocol introduces a **dedicated network fee denominated in ETH**, applied to ETH-denominated clusters. - -Under the legacy SSV-based model, the network fee calculation incorporated an ETH/SSV conversion factor, reflecting the fact that protocol fees were accrued in SSV while staking rewards and execution costs were denominated in ETH. With ETH-denominated clusters, this conversion is no longer required. - -For ETH-denominated clusters, the network fee is calculated natively in ETH as: - -![image|690x70, 50%](upload://ri9U6MpvfFhv8iWC0aOubQIUgiM.png) - -This formulation removes SSV entirely from the network fee calculation and aligns fee accrual directly with ETH-denominated validator rewards. - -##### Proposed Network Fee - -Applying the ETH-denominated network fee formulation yields the following **proposed initial network fee parameter** for ETH-denominated clusters: - -| Parameter | Current Value | Proposed Value | Deviance | -| :---- | :---- | :---- | :---- | -| *ethNetworkFee* | – | 0.000000003550929823 ETH (0.00928 ETH \- annual) | 100% (\>15%) | - -### Implications for the Legacy SSV Network Fee - -Once all clusters have migrated from SSV-based accounting to ETH-denominated clusters, the protocol will no longer rely on SSV-denominated network fees or ETH/SSV conversion logic. - -The existing governance mechanism for bounding the SSV network fee via a ratio-based maximum, as defined in [DIP-49](https://snapshot.org/#/s:mainnet.ssvnetwork.eth/proposal/0x5300de7fd0df8c07b06b1e4ad71bdf036945b26787b0157d70ab80fee3ad4126), was introduced to constrain the network fee under a model where fees were denominated in SSV and implicitly exposed to ETH price dynamics. - -Under an ETH-denominated fee model, this constraint becomes irrelevant. With network fees calculated and collected directly in ETH, there is no longer an SSV/ETH ratio to bound, and governance of the protocol network fee is expressed solely through the ETH-denominated network fee parameter. - ---- - -## Future Consideration: Public-Good DVT Clusters (SSV-Based) - -In future versions of the protocol, the SSV Network may explore supporting SSV-based clusters as a dedicated mode for public-good DVT use cases. - -Under this model, public-good DVT clusters would operate without paying protocol-level network fees. In exchange, these clusters would not participate in incentive programs such as the Incentivized Mainnet (IM). This preserves economic neutrality while allowing certain DVT deployments to operate purely as public infrastructure. - -This approach acknowledges that while SSV-based clusters are being deprecated for ongoing commercial operation, they may still serve a purpose as a constrained and clearly defined execution mode for non-commercial validator setups - such as research, experimentation, or ecosystem infrastructure - without distorting the protocol's economic model. - -This concept is not part of the current release and is presented as a potential future extension to support public-good DVT use cases in a principled and economically isolated manner. diff --git a/ssv-review/planning/CONSOLIDATED-AUDIT-FINDINGS.md b/ssv-review/planning/CONSOLIDATED-AUDIT-FINDINGS.md deleted file mode 100644 index b642e6d03..000000000 --- a/ssv-review/planning/CONSOLIDATED-AUDIT-FINDINGS.md +++ /dev/null @@ -1,785 +0,0 @@ -# Consolidated Audit Findings — SSV Network v2.0.0 - -**Generated:** 2026-03-17 -**Sources:** 9 independent audit scans (state invariant, behavioral state, input/arithmetic safety, semantic guard, SCV cheatsheet, staking rewards, oracle/flash loan, DoS/griefing, external call safety) -**Branch:** `ssv-staking` -**Cross-reference:** `ssv-review/planning/MAINNET-READINESS.md` - ---- - -## Priority Summary - -| ID | Description | Type | Severity | Resolution | -|----|-------------|------|----------|------------| -| CA-01 | Silent uint64 truncation in `networkTotalEarnings` DAO earnings | Arithmetic Safety | Medium-High | Already fixed (ref MAINNET-READINESS QUALITY-12) | -| CA-02 | Fees permanently lost when `totalStaked == 0` in `_syncFees` | Staking Rewards | Medium | Open (ref BUG-6 — mitigated by deployment sequencing) | -| CA-03 | Aggregate vs per-cluster rounding conservation law violation | Arithmetic Safety | Medium | Closed (ref BUG-19 — accepted known behavior) | -| CA-04 | Unsafe uint128 to uint64 cast in operator earnings accumulation | Arithmetic Safety | Medium | Already fixed (ref MAINNET-READINESS QUALITY-12) | -| CA-05 | uint64 overflow in `blockDiffEthFee` operator snapshot DoS | Arithmetic Safety | Medium | Open | -| CA-06 | Oracle quorum can be set to zero | Oracle Security | Medium | Already fixed (ref MAINNET-READINESS SEC-20) | -| CA-07 | Oracle weight assumes all delegation slots active | Oracle Security | Medium | Open | -| CA-08 | `migrateClusterToETH` missing `nonReentrant` modifier | Reentrancy | Medium | Already closed (ref MAINNET-READINESS SEC-6) | -| CA-09 | `accEthPerShare` precision loss at scale | Staking Rewards | Medium | Closed (ref BUG-18 — accepted as part of accumulator model) | -| CA-10 | Staking reward dilution via flash loan | Flash Loan | Medium | Mitigated by design (settlement ordering + 7-day cooldown) | -| CA-11 | `withdrawUnlocked` gas scales with pending request count | DoS / Griefing | Medium | Open (self-DoS only, capped at 2000) | -| CA-12 | External whitelisting contract can DoS validator registration | DoS / Griefing | Medium | Open (operator self-DoS only) | -| CA-13 | `onCSSVTransfer` hook can block all cSSV transfers | DoS / Griefing | Medium | Open (governance upgrade risk only) | -| CA-14 | `onCSSVTransfer` missing `nonReentrant` modifier | Reentrancy | Low | Already closed (ref MAINNET-READINESS SEC-7) | -| CA-15 | `commitRoot` accepts zero merkle root | Input Validation | Low | Already closed (ref MAINNET-READINESS SEC-14) | -| CA-16 | `removeOperator` doesn't clear `operatorEthVUnits` | State Cleanup | Low | Already fixed (ref MAINNET-READINESS QUALITY-10) | -| CA-17 | Dust trapped on reward claim with zero cSSV balance | Staking Rewards | Low | Already fixed (ref MAINNET-READINESS SEC-16b) | -| CA-18 | Governance fee params lack min/max bounds | Input Validation | Low | Open (ref MAINNET-READINESS SEC-17) | -| CA-19 | uint64 overflow in unstake unlock time calculation | Arithmetic Safety | Low | Open | -| CA-20 | Zero-value deposit/withdrawal accepted | Input Validation | Low | Already closed (ref MAINNET-READINESS SEC-16) | -| CA-21 | Oracle quorum weight manipulation via cSSV supply | Oracle Security | Low | Mitigated by design (equal-weight model) | -| CA-22 | No staleness check on committed root age | Oracle Security | Low | Open (informational) | -| CA-23 | Raw transfer/transferFrom instead of SafeERC20 | External Call Safety | Low | Open (no current risk, SSV is standard ERC20) | -| CA-24 | External whitelisting contract call without gas cap | External Call Safety | Low | Open (same root cause as CA-12) | -| CA-25 | Operator fee execution window block stuffing | DoS / Griefing | Low | Open (economically infeasible on L1) | -| CA-26 | Competing oracle proposals leave ghost state | State Cleanup | Low | Open | -| CA-27 | `ClusterBalanceUpdated` emitted for SSV clusters with unchanged state | Event Correctness | Low | Open | -| CA-28 | `claimEthRewards` dual balance check redundancy | Code Quality | Low | Open | -| CA-29 | Dead code in `_executeLiquidation` wrong accounting direction | Code Quality | Info | Open | -| CA-30 | `rescueERC20` no module-level access control | Access Control | Info | Open (proxy-level `onlyOwner` sufficient) | -| CA-31 | CLAUDE.md stale docs on `reactivate` nonReentrant | Documentation | Info | Open | -| CA-32 | No SafeCast used anywhere (systemic) | Arithmetic Safety | Info | Open | -| CA-33 | Rounding direction analysis | Arithmetic Safety | Info | No vulnerability | -| CA-34 | `_syncFees` defensive `current < previous` path | Code Quality | Info | Open | -| CA-35 | `onCSSVTransfer` virtual modifier override risk | Upgrade Safety | Info | Open | -| CA-36 | Flash loan attack surface — core cluster operations | Flash Loan | Info | No vulnerability | -| CA-37 | No circular price dependencies | Oracle Security | Info | No vulnerability | -| CA-38 | Oracle replacement mid-round voting edge case | Oracle Security | Info | Correctly handled | -| CA-39 | ETH transfer pattern (push payments) | External Call Safety | Info | Correctly implemented | -| CA-40 | `delegatecall` usage — trusted targets only | External Call Safety | Info | Correctly implemented | -| CA-41 | No approve race conditions | External Call Safety | Info | No vulnerability | -| CA-42 | Fee-on-transfer / rebasing token compatibility | External Call Safety | Info | Not applicable | -| CA-43 | Oracle `hasVoted` storage never cleaned | State Cleanup | Info | By design, acceptable growth | - ---- - -## Detailed Findings - ---- - -### MEDIUM-HIGH - ---- - -#### CA-01: Silent uint64 Truncation in `networkTotalEarnings()` — DAO Earnings Lost - -**Severity:** Medium-High -**Type:** Arithmetic Safety -**Location:** `contracts/libraries/ProtocolLib.sol:84-90` -**Resolution:** Already fixed (ref MAINNET-READINESS QUALITY-12) - -**Source:** STATE-INVARIANT-REPORT.md (SIV-01) -**Cross-references:** z_input_arithmetic_safety_scan.md (Finding 2), z_behavioral_state.md (F-3), z_staking_audit_report.md (Finding #2) - -**Description:** - -The `networkTotalEarnings()` function computes `earningsUnits` as `uint128` but then truncates to `uint64` via `PackedETH.wrap(uint64(earningsUnits))`. In Solidity 0.8, explicit narrowing casts silently truncate without reverting. If the product `blockDelta * networkFee_raw * totalVUnits / BPS_DENOMINATOR` exceeds `type(uint64).max` (~1.844e19), the result wraps silently. - -```solidity -uint128 earningsUnits = (idx * PackedETH.unwrap(sp.ethNetworkFee) * units) / BPS_DENOMINATOR; -return sp.ethDaoBalance.add(PackedETH.wrap(uint64(earningsUnits))); -// ^^^^^^^^^^^^^^^^^^^^^^^^ -// Silent truncation if earningsUnits > type(uint64).max -``` - -**Root Cause:** `updateNetworkFee()` does not enforce an upper bound on `ethNetworkFee`. The only constraint is `PackedETHLib.pack(fee)` not reverting, which allows fees up to `type(uint64).max * ETH_DEDUCTED_DIGITS`. Combined with even modest `daoTotalEthVUnits`, the product overflows `uint64`. - -**Impact:** -- DAO earnings silently truncated — `ethDaoBalance` understated -- `stakingEthPoolBalance` (synced from `ethDaoBalance`) also understated — staking rewards distributed are less than earned -- The "lost" ETH stays in the contract but can never be claimed by stakers -- Requires malicious/negligent governance to set extreme fee values — not exploitable by external actors - -**Practical reachability:** With current proposed parameters (`fee_packed ~ 35,509`, `daoTotalEthVUnits ~ 1e9`, `blockDelta ~ 2.5e6`), `earningsUnits ~ 8.87e15` — fits in `uint64`. Overflow requires either extreme governance-set fee values or decades without DAO earnings settlement. - -**Recommendation:** Apply `SafeCast.toUint64(earningsUnits)` to revert on overflow, or add an upper bound in `updateNetworkFee()`. - -**Fix (QUALITY-12):** A lightweight `_safeUint64(uint128)` helper was added to `SSVCoreTypes.sol` with a custom `SafeCastOverflow` error. The unsafe cast in `ProtocolLib.sol:89` was replaced with `_safeUint64(earningsUnits)`. - ---- - -### MEDIUM - ---- - -#### CA-02: Fees Permanently Lost When `totalStaked == 0` in `_syncFees` - -**Severity:** Medium -**Type:** Staking Rewards -**Location:** `contracts/modules/SSVStaking.sol:165-184` -**Resolution:** Open (ref MAINNET-READINESS BUG-6 — mitigated by deployment sequencing) - -**Source:** STATE-INVARIANT-REPORT.md (SIV-04) -**Cross-references:** z_staking_audit_report.md (Finding #3), z_behavioral_state.md (F-1) - -**Description:** - -When `totalStaked == 0` (no cSSV exists), `_syncFees` skips the `accEthPerShare` update but still advances `stakingEthPoolBalance` to `current`. Fees accrued during the zero-supply period are permanently lost — they've been debited from `ethDaoBalance` but never reach stakers. - -```solidity -uint256 totalStaked = ICSSVToken(CSSV_ADDRESS).totalSupply(); -if (totalStaked != 0) { - newFeesWei = PackedETHLib.unpack(packedNewFees); - s.accEthPerShare += uint128((newFeesWei * PRECISION) / totalStaked); -} -s.stakingEthPoolBalance = current; // Advanced regardless! -``` - -**Impact:** Network fees earned during periods with zero cSSV supply are permanently non-distributable. Relevant during protocol bootstrap or black swan events where all SSV is unstaked. - -**Recommendation:** Either (a) defer `stakingEthPoolBalance` advancement when `totalStaked == 0`, or (b) initialize `stakingEthPoolBalance = sp.networkTotalEarnings()` at staking module initialization so the first `_syncFees` only distributes fees from that point forward. Note: option (a) gives all accumulated fees to the first staker, which could incentivize front-running. - ---- - -#### CA-03: Aggregate vs Per-Cluster Rounding Conservation Law Violation - -**Severity:** Medium -**Type:** Arithmetic Safety -**Location:** `contracts/libraries/OperatorLib.sol:52-72`, `contracts/libraries/ProtocolLib.sol:84-90`, `contracts/libraries/ClusterLib.sol:306-321` -**Resolution:** Closed (ref MAINNET-READINESS BUG-19 — accepted known behavior) - -**Source:** STATE-INVARIANT-REPORT.md (SIV-02) - -**Description:** - -Each cluster pays fees proportional to its own `vUnits` (floor division), but operators earn proportional to their aggregate `effectiveVUnits` across ALL clusters (also floor division). Due to the mathematical property `floor(a*x/n) + floor(a*y/n) <= floor(a*(x+y)/n)`, operators and the DAO virtually earn slightly more than clusters collectively pay, creating a slow insolvency drift. - -**Bounded magnitude:** Per settlement: at most `(numClusters - 1) * ETH_DEDUCTED_DIGITS` wei = `(N-1) * 100,000 wei`. Per year (2.5M blocks) with 1,000 clusters: ~0.00025 ETH/year. - -**Recommendation:** Document as a known accepted issue. No code change required unless operating at extreme scale. - ---- - -#### CA-04: Unsafe uint128 to uint64 Cast in Operator Earnings Accumulation - -**Severity:** Medium -**Type:** Arithmetic Safety -**Location:** `contracts/libraries/OperatorLib.sol:68-69, 93-94, 306-307` -**Resolution:** Already fixed (ref MAINNET-READINESS QUALITY-12) - -**Source:** z_input_arithmetic_safety_scan.md (Finding 1) -**Cross-references:** z_scv-scan.md (SCV-05) - -**Description:** - -The operator earnings delta is computed as `uint128` but silently truncated to `uint64` when stored via `PackedETH.wrap(uint64(delta))`. If `delta` exceeds `type(uint64).max`, operator earnings are permanently lost with no revert. - -```solidity -uint128 delta = (uint128(blockDiffEthFee) * uint128(effectiveVUnits)) / BPS_DENOMINATOR; -operator.ethSnapshot.balance = operator.ethSnapshot.balance.add(PackedETH.wrap(uint64(delta))); -``` - -**Practical reachability:** With realistic parameters (50,400 blocks/week, packed fee ~35,500, 2,000 validators at max EB): `delta ~ 2.29e14` — fits in `uint64`. Overflow requires pathological conditions (decades without snapshot updates or extreme fee values). - -**Recommendation:** Use `SafeCast.toUint64(delta)` to fail loudly on overflow instead of silently truncating. - -**Fix (QUALITY-12):** The `_safeUint64(uint128)` helper added to `SSVCoreTypes.sol` replaced all 3 unsafe casts in `OperatorLib.sol` (lines 69, 94, 307). Overflow now reverts with `SafeCastOverflow`. - ---- - -#### CA-05: uint64 Overflow in `blockDiffEthFee` — Operator Snapshot DoS - -**Severity:** Medium -**Type:** Arithmetic Safety -**Location:** `contracts/libraries/OperatorLib.sol:58, 85` -**Resolution:** Open - -**Source:** z_behavioral_state.md (F-2) - -**Description:** - -```solidity -uint64 blockDiffEthFee = (currentBlock - operator.ethSnapshot.block) * PackedETH.unwrap(operator.ethFee); -``` - -The multiplication of `uint32 blockDiff * uint64 ethFee` produces `uint64`. Solidity 0.8 checked arithmetic reverts on overflow. Overflow occurs when `fee_packed > ~4.28e9`, corresponding to an actual fee > ~1,100 ETH/year per vUnit. While absurdly high for a real operator, `operatorMaxFee` has no upper-bound check against this threshold. - -**Impact:** If governance sets `operatorMaxFee` to an extreme value and an operator adopts it, any call to `updateSnapshotSt`/`updateSnapshot` reverts with arithmetic overflow. All cluster operations involving this operator are permanently blocked. Recovery via `reduceOperatorFee` also fails because it calls `updateSnapshot` internally. - -**Recommendation:** Upcast before multiplication: `uint128 blockDiffEthFee = uint128(currentBlock - operator.ethSnapshot.block) * uint128(PackedETH.unwrap(operator.ethFee))`. Also add an absolute cap in `updateMaximumOperatorFee`. - ---- - -#### CA-06: Oracle Quorum Can Be Set to Zero - -**Severity:** Medium -**Type:** Oracle Security -**Location:** `contracts/modules/SSVDAO.sol:252-258` -**Resolution:** Already fixed (ref MAINNET-READINESS SEC-20) - -**Source:** z_scv-scan.md (SCV-01) -**Cross-references:** z_input_arithmetic_safety_scan.md (Finding 3) - -**Description:** - -The `updateQuorumBps` function allowed `quorumBps = 0`. With zero quorum, `threshold = 0` in `commitRoot()`, so any single oracle vote immediately commits a root, bypassing multi-oracle consensus. A compromised oracle could commit a fraudulent Merkle root containing arbitrary effective balances. - -**Resolution:** Fixed via MAINNET-READINESS SEC-20 — `quorumBps` now validates `!= 0 && <= 10_000`. - ---- - -#### CA-07: Oracle Weight Assumes All Delegation Slots Are Active - -**Severity:** Medium -**Type:** Oracle Security -**Location:** `contracts/modules/SSVDAO.sol:199` -**Resolution:** Open - -**Source:** z_scv-scan.md (SCV-02) - -**Description:** - -The oracle weight calculation divides `totalStaked` by `s.defaultOracleIds.length`, which is always 4 (fixed-size array `uint32[MAX_DELEGATION_SLOTS]`). If fewer than 4 oracle slots are populated, active oracles cannot reach quorum. For example, with 2 oracles and 75% quorum: `2 * (totalStaked/4) = 50%` — never reaches 75%. - -**Impact:** The EB root commitment system becomes permanently stuck until all 4 slots are filled. - -**Recommendation:** Track the count of active oracle slots and use that for weight calculation, or count non-zero entries in `defaultOracleIds` dynamically. - ---- - -#### CA-08: `migrateClusterToETH` Missing `nonReentrant` Modifier - -**Severity:** Medium -**Type:** Reentrancy -**Location:** `contracts/modules/SSVClusters.sol:259` -**Resolution:** Already closed (ref MAINNET-READINESS SEC-6 — no callback risk) - -**Source:** z_semantic_guard_scan.md (SGA-01) -**Cross-references:** z_scv-scan.md (SCV-06) - -**Description:** - -`migrateClusterToETH` modifies cluster state, operator state, DAO accounting, and EB deviation accounting, then performs an external ERC20 token transfer via `CoreLib.transferTokenBalance` — all without `nonReentrant`. 10 of 11 functions with external transfers are protected. - -**Mitigating factors:** The SSV token is a standard ERC20 without transfer callbacks. CEI pattern is followed — all state updates complete before the transfer. The SSV cluster hash is deleted before the transfer, so re-migration would revert. - -**Resolution:** Closed — no callback risk with standard ERC20 SSV token. - ---- - -#### CA-09: `accEthPerShare` Precision Loss at Scale - -**Severity:** Medium -**Type:** Staking Rewards -**Location:** `contracts/modules/SSVStaking.sol:202` -**Resolution:** Closed (ref MAINNET-READINESS BUG-18 — accepted as part of accumulator model) - -**Source:** z_staking_audit_report.md (Finding #1) -**Cross-references:** z_scv-scan.md (SCV-04), z_input_arithmetic_safety_scan.md (Finding 9) - -**Description:** - -The `accEthPerShare` accumulator increment can round to zero when `newFeesWei * PRECISION < totalStaked`: - -```solidity -s.accEthPerShare += uint128((newFeesWei * PRECISION) / totalStaked); -``` - -If `totalStaked > 1e23` (100,000 SSV tokens) and `newFeesWei` is at the minimum packed increment (100,000 wei), the increment rounds to zero. Those fees are absorbed into `stakingEthPoolBalance` but never distributed — permanently orphaned. - -**Impact:** Low economic impact under current parameters, but fees are permanently lost from the staker pool. Over extended operation, orphaned fees accumulate silently. - -**Recommendation:** Defer `stakingEthPoolBalance` advancement when `accEthPerShare` increment rounds to zero, so small fees accumulate to the next sync. - ---- - -#### CA-10: Staking Reward Dilution via Flash Loan - -**Severity:** Medium -**Type:** Flash Loan -**Location:** `contracts/modules/SSVStaking.sol:183` -**Resolution:** Mitigated by design - -**Source:** z_oracle_flashloan_scan.md (Finding 2) - -**Description:** - -An attacker could attempt to dilute staking rewards by flash-borrowing SSV, calling `stake()` to inflate cSSV supply, then claiming rewards. - -**Why this is mitigated:** -1. Settlement ordering in `stake()`: `_syncFees()` runs at OLD `totalSupply`, then `_settle()` at OLD balance, THEN cSSV is minted. Attacker only earns rewards from fees accruing after their stake. -2. The 7-day unstaking cooldown prevents flash-loan-in-single-tx exploitation. -3. Residual risk is economic dilution inherent to any pro-rata staking system, not a contract bug. - ---- - -#### CA-11: `withdrawUnlocked` Gas Scales with Pending Request Count - -**Severity:** Medium -**Type:** DoS / Griefing -**Location:** `contracts/modules/SSVStaking.sol:230` -**Resolution:** Open (self-DoS only, capped at 2000) - -**Source:** z_dos_griefing_scan.md (Finding 1) - -**Description:** - -`calculateTotalUnfrozenBalance` iterates over the user's entire `withdrawalRequests[]` array. While capped at `MAX_PENDING_REQUESTS = 2000`, a user who accumulates many small unstake requests faces high gas costs. Worst case (all 2000 unlocked): ~11M gas — within block limit but expensive. - -**Impact:** Self-inflicted only — each `requestUnstake` requires burning cSSV. User's own funds locked behind expensive withdrawal. Cannot withdraw in smaller batches. - -**Recommendation:** Consider adding a paginated withdrawal function that limits how many requests are processed per call. - ---- - -#### CA-12: External Whitelisting Contract Can DoS Validator Registration - -**Severity:** Medium -**Type:** DoS / Griefing -**Location:** `contracts/libraries/OperatorLib.sol:167, 203-204` -**Resolution:** Open (operator self-DoS only) - -**Source:** z_dos_griefing_scan.md (Finding 2) -**Cross-references:** z_external_call_scan.md (Finding 2) - -**Description:** - -During validator registration, if an operator has a whitelisting contract set, the function calls `ISSVWhitelistingContract(whitelistedAddress).isWhitelisted(msg.sender, operatorId)`. If this external contract reverts unconditionally, consumes excessive gas, or enters an infinite loop, no one can register validators with that operator. - -**Mitigating factors:** Only the operator owner can set a whitelisting contract. The operator can remove it at any time. Existing validators are unaffected. - -**Recommendation:** Consider wrapping the external `isWhitelisted` call in a try/catch with a gas limit. - ---- - -#### CA-13: `onCSSVTransfer` Hook Can Block All cSSV Transfers - -**Severity:** Medium -**Type:** DoS / Griefing -**Location:** `contracts/modules/SSVStaking.sol:173` -**Resolution:** Open (governance upgrade risk only) - -**Source:** z_dos_griefing_scan.md (Finding 3) - -**Description:** - -The cSSV token calls `onCSSVTransfer(from, to, amount)` on every transfer. If the staking module is upgraded to a buggy version where `_syncFees` reverts, all cSSV transfers are blocked — creating a single point of failure that freezes the entire cSSV token economy. - -**Mitigating factors:** This is an admin/upgrade risk, not an external attacker vector. The proxy upgrade pattern means the DAO can deploy a fix. - -**Recommendation:** Consider adding a circuit breaker or try/catch wrapper in the cSSV token for the `onCSSVTransfer` hook. - ---- - -### LOW - ---- - -#### CA-14: `onCSSVTransfer` Missing `nonReentrant` Modifier - -**Severity:** Low -**Type:** Reentrancy -**Location:** `contracts/modules/SSVStaking.sol:173` -**Resolution:** Already closed (ref MAINNET-READINESS SEC-7 — trusted cSSV contract) - -**Source:** z_semantic_guard_scan.md (SGA-02) -**Cross-references:** z_external_call_scan.md (Finding 3) - -**Description:** - -`onCSSVTransfer` modifies staking accumulator state without `nonReentrant`, while 5 of 6 other accumulator-modifying staking functions are protected. Currently safe because the function is only callable by the immutable `CSSV_ADDRESS` and cSSV is a standard ERC20 with no callbacks. - ---- - -#### CA-15: `commitRoot` Accepts Zero Merkle Root - -**Severity:** Low -**Type:** Input Validation -**Location:** `contracts/modules/SSVDAO.sol:168` -**Resolution:** Already closed (ref MAINNET-READINESS SEC-14 — coordinated oracles) - -**Source:** z_semantic_guard_scan.md (SGA-03) -**Cross-references:** z_input_arithmetic_safety_scan.md (Finding 7), z_scv-scan.md (SCV-07b) - -**Description:** - -No check that `merkleRoot != bytes32(0)`. A zero root committed by quorum would be permanently unusable since `_verifyEBRoots` treats zero as non-existent. `latestCommittedBlock` would advance past this block, blocking EB updates until a new root is committed. - ---- - -#### CA-16: `removeOperator` Doesn't Clear `operatorEthVUnits` - -**Severity:** Low -**Type:** State Cleanup -**Location:** `contracts/modules/SSVOperators.sol:344-355` -**Resolution:** Already fixed (ref MAINNET-READINESS QUALITY-10) - -**Source:** STATE-INVARIANT-REPORT.md (SIV-03) - -**Description:** - -`_resetOperatorState()` resets `ethValidatorCount`, `ethSnapshot`, `ethFee`, etc., but does not clear `operatorEthVUnits[operatorId]` from `SSVStorageEB`. No functional impact since `updateSnapshotSt()` is skipped for removed operators, but off-chain analytics would see stale values. - ---- - -#### CA-17: Dust Trapped on Reward Claim with Zero cSSV Balance - -**Severity:** Low -**Type:** Staking Rewards -**Location:** `contracts/modules/SSVStaking.sol:109-139` -**Resolution:** Already fixed (ref MAINNET-READINESS SEC-16b, BUG-20) - -**Source:** STATE-INVARIANT-REPORT.md (SIV-05) -**Cross-references:** z_staking_audit_report.md (Finding #4b) - -**Description:** - -When a user has zero cSSV and a sub-precision remainder (`< ETH_DEDUCTED_DIGITS = 100,000 wei`), the remainder is deleted from `accrued` but not returned to the pool. Maximum dust per user: 99,999 wei (~0.0000001 ETH). - ---- - -#### CA-18: Governance Fee Parameters Lack Min/Max Bounds - -**Severity:** Low -**Type:** Input Validation -**Location:** `contracts/modules/SSVDAO.sol:85-96, 263-266` -**Resolution:** Open (tracked as MAINNET-READINESS SEC-17) - -**Source:** z_scv-scan.md (SCV-03) -**Cross-references:** z_input_arithmetic_safety_scan.md (Finding 4) - -**Description:** - -Three governance parameters can be set to zero with no validation: `declareOperatorFeePeriod`, `executeOperatorFeePeriod`, `cooldownDuration`. A misconfiguration or governance attack could eliminate time-based protections. Additionally, no upper bounds exist — `cooldownDuration` could be set to `type(uint64).max`, permanently locking all unstaked tokens. - -**Recommendation:** Enforce both minimum and maximum value constants for each parameter. - ---- - -#### CA-19: uint64 Overflow in Unstake Unlock Time Calculation - -**Severity:** Low -**Type:** Arithmetic Safety -**Location:** `contracts/modules/SSVStaking.sol:87-88` -**Resolution:** Open - -**Source:** z_input_arithmetic_safety_scan.md (Finding 5) - -**Description:** - -The unlock time is computed as `uint64(block.timestamp + s.cooldownDuration)`. If `cooldownDuration` is set to a value close to `type(uint64).max`, the addition result (in `uint256`) silently truncates when cast to `uint64`, wrapping to a small value — allowing immediate withdrawal. - -**Note:** Requires an admin to set `cooldownDuration` to an extreme value (see CA-18). - -**Recommendation:** Use `SafeCast.toUint64()` or validate before casting. - ---- - -#### CA-20: Zero-Value Deposit and Withdrawal Accepted - -**Severity:** Low -**Type:** Input Validation -**Location:** `contracts/modules/SSVClusters.sol:186, 206` -**Resolution:** Already closed (ref MAINNET-READINESS SEC-16) - -**Source:** z_input_arithmetic_safety_scan.md (Finding 6) - -**Description:** - -Both `deposit()` and `withdraw()` accept zero-value operations. A zero deposit updates the cluster hash and emits events with `value = 0`. A zero withdrawal triggers balance checks, operator index reads, and a 0-wei ETH transfer. No fund-loss impact but pollutes event logs. - ---- - -#### CA-21: Oracle Quorum Weight Manipulation via cSSV Supply - -**Severity:** Low -**Type:** Oracle Security -**Location:** `contracts/modules/SSVDAO.sol:191-197` -**Resolution:** Mitigated by design - -**Source:** z_oracle_flashloan_scan.md (Finding 1) - -**Description:** - -If an attacker front-runs the first oracle vote and inflates cSSV supply (via flash loan + `stake()`), the quorum threshold increases. However, the equal-weight model (`weight = totalStaked / oracleCount`) means inflating supply increases both threshold AND each oracle's weight proportionally — the ratio stays the same. With 4 oracles and 75% quorum, 3 votes always suffice regardless of supply. - ---- - -#### CA-22: No Staleness Check on Committed Root Age - -**Severity:** Low -**Type:** Oracle Security -**Location:** `contracts/modules/SSVClusters.sol:348, 434-442` -**Resolution:** Open (informational) - -**Source:** z_oracle_flashloan_scan.md (Finding 3) - -**Description:** - -There is no check on how old the latest committed root is relative to the current block. If oracles stop committing roots, `latestCommittedBlock` could be hundreds of blocks old. Clusters would operate with outdated effective balance values. Oracle liveness is a governance assumption, not a contract-level guarantee. - -**Recommendation:** Consider adding a `MAX_ROOT_AGE` parameter: `if (block.number - ctx.blockNum > MAX_ROOT_AGE) revert RootTooOld()`. - ---- - -#### CA-23: Raw `transfer`/`transferFrom` Instead of SafeERC20 - -**Severity:** Low -**Type:** External Call Safety -**Location:** `contracts/libraries/CoreLib.sol:46`, `contracts/modules/SSVStaking.sol:53, 103` -**Resolution:** Open (no current risk) - -**Source:** z_external_call_scan.md (Finding 1) - -**Description:** - -`SSVStaking` imports `SafeERC20` and declares `using SafeERC20 for IERC20`, but only uses it for `rescueERC20`. The SSV token's own `transfer`/`transferFrom` calls use the raw ERC20 interface. Currently safe because SSV is a standard OZ ERC20, but inconsistent with the imported library. - ---- - -#### CA-24: External Whitelisting Contract Call Without Gas Cap - -**Severity:** Low -**Type:** External Call Safety -**Location:** `contracts/libraries/OperatorLib.sol:203-204` -**Resolution:** Open (same root cause as CA-12) - -**Source:** z_external_call_scan.md (Finding 2) - -**Description:** - -The `isWhitelisted()` call to operator-chosen external contracts forwards all remaining gas. A malicious contract could consume excessive gas (gas bomb) or return large data. See CA-12 for full analysis. - ---- - -#### CA-25: Operator Fee Execution Window Block Stuffing - -**Severity:** Low -**Type:** DoS / Griefing -**Location:** `contracts/modules/SSVOperators.sol:146, 158-162` -**Resolution:** Open (economically infeasible on L1) - -**Source:** z_dos_griefing_scan.md (Finding 4) - -**Description:** - -`executeOperatorFee` must be called within the time window `[approvalBeginTime, approvalEndTime]`. A well-funded attacker could theoretically stuff blocks to prevent execution. With `executeOperatorFeePeriod` set to 24+ hours, block stuffing costs ~$10M+ on Ethereum mainnet. The operator can re-declare the fee if the window is missed. - ---- - -#### CA-26: Competing Oracle Proposals Leave Ghost State - -**Severity:** Low -**Type:** State Cleanup -**Location:** `contracts/modules/SSVDAO.sol:168-218` -**Resolution:** Open - -**Source:** z_behavioral_state.md (F-4) - -**Description:** - -When two oracles propose competing roots for the same `blockNum`, if root A reaches quorum first, the `rootCommitments[key_B]` and `roundFrozenSupply[key_B]` entries for root B are never cleaned up — they persist in storage indefinitely. No fund loss or security impact, only storage bloat proportional to oracle disagreement frequency. - ---- - -#### CA-27: `ClusterBalanceUpdated` Emitted for SSV Clusters With Unchanged State - -**Severity:** Low -**Type:** Event Correctness -**Location:** `contracts/modules/SSVClusters.sol:411-416` -**Resolution:** Open - -**Source:** z_behavioral_state.md (F-5) - -**Description:** - -In `_updateClusterBalanceInternal`, for `VERSION_SSV` clusters only the EB snapshot is updated — no fee accounting occurs. The `ClusterBalanceUpdated` event fires unconditionally with the unmodified `cluster` struct. The SSV oracle subscribes to this event and receiving it for an SSV cluster with unchanged balance could confuse off-chain indexers. - ---- - -#### CA-28: `claimEthRewards` Dual Balance Check Redundancy - -**Severity:** Low -**Type:** Code Quality -**Location:** `contracts/modules/SSVStaking.sol:137-141` -**Resolution:** Open - -**Source:** z_staking_audit_report.md (Finding #4) - -**Description:** - -`claimEthRewards` checks payout against both `stakingEthPoolBalance` AND `ethDaoBalance`. After `_syncFees`, these values should be equal. If they diverge (transient cross-module interaction), legitimate claims could be blocked — though divergence is self-correcting on next `_syncFees` call. - ---- - -### INFO - ---- - -#### CA-29: Dead Code in `_executeLiquidation` Wrong Accounting Direction - -**Severity:** Info -**Type:** Code Quality -**Location:** `contracts/modules/SSVClusters.sol:552, 573-591` -**Resolution:** Open - -**Source:** z_semantic_guard_scan.md (SGA-04) - -**Description:** - -The deviation accounting block handles `vUnitsCluster < baselineVUnits` by ADDING deviation to `daoTotalEthVUnits` and `operatorEthVUnits` — wrong direction. This case is unreachable because `_verifyEBLimits` enforces `effectiveBalance >= 32 ETH/validator`, so `vUnitsCluster >= baselineVUnits` always holds. If the code were ever reached due to future EB limit changes, accounting would be incorrect. - -**Recommendation:** Remove the dead `else` branch. - ---- - -#### CA-30: `rescueERC20` No Module-Level Access Control - -**Severity:** Info -**Type:** Access Control -**Location:** `contracts/modules/SSVStaking.sol:156` -**Resolution:** Open (proxy-level `onlyOwner` sufficient) - -**Source:** z_semantic_guard_scan.md (SGA-05) - -**Description:** - -`rescueERC20` relies exclusively on the proxy-level `onlyOwner` modifier. The delegatecall architecture means calling the module directly operates on the module's own empty storage, not the proxy's — direct module calls cannot drain proxy assets. - ---- - -#### CA-31: CLAUDE.md Stale Docs on `reactivate` nonReentrant - -**Severity:** Info -**Type:** Documentation -**Location:** CLAUDE.md, Security Rules section -**Resolution:** Open - -**Source:** z_semantic_guard_scan.md (SGA-06) - -**Description:** - -CLAUDE.md states `reactivate` is "Intentionally NOT protected" but in the code at `SSVClusters.sol:132`, `reactivate` IS protected with `nonReentrant`. Documentation is stale and could mislead auditors. - ---- - -#### CA-32: No SafeCast Library Used Anywhere - -**Severity:** Info -**Type:** Arithmetic Safety -**Location:** Multiple (~50+ casts across codebase) -**Resolution:** Open - -**Source:** z_input_arithmetic_safety_scan.md (Finding 8) - -**Description:** - -The codebase performs ~50+ explicit downcasts without using OpenZeppelin's SafeCast. Most casts are safe due to value constraints (e.g., `uint32(block.number)` won't overflow for ~1,600 years), but the absence of SafeCast means future changes widening value ranges could silently introduce truncation bugs. The most concerning casts (`uint128 -> uint64` in OperatorLib and ProtocolLib) are covered by CA-01 and CA-04. - ---- - -#### CA-33: Rounding Direction Analysis - -**Severity:** Info -**Type:** Arithmetic Safety -**Resolution:** No vulnerability - -**Source:** z_input_arithmetic_safety_scan.md (Finding 10) - -**Description:** - -All rounding directions were verified. Cluster fee deductions round down (user-favorable). Staking rewards and DAO earnings round down (protocol-favorable). `ebToVUnits` rounds up (protocol-favorable). This asymmetry is standard and the rounding dust is immaterial. - ---- - -#### CA-34: `_syncFees` Defensive `current < previous` Path - -**Severity:** Info -**Type:** Code Quality -**Location:** `contracts/modules/SSVStaking.sol:191-194` -**Resolution:** Open - -**Source:** z_staking_audit_report.md (Finding #5) - -**Description:** - -If `current < previous` (which shouldn't happen under normal invariants), the pool balance is silently reduced without reverting. This path is unreachable under correct protocol operation, but if triggered by a bug in another module, staker rewards would be silently lost. - -**Recommendation:** Add a revert when `current < previous` to fail loudly. - ---- - -#### CA-35: `onCSSVTransfer` Virtual Modifier Override Risk - -**Severity:** Info -**Type:** Upgrade Safety -**Location:** `contracts/modules/SSVStaking.sol:173` -**Resolution:** Open - -**Source:** z_staking_audit_report.md (Finding #6) - -**Description:** - -The `virtual` keyword allows overriding in derived contracts. If a future upgrade overrides `onCSSVTransfer` without proper reward settlement, it could break the accumulator pattern. The unused `amount` parameter may confuse future developers. - ---- - -#### CA-36 through CA-43: Informational Non-Findings - -The following were verified as safe or not applicable: - -| ID | Description | Source | Verdict | -|----|-------------|--------|---------| -| CA-36 | Flash loan attack surface on core cluster operations | z_oracle_flashloan_scan.md (F4) | No vulnerability — no market-price oracles | -| CA-37 | Circular price dependencies | z_oracle_flashloan_scan.md (F5) | None exist | -| CA-38 | Oracle replacement mid-round voting | z_oracle_flashloan_scan.md (F6) | Correctly handled | -| CA-39 | ETH transfer pattern (push payments) | z_external_call_scan.md (F4) | Correctly implemented with CEI + nonReentrant | -| CA-40 | `delegatecall` usage | z_external_call_scan.md (F5) | Trusted targets only, owner-controlled | -| CA-41 | No approve race conditions | z_external_call_scan.md (F6) | Clean | -| CA-42 | Fee-on-transfer / rebasing token compatibility | z_external_call_scan.md (F7) | Not applicable — only known tokens | -| CA-43 | Oracle `hasVoted` storage never cleaned | z_dos_griefing_scan.md (F5) | By design, acceptable growth (~1,460 slots/year) | - ---- - -## Cross-Reference Index - -This table maps each consolidated finding back to its source report(s) for traceability. - -| CA ID | STATE-INVARIANT | behavioral_state | input_arithmetic | scv-scan | semantic_guard | staking_audit | oracle_flashloan | dos_griefing | external_call | -|-------|----------------|-----------------|-----------------|----------|---------------|--------------|-----------------|-------------|--------------| -| CA-01 | SIV-01 | F-3 | Finding 2 | — | — | Finding #2 | — | — | — | -| CA-02 | SIV-04 | F-1 | — | — | — | Finding #3 | — | — | — | -| CA-03 | SIV-02 | — | — | — | — | — | — | — | — | -| CA-04 | — | — | Finding 1 | SCV-05 | — | — | — | — | — | -| CA-05 | — | F-2 | — | — | — | — | — | — | — | -| CA-06 | — | — | Finding 3 | SCV-01 | — | — | — | — | — | -| CA-07 | — | — | — | SCV-02 | — | — | — | — | — | -| CA-08 | — | — | — | SCV-06 | SGA-01 | — | — | — | — | -| CA-09 | — | — | Finding 9 | SCV-04 | — | Finding #1 | — | — | — | -| CA-10 | — | — | — | — | — | — | Finding 2 | — | — | -| CA-11 | — | — | — | — | — | — | — | Finding 1 | — | -| CA-12 | — | — | — | — | — | — | — | Finding 2 | Finding 2 | -| CA-13 | — | — | — | — | — | — | — | Finding 3 | — | -| CA-14 | — | — | — | — | SGA-02 | — | — | — | Finding 3 | -| CA-15 | — | — | Finding 7 | — | SGA-03 | — | — | — | — | -| CA-16 | SIV-03 | — | — | — | — | — | — | — | — | -| CA-17 | SIV-05 | — | — | — | — | Finding #4b | — | — | — | -| CA-18 | — | — | Finding 4 | SCV-03 | — | — | — | — | — | -| CA-19 | — | — | Finding 5 | — | — | — | — | — | — | -| CA-20 | — | — | Finding 6 | — | — | — | — | — | — | -| CA-21 | — | — | — | — | — | — | Finding 1 | — | — | -| CA-22 | — | — | — | — | — | — | Finding 3 | — | — | -| CA-23 | — | — | — | — | — | — | — | — | Finding 1 | -| CA-24 | — | — | — | — | — | — | — | — | Finding 2 | -| CA-25 | — | — | — | — | — | — | — | Finding 4 | — | -| CA-26 | — | F-4 | — | — | — | — | — | — | — | -| CA-27 | — | F-5 | — | — | — | — | — | — | — | -| CA-28 | — | — | — | — | — | Finding #4 | — | — | — | - ---- - -## Statistics - -| Severity | Total | Open | Already Fixed/Closed | Mitigated by Design | -|----------|-------|------|---------------------|-------------------| -| Medium-High | 1 | 0 | 1 | 0 | -| Medium | 12 | 5 | 5 | 2 | -| Low | 15 | 8 | 4 | 1 | -| Info | 15 | 6 | 0 | 0 | -| **Total** | **43** | **19** | **10** | **3** | - -**Unique actionable findings (Open, Medium or above):** 5 diff --git a/ssv-review/planning/INVARIANTS_TEST_PLAN.md b/ssv-review/planning/INVARIANTS_TEST_PLAN.md deleted file mode 100644 index 4e1da4c77..000000000 --- a/ssv-review/planning/INVARIANTS_TEST_PLAN.md +++ /dev/null @@ -1,286 +0,0 @@ -# Echidna Invariant Coverage Report - -**Generated:** 2026-03-19 -**Sources:** SPEC.md, FLOWS.md, MAINNET-READINESS.md, echidna test files, unit/integration tests - ---- - -## 1. Echidna Invariants Already Implemented (115 total) - -### SSVAccountingEchidna.sol (7 invariants) -| # | Invariant | Description | -|---|-----------|-------------| -| 1 | `echidna_eth_conservation` | ETH balance + outflows >= inflows | -| 2 | `echidna_ssv_conservation` | SSV token balance <= minted amount | -| 3 | `echidna_eth_solvency` | Contract ETH balance >= net inflows | -| 4 | `echidna_operator_vunits_matches_clusters` | Operator effective vUnits align with all their active clusters | -| 5 | `echidna_migration_one_way` | Migrated SSV clusters removed from clusters[], present in ethClusters[] | -| 6 | `echidna_ssv_accrual_no_overflow` | SSV operator earnings never decrease due to overflow | -| 7 | `echidna_vunits_deviation_consistent` | Total DAO vUnits match sum of cluster vUnits + migrated clusters | - -### SSVClustersEchidna.sol (18 invariants) -| # | Invariant | Description | -|---|-----------|-------------| -| 8 | `echidna_cluster_hash_consistent` | Stored cluster hash matches in-memory cluster data | -| 9 | `echidna_inactive_clusters_zeroed` | Inactive clusters have zero balance, index, networkFeeIndex | -| 10 | `echidna_cluster_balance_accounting` | Sum of tracked cluster balances matches expected total | -| 11 | `echidna_eth_balance_accounting` | Contract ETH >= all cluster balances + operator earnings + DAO + staking pool | -| 12 | `echidna_withdraw_limit_enforced` | Cannot withdraw more than cluster balance | -| 13 | `echidna_withdraw_conserves_balance` | Withdrew amount matches balance reduction (contract + owner) | -| 14 | `echidna_owner_withdraw_only` | Only cluster owner can withdraw | -| 15 | `echidna_liquidation_cleans_state` | Liquidation pays correct amount and resets cluster to empty | -| 16 | `echidna_reactivate_requires_inactive` | Cannot reactivate already-active cluster | -| 17 | `echidna_dust_liquidation_reachable` | Clusters with balance < burn rate are liquidatable | -| 18 | `echidna_eb_snapshot_block_lte_current` | EB snapshot lastUpdateBlock <= current block | -| 19 | `echidna_eb_snapshot_root_monotonic` | EB snapshot root block number never decreases | -| 20 | `echidna_eb_update_requires_root` | EB update reverts without committed Merkle root | -| 21 | `echidna_eb_update_frequency` | Cannot update EB twice within minBlocksBetweenUpdates window | -| 22 | `echidna_eb_update_staleness` | Cannot update EB with stale root | -| 23 | `echidna_fee_index_current_after_settle` | Fee indices are current after cluster settlement | -| 24 | `echidna_fee_uses_old_vunits_on_eb_change` | Fees computed with OLD vUnits on EB change | -| 25 | `echidna_liquidation_clears_eb_snapshot` | Liquidation zeros the EB snapshot | - -### SSVOperatorsEchidna.sol (20 invariants) -| # | Invariant | Description | -|---|-----------|-------------| -| 26 | `echidna_unique_active_pubkeys` | No duplicate public keys among active operators | -| 27 | `echidna_id_monotonic` | Operator IDs never decrease | -| 28 | `echidna_registered_owners_non_zero` | All active operators have non-zero owner | -| 29 | `echidna_eth_fee_within_max` | Operator ETH fee <= protocol maximum | -| 30 | `echidna_eth_fee_minimum` | Operators registered with ETH fee >= protocol minimum | -| 31 | `echidna_declare_fee_from_zero_reverts` | Cannot declare fee increase from zero fee | -| 32 | `echidna_declare_does_not_change_fee` | Declare does not immediately change current fee | -| 33 | `echidna_execute_requires_valid_window` | Execute fails outside approval window | -| 34 | `echidna_execute_rejects_invalid_fee` | Execute fails if fee > max operator fee | -| 35 | `echidna_reduce_fee_decreases` | Reduce actually decreases fee and clears pending declarations | -| 36 | `echidna_withdraw_limit_enforced` | Cannot withdraw more ETH than operator balance | -| 37 | `echidna_withdraw_all_clears_balance` | withdrawAll zeros the ETH balance | -| 38 | `echidna_withdraw_conserves_balance` | Withdraw amount matches balance reduction | -| 39 | `echidna_earnings_monotonic` | Operator earnings never decrease | -| 40 | `echidna_fee_change_latency` | Fee index updates with correct latency | -| 41 | `echidna_eth_withdraw_keeps_ssv` | ETH withdrawal doesn't affect SSV balance | -| 42 | `echidna_ssv_withdraw_keeps_eth` | SSV withdrawal doesn't affect ETH balance | -| 43 | `echidna_owner_only_actions` | Non-owners cannot remove/declare/execute/withdraw | -| 44 | `echidna_remove_cleans_state` | Removal zeros fee, balances, snapshot blocks, validator count | -| 45 | `echidna_remove_pays_out` | Removal pays out both ETH and SSV balances exactly | - -### SSVValidatorsEchidna.sol (8 invariants) -| # | Invariant | Description | -|---|-----------|-------------| -| 46 | `echidna_validator_hash_consistent` | Validator storage hash matches tracked state | -| 47 | `echidna_cluster_hash_consistent` | Cluster hash consistent with tracked state | -| 48 | `echidna_cluster_validator_counts` | Cluster validator count matches active validators | -| 49 | `echidna_operator_validator_counts` | Operator ethValidatorCount matches tracked registrations | -| 50 | `echidna_cluster_balance_accounting` | Sum of tracked cluster balances matches expected total | -| 51 | `echidna_no_duplicate_validators` | Cannot register same validator twice | -| 52 | `echidna_owner_only_remove` | Only validator owner can remove | -| 53 | `echidna_owner_only_exit` | Only validator owner can exit | - -### SSVStakingEchidna.sol (15 invariants) -| # | Invariant | Description | -|---|-----------|-------------| -| 54 | `echidna_sync_fees_handles_decrease` | syncFees handles pool balance decrease correctly | -| 55 | `echidna_sync_fees_never_fails` | syncFees succeeds and produces correct pool balance | -| 56 | `echidna_invalid_stake_reverts` | Stake with amount < minimum reverts | -| 57 | `echidna_invalid_unstake_reverts` | Unstake with amount > balance or excess pending reverts | -| 58 | `echidna_invalid_withdraw_reverts` | Withdraw with no unlocked requests reverts | -| 59 | `echidna_cssv_supply_matches_users` | cSSV supply = sum of user balances = expected supply | -| 60 | `echidna_cssv_supply_lte_ssv_backing` | cSSV supply <= SSV token balance in contract | -| 61 | `echidna_ssv_balance_matches_staked_plus_pending` | SSV balance = cSSV supply + pending unstake | -| 62 | `echidna_pool_matches_dao_balance` | Staking ETH pool balance = DAO ETH balance | -| 63 | `echidna_pending_requests_bounded` | Pending unstake requests <= MAX_PENDING_REQUESTS (2000) | -| 64 | `echidna_user_index_leq_acc` | User accEthPerShare index <= global accumulator | -| 65 | `echidna_accrued_within_pool` | Accrued rewards (rounded down) <= available pool | -| 66 | `echidna_cssv_transfer_settles_both` | cSSV transfer triggers reward settlement for both parties | -| 67 | `echidna_claim_payout_precision` | Claim payout divisible by ETH_DEDUCTED_DIGITS | -| 68 | `echidna_no_free_rewards_on_transfer` | Transfer doesn't mint/destroy rewards | - -### CSSVTokenEchidna.sol (9 invariants) -| # | Invariant | Description | -|---|-----------|-------------| -| 69 | `echidna_supply_equals_minted_minus_burned` | totalSupply = minted - burned | -| 70 | `echidna_burned_lte_minted` | Burned amount never exceeds minted | -| 71 | `echidna_individual_balance_lte_supply` | Each user balance <= totalSupply | -| 72 | `echidna_staking_is_self` | ssvStaking address = this contract | -| 73 | `echidna_name_immutable` | Name = "cSSV" | -| 74 | `echidna_symbol_immutable` | Symbol = "cSSV" | -| 75 | `echidna_decimals_is_18` | Decimals = 18 | -| 76 | `echidna_zero_address_has_no_balance` | Zero address balance = 0 | -| 77 | `echidna_supply_non_negative` | Supply >= 0 | - -### SSVDAOEchidna.sol (23 invariants) -| # | Invariant | Description | -|---|-----------|-------------| -| 78 | `echidna_network_fee_matches_expected` | ETH network fee index monotonically increases correctly | -| 79 | `echidna_network_fee_ssv_matches_expected` | SSV network fee index monotonically increases correctly | -| 80 | `echidna_liquidation_thresholds_valid` | Liquidation thresholds >= minimum (21,480 blocks) | -| 81 | `echidna_quorum_bps_valid` | Quorum <= 10,000 BPS | -| 82 | `echidna_dao_balance_matches_expected` | DAO token balance = stored balance * DEDUCTED_DIGITS | -| 83 | `echidna_withdraw_limits_enforced` | Cannot overdraw DAO SSV balance | -| 84 | `echidna_withdraw_conserves_balance` | DAO withdrawal conserves token balance | -| 85 | `echidna_commit_root_only_oracle` | Non-oracle addresses cannot commit roots | -| 86 | `echidna_commit_root_no_duplicate_votes` | Same oracle cannot vote twice for same (block, root) pair | -| 87 | `echidna_commit_root_not_future` | Cannot commit root for future block number | -| 88 | `echidna_commit_root_not_stale` | Cannot commit root for block <= latestCommittedBlock | -| 89 | `echidna_committed_block_monotonic` | latestCommittedBlock never decreases | -| 90 | `echidna_commit_root_dust_round_reaches_quorum` | Dusty supply rounds reach quorum at 3 votes | -| 91 | `echidna_commit_root_dust_round_not_before_threshold` | Cannot finalize dusty round before 3 votes | -| 92 | `echidna_commit_root_dust_round_uses_truncated_supply` | Dusty round freezes truncated supply | -| 93 | `echidna_commit_root_below_oracle_count_reverts` | Cannot commit with fewer oracles than oracle slots | -| 94 | `echidna_oracle_mapping_consistent` | Oracle bidirectional mapping is consistent | -| 95 | `echidna_finalized_weight_cleared` | Finalized root commitments are cleared | -| 96 | `echidna_commitment_weight_lte_supply` | Commitment weight never exceeds cSSV total supply | -| 97 | `echidna_finalization_implies_quorum` | Root finalized only if weight >= quorum threshold | -| 98 | `echidna_dao_earnings_monotonic` | DAO earnings never decrease | -| 99 | `echidna_dao_index_block_lte_current` | DAO index blocks <= current block | -| 100 | `echidna_dao_earnings_matches_formula` | DAO earnings = (blockDelta * fee * vUnits) / BPS_DENOMINATOR | - -### SSVMigrationEchidna.sol (3 invariants) -| # | Invariant | Description | -|---|-----------|-------------| -| 101 | `echidna_migration_removed_refund_exact` | Migration refunds exact SSV balance to cluster owner | -| 102 | `echidna_migration_removed_operator_not_eth_initialized` | Removed operators don't get ETH snapshot initialized during migration | -| 103 | `echidna_removed_operator_state_and_frozen_index_preserved` | Removed operators retain frozen snapshot.index | - -### SSVEBProofEchidna.sol (3 invariants) -| # | Invariant | Description | -|---|-----------|-------------| -| 104 | `echidna_eb_merkle_proof_verified` | EB updates with invalid Merkle proofs are rejected | -| 105 | `echidna_eb_bounds_enforced` | EB outside [32, 2048] ETH/validator rejected | -| 106 | `echidna_eb_snapshot_fields_exact` | EB snapshot fields set exactly | - -### CSSVTokenAccessControlEchidna.sol (3 invariants) -| # | Invariant | Description | -|---|-----------|-------------| -| 107 | `echidna_attacker_cannot_mint` | Non-authorized address cannot mint | -| 108 | `echidna_attacker_cannot_burn` | Non-authorized address cannot burn | -| 109 | `echidna_only_self_is_staking` | ssvStaking address is the contract itself | - -### SSVOperatorFeeGovEchidna.sol (1 invariant) -| # | Invariant | Description | -|---|-----------|-------------| -| 110 | `echidna_execute_rejects_legacy_declarations` | Cannot execute fee requests declared before UPGRADE_TIMESTAMP | - -### SSVEdgeCasesEchidna.sol (4+ invariants) -| # | Invariant | Description | -|---|-----------|-------------| -| 111 | `echidna_yoyo_liquidation_reachable` | Liquidate -> reactivate -> liquidate cycle succeeds | -| 112 | `echidna_reactivation_vunits_mismatch` | vUnits correctly handled in liquidation->reactivation flow | -| 113 | `echidna_validator_spam_no_failure` | Max validators per operator doesn't cause overflow | -| 114 | Additional edge cases | Fee index overflow, packing overflow, ETH accrual integrity | - -### SSVLegacyClustersEchidna.sol (1 invariant) -| # | Invariant | Description | -|---|-----------|-------------| -| 115 | `echidna_ssv_liquidation_resets_and_pays` | SSV liquidation pays exact cluster balance and resets state | - ---- - -## 2. Spec Invariants (SPEC.md Section 11 - Explicitly Labeled) - -| # | Invariant | Spec Reference | Echidna Coverage | -|---|-----------|----------------|-----------------| -| A1 | **ETH Conservation**: `contract.ETH >= Sum(ETH cluster balances) + Sum(operator ETH earnings) + DAO ETH + staking pool` | SPEC L991-1002 | COVERED: `echidna_eth_balance_accounting`, `echidna_eth_conservation`, `echidna_eth_solvency` | -| A2 | **SSV Conservation**: `contract.SSV >= Sum(SSV cluster balances) + Sum(operator SSV earnings) + DAO SSV + stakingHeldSSV` | SPEC L1004-1015 | COVERED: `echidna_ssv_conservation` | -| A3 | **Validator Count Consistency**: `ethDaoValidatorCount == Sum(cluster.validatorCount)` across all active ETH clusters | SPEC L1017-1023 | **GAP**: per-cluster/per-operator counts tested but NOT the global DAO-level sum | -| A4 | **vUnit Consistency**: `daoTotalEthVUnits == ethDaoValidatorCount * BPS + Sum(cluster_deviations)` | SPEC L1025-1031 | COVERED: `echidna_vunits_deviation_consistent` | -| A5 | **Cluster Hash Integrity**: every operation ends with `s.ethClusters[key] == cluster.hashClusterData()` | SPEC L1033-1040 | COVERED: `echidna_cluster_hash_consistent` | -| A6 | **cSSV Supply Accounting**: `cSSV.totalSupply() == Sum(staked SSV) - Sum(unstake-requested SSV)` | SPEC L1042-1049 | COVERED: `echidna_cssv_supply_matches_users`, `echidna_ssv_balance_matches_staked_plus_pending` | -| A7 | **Accumulator Monotonicity**: `accEthPerShare` never decreases | SPEC L1051-1057 | COVERED: `echidna_user_index_leq_acc` (implicitly) | -| A8 | **Oracle Block Monotonicity**: `latestCommittedBlock` never decreases | SPEC L1059-1065 | COVERED: `echidna_committed_block_monotonic` | -| A9 | **Cluster Version Exclusivity**: `(s.clusters[key] != 0) XOR (s.ethClusters[key] != 0)` | SPEC L1067-1073 | **GAP**: `echidna_migration_one_way` checks post-migration but NOT the global XOR across all keys | -| A10 | **Operator Dual Tracking**: `operator.validatorCount + operator.ethValidatorCount == total validators using operator` | SPEC L1075-1082 | **GAP**: per-version counts tested separately, cross-version sum never asserted | - ---- - -## 3. Gap Analysis: Spec'd but NOT Fuzz-Tested - -### HIGH Priority (Accounting & Core Safety) - -| ID | Invariant | Spec Source | Why It Matters | -|----|-----------|-------------|----------------| -| **A3** | `ethDaoValidatorCount == Sum(cluster.validatorCount)` global sum | SPEC L1017 | Wrong DAO validator count -> wrong network fee calculations for all clusters | -| **A9** | Cluster version exclusivity: `clusters[key] XOR ethClusters[key]` globally | SPEC L1067 | Violation means a cluster exists in both maps -> double-accounting, double-liquidation | -| **A10** | Operator dual tracking: `op.validatorCount + op.ethValidatorCount == total` | SPEC L1075 | Cross-version validator count mismatch -> earnings drift, wrong EB baselines | -| **B7** | Implicit EB default: when `clusterEB.vUnits == 0`, use `validatorCount * BPS_DENOMINATOR` | SPEC L322 | Wrong default vUnits -> wrong fee accrual for all clusters before first EB update | -| **B8** | SSV clusters never use EB for fee scaling | SPEC L325 | If SSV fees accidentally used EB, legacy cluster balances would drain at wrong rate | -| **B9** | Fee settlement uses old rate before storing new rate | SPEC L892 | Out-of-order settlement -> operators earn fees at new rate for blocks served at old rate | -| **C8** | Rewards STOP accruing at exact `requestUnstake` moment for burned portion | SPEC L447 | If rewards continue accruing on burned cSSV, reward pool drains faster than expected | -| **E3** | Net-zero validator shift on migration: SSV count down, ETH count up by same N | FLOWS L452 | Non-zero-sum shift -> DAO counts diverge from reality -> fee/liquidation miscalculations | - -### MEDIUM Priority (Lifecycle & Edge Cases) - -| ID | Invariant | Spec Source | Why It Matters | -|----|-----------|-------------|----------------| -| **B11** | Cluster balance never negative after arbitrary operation sequences | SPEC L933 | `max(0, balance - fees)` pattern could be bypassed in edge cases under fuzzing | -| **C9** | Dust forfeiture: `remainder > 0 && balanceOf == 0` -> dust forfeited; `balanceOf > 0` -> preserved | SPEC L412-422 | Wrong dust handling -> either locked ETH or reward inflation | -| **C10** | Zero-cSSV users cannot accrue future rewards | SPEC L420 | If accrual continues with zero balance, `pendingReward` computation is undefined | -| **C11** | `withdrawUnlocked` batch processes ALL matured requests, leaves immature intact | SPEC L115 | Partial processing -> stuck SSV tokens; wrong swap-and-pop -> data corruption | -| **D3** | Deposit into liquidated cluster succeeds | FLOWS L278 | If blocked, users cannot prepare for reactivation | -| **D4** | Withdraw from liquidated cluster (fee settlement skipped) succeeds | FLOWS L309 | If blocked or fees applied, users lose funds from dead clusters | -| **D6** | Reactivation with removed operators: removed operators silently skipped | FLOWS L387 | If revert, clusters with removed operators are permanently stuck | -| **G1** | Removed operator `owner` field preserved (non-zero) | FLOWS L640 | If zeroed, off-chain systems lose operator identity; re-registration detection breaks | -| **G2** | Removed operator earnings remain withdrawable post-removal | FLOWS L640 | If not, operators lose earned fees on removal | -| **G6** | `ensureETHDefaults` initialization: first ETH interaction sets `ethFee = DEFAULT_OPERATOR_ETH_FEE` | SPEC L269 | Wrong initialization -> operators charge wrong ETH fee to all clusters | - -### LOW Priority (Oracle & Token Bounds) - -| ID | Invariant | Spec Source | Why It Matters | -|----|-----------|-------------|----------------| -| **C12** | `cSSV.totalSupply() <= SSV.totalSupply()` | FLOWS L866 | Theoretical upper bound; violation implies unbacked cSSV | -| **F9** | Failed quorum proposals persist (no auto-cleanup) | SPEC L476 | Storage hygiene; not a security issue but verifies no unintended cleanup | -| **F10** | Re-voting same `blockNum` with different root succeeds | SPEC L74 | Oracle operational flexibility; positive-case coverage | -| **F11** | Frozen voting supply exact formula on first vote (truncated to multiple of oracle count) | SPEC L463 | Already partially covered by dust-round tests | - ---- - -## 4. Cross-Reference with Unit/Integration Tests - -Some gaps above ARE tested in the JS test suite but NOT under fuzzing: - -| Gap ID | JS Test Coverage | Fuzzing Value | -|--------|-----------------|---------------| -| A3 | `test/e2e/cross-cutting/validator-count-invariant.test.ts` | Fuzzing would catch edge cases in concurrent register/remove/liquidate/migrate sequences | -| B11 | `test/simulation/invariants.ts` (Monte Carlo) | Echidna explores more state-space than simulation | -| D3 | `test/unit/SSVClusters/deposit.test.ts` | Fuzzing would test deposit-into-liquidated with arbitrary cluster states | -| D4 | Implicitly in `test/unit/SSVClusters/withdraw.test.ts` | Fuzzing would test withdraw-from-liquidated with fee edge cases | -| G1 | `test/unit/SSVOperators/removeOperator.test.ts` | Fuzzing would test removal after complex operator lifecycle sequences | -| G2 | `test/unit/SSVOperators/withdrawOperatorEarnings.test.ts` | Fuzzing would test post-removal withdrawal under arbitrary fee/EB states | - ---- - -## 5. Recommended Implementation Order - -### Phase 1: Global Accounting Invariants (HIGH impact, moderate effort) -1. **A3** - Add `echidna_dao_validator_count_consistent` to SSVAccountingEchidna -2. **A9** - Add `echidna_cluster_version_exclusive` to SSVAccountingEchidna -3. **A10** - Add `echidna_operator_total_validators_consistent` to SSVAccountingEchidna -4. **E3** - Add `echidna_migration_net_zero_validators` to SSVMigrationEchidna - -### Phase 2: Fee Calculation Correctness (HIGH impact, higher effort) -5. **B7** - Add `echidna_implicit_eb_default_used` to SSVClustersEchidna -6. **B8** - Add `echidna_ssv_fees_ignore_eb` to SSVClustersEchidna or SSVLegacyClustersEchidna -7. **B9** - Add `echidna_fee_settle_before_change` to SSVOperatorsEchidna - -### Phase 3: Staking Reward Edge Cases (HIGH impact, moderate effort) -8. **C8** - Add `echidna_unstake_stops_accrual` to SSVStakingEchidna -9. **C9** - Add `echidna_dust_forfeiture_correct` to SSVStakingEchidna -10. **C10** - Add `echidna_zero_cssv_no_accrual` to SSVStakingEchidna - -### Phase 4: Cluster Lifecycle Edges (MEDIUM impact, lower effort) -11. **B11** - Add `echidna_cluster_balance_non_negative` to SSVClustersEchidna -12. **C11** - Add `echidna_withdraw_unlocked_batch_correct` to SSVStakingEchidna -13. **D3** - Add `echidna_deposit_liquidated_succeeds` to SSVClustersEchidna -14. **D4** - Add `echidna_withdraw_liquidated_skips_fees` to SSVClustersEchidna -15. **D6** - Add `echidna_reactivate_with_removed_operators` to SSVClustersEchidna - -### Phase 5: Operator Lifecycle (MEDIUM impact, lower effort) -16. **G1** - Add `echidna_removed_operator_owner_preserved` to SSVOperatorsEchidna -17. **G2** - Add `echidna_removed_operator_earnings_withdrawable` to SSVOperatorsEchidna -18. **G6** - Add `echidna_ensure_eth_defaults_correct` to SSVOperatorsEchidna - -### Phase 6: Token & Oracle Edges (LOW impact, low effort) -19. **C12** - Add `echidna_cssv_supply_lte_ssv_total_supply` to CSSVTokenEchidna -20. **F9** - Add `echidna_failed_quorum_persists` to SSVDAOEchidna -21. **F10** - Add `echidna_revote_different_root_succeeds` to SSVDAOEchidna -22. **F11** - Extend existing dust-round tests in SSVDAOEchidna diff --git a/ssv-review/planning/MAINNET-READINESS.md b/ssv-review/planning/MAINNET-READINESS.md deleted file mode 100644 index f3571fdc5..000000000 --- a/ssv-review/planning/MAINNET-READINESS.md +++ /dev/null @@ -1,4373 +0,0 @@ -# SSV Network v2.0.0 — Mainnet Readiness Checklist - -**Generated:** 2026-02-17 -**Updated:** 2026-03-16 -**Sources:** Verified bug report, verified test coverage gap analysis, verified scripts & ops audit, DIP-X vs implementation review reports (ETH Payments, Effective Balance, SSV Staking) -**Branch:** `ssv-staking` (base for all feature branches) - ---- - -## Priority Summary - -| ID | Task | Type | Priority | Effort | -|----|------|------|----------|--------| -| BUG-1 | ~~`ensureETHDefaults` overwritten by stale memory copy~~ | Critical Bug Fix | P0 | ✅ Fixed | -| BUG-2 | ~~`_resetOperatorState` doesn't clear `operator.owner`~~ | ~~Critical Bug Fix~~ Won't Fix | ~~P0~~ | ✅ By design | -| BUG-3 | ~~`ensureETHDefaults` resurrects removed operators~~ | Critical Bug Fix | P0 | ✅ Mitigated | -| BUG-4 | ~~Double deviation cleanup on liquidated cluster validator removal~~ | Critical Bug Fix | P0 | ✅ Fixed ([PR #429](https://github.com/ssvlabs/ssv-network/pull/429)) | -| BUG-5 | ~~`_liquidateAfterEBUpdateIfNeeded` condition too strict for ETH-only operators~~ | Critical Bug Fix | P1 | ✅ Fixed | -| BUG-6 | ~~Rewards lost when `totalStaked == 0` in staking `_syncFees`~~ | Critical Bug Fix | P1 | ✅ Mitigated (deployment) | -| BUG-7 | ~~`DEFAULT_OPERATOR_ETH_FEE` value deviates from DIP-X spec~~ | ~~Critical Bug Fix~~ | ~~P1~~ | ✅ Closed (negligible) | -| BUG-8 | ~~Cooldown duration uses `block.timestamp` but DIP specifies blocks~~ | ~~Critical Bug Fix~~ | ~~P1~~ | ✅ Closed (not a bug, added NatSpec) | -| BUG-9 | ~~`uint64(delta)` silent truncation in operator earnings accumulation~~ | ~~Critical Bug Fix~~ | ~~P1~~ | ✅ Closed (not realistic) | -| BUG-10 | ~~Remove liquidation check in `withdraw` function~~ | Critical Bug Fix | P2 | ✅ Fixed | -| BUG-12 | ~~`removeValidator` / `bulkRemoveValidator` blocked for legacy SSV clusters~~ | Critical Bug Fix | P1 | ✅ Done (Product approved) | -| BUG-13 | ~~Silent default ETH fee assignment for legacy operators during migration~~ | Observability Fix | P2 | ✅ Fixed (PR #502) | -| BUG-14 | ~~Removed operator SSV fees skipped during `migrateClusterToETH` fee settlement (double-payment)~~ | Critical Bug Fix | P1 | ✅ Fixed | -| BUG-14b | ~~`reduceOperatorFee` / `declareOperatorFee` overwrite explicit zero ETH fees for legacy SSV operators~~ | Critical Bug Fix | P1 | ✅ Fixed (ensureETHDefaults marker pattern) | -| BUG-15 | ~~`withdrawAllVersionOperatorEarnings` initializes ETH snapshot for legacy SSV-only operators~~ | Critical Bug Fix | P1 | ✅ Fixed | -| BUG-16 | ~~SSVNetworkViews enforce cluster version checks and unify isActive logic~~ | Critical Bug Fix | P1 | ✅ Fixed | -| BUG-17 | ~~`commitRoot` quorum can become unreachable due to truncation in per-oracle weight math~~ | Critical Bug Fix | P0 | ✅ Fixed | -| BUG-18 | ~~Staking Rewards Accumulator Precision Loss~~ | High Bug Fix | P1 | ✅ Closed (accepted as part of the accumulator model) | -| BUG-19 | ~~Aggregate vs per-cluster rounding causes conservation law violation~~ | Medium Bug Fix | P1 | ✅ Closed (accepted as a known precision limitation) | -| BUG-20 | Dust permanently trapped on reward claim with zero cSSV balance | Low Bug Fix | P1 | ✅ Closed (Fixed on SEC-16b) | -| SEC-1 | ~~`updateQuorumBps(0)` allows zero-threshold oracle commits~~ | Security Hardening | P2 | ✅ Mitigated (owner-only) | -| SEC-2 | ~~`quorumBps` not initialized during upgrade — zero by default~~ | Security Hardening | P0 | ✅ Fixed — `initializeSSVStaking` now takes `quorumBps` param and validates `!= 0 && <= 10_000` | -| SEC-3 | ~~`replaceOracle` doesn't invalidate pending votes~~ | Security Hardening | ~~P1~~ P2 | ✅ Mitigated (owner-only + coordinated oracles) | -| SEC-4 | ~~`updateUnstakeCooldownDuration` allows zero cooldown~~ | Security Hardening | ~~P1~~ P2 | ✅ Mitigated (owner-only, no accounting risk) | -| SEC-5 | ~~`totalStaked` changes between oracle votes (front-running)~~ | Security Hardening | ~~P1~~ P2 | ✅ Mitigated (impractical) | -| SEC-6 | ~~Add `nonReentrant` to `migrateClusterToETH`~~ | Security Hardening | P2 | ✅ Closed (no callback risk) | -| SEC-7 | ~~Add `nonReentrant` to `onCSSVTransfer`~~ | Security Hardening | P2 | ✅ Closed (trusted cSSV contract) | -| SEC-8 | ~~`reactivate` not emitting warning for removed operators~~ | Security Hardening | P2 | ✅ Closed (visible off-chain) | -| SEC-9 | ~~`operatorMaxFee` function signature differs from DIP-X spec~~ | Security Hardening | P2 | ✅ Closed (by design, PR #390) | -| SEC-10 | ~~cSSV token lacks governance/voting extensions (ERC20Votes)~~ | Security Hardening | P2 | ✅ Closed (Snapshot-based governance, same as SSV) | -| SEC-11 | ~~`hasDeviation` reactivation optimization uses global counter for per-operator decision~~ | Security Hardening | ~~P1~~ P3 | ✅ Closed (BUG-4 fix resolves root cause) | -| SEC-12 | ~~`deposit()` accepts deposits to liquidated ETH clusters without fee settlement~~ | Security Hardening | P2 | ✅ Closed (by design — document in FLOWS.md) | -| SEC-13 | ~~`OperatorWithdrawn` event doesn't distinguish ETH vs SSV withdrawals~~ | Security Hardening | P2 | ✅ Fixed — `OperatorWithdrawnSSV` added to `ISSVOperators.sol`; SSV path emits it, ETH path unchanged | -| SEC-14 | ~~`commitRoot` accepts `bytes32(0)` as merkleRoot — permanently wastes block slot~~ | Security Hardening | P2 | ✅ Closed (coordinated oracles) | -| SEC-15 | ~~Min/max operator fee can be set to contradictory values~~ | Security Hardening | P2 | ✅ Closed (owner-only setters) | -| SEC-16 | ~~Missing zero-value/zero-address guards on deposit and withdraw~~ | Security Hardening | P2 | ✅ Closed | -| SEC-16b | ~~Dust ETH stranded in `accrued` after full cSSV transfer + claim~~ | Security Hardening | P1 | ✅ Fixed | -| SEC-17 | DAO governance functions lack input guardrails (min/max/non-zero) | Security Hardening | P1 | M | -| SEC-18 | ETH-only operators can call `withdrawOperatorEarningsSSV` (no-op but wastes gas) | Security Hardening | P3 | S | -| SEC-19 | ~~`minBlocksBetweenUpdates` never initialized — EB update rate limit silently disabled~~ | Security Hardening | P1 | ✅ Fixed | -| SEC-20 | ~~Oracle Quorum Can Be Set to Zero~~ | Security Hardening | P2 | ✅ Fixed | -| TEST-1 | ~~Validator register/remove with non-zero operator fees~~ | Unit Test Completeness | P0 | ✅ Closed (Addressed in PR #443) | -| TEST-2 | ~~EB-weighted operator earnings accumulation~~ | Unit Test Completeness | P0 | ✅ Closed (Addressed in PR #444) | -| TEST-3 | ~~Balance delta assertions in liquidation paths~~ | Unit Test Completeness | P0 | ✅ Closed (PR #445) | -| TEST-4 | ~~`updateClusterBalance` on liquidated clusters~~ | Unit Test Completeness | P0 | ✅ Closed (PR #447 + enhanced with 3 edge cases) | -| TEST-5 | ~~Oracle quorum edge cases~~ | Unit Test Completeness | P0 | ✅ Closed (Addressed in PR #449) | -| TEST-6 | ~~EB decrease scenarios~~ | Unit Test Completeness | P0 | ✅ Closed (Addressed in PR #451) | -| TEST-7 | ~~Reentrancy in staking functions~~ | Unit Test Completeness | P0 | ✅ Closed (Addressed in PR #452) | -| TEST-8 | ~~Forbid creating clusters with removed operators~~ | Unit Test Completeness | P0 | ✅ Closed (Addressed in PR #453) | -| TEST-9 | ~~Migration balance accounting verification~~ | Unit Test Completeness | P1 | ✅ Done | -| TEST-10 | ~~Operator fee change + EB burn rate interaction~~ | Unit Test Completeness | P1 | ✅ Done | -| TEST-11 | ~~Network fee update impact on active clusters~~ | Unit Test Completeness | P1 | ✅ Done | -| TEST-12 | ~~Multi-staker reward fairness~~ | Unit Test Completeness | P1 | ✅ Done | -| TEST-13 | ~~Liquidation + reactivation multi-cycle accounting~~ | Unit Test Completeness | P1 | ✅ Done | -| TEST-14 | ~~Reactivation with EB deviation solvency check~~ | Unit Test Completeness | P1 | ✅ Done | -| TEST-15 | ~~SSV cluster operations completeness~~ | Unit Test Completeness | P1 | ✅ Closed (legacy SSV fee settlement covered; direct SSV withdraw is spec-blocked) | -| TEST-16 | ~~View function coverage (SSVViews)~~ | Unit Test Completeness | P1 | ✅ Fixed | -| TEST-17 | ~~Staking rewards from EB-weighted cluster fees~~ | Unit Test Completeness | P1 | ✅ Closed (Covered in `test/integration/SSVNetwork/staking.test.ts`) | -| TEST-18 | `withdrawNetworkETHEarnings` (DAO ETH withdrawal) | Unit Test Completeness | P1 | S | -| TEST-19 | ~~Operator removal impact on active ETH clusters~~ | Unit Test Completeness | P1 | ✅ Closed (covered by unit tests) | -| TEST-19a | Operator removal impact on active ETH clusters (edge cases) | Unit Test Completeness | P1 | S | -| TEST-20 | ~~Cooldown duration changes affecting pending requests~~ | Unit Test Completeness | P1 | ✅ Closed (covered by unit tests) | -| TEST-21 | ~~EB boundary values (min/max per validator)~~ | Unit Test Completeness | P2 | ✅ Closed | -| TEST-22 | ~~Dust/precision edge cases~~ | Unit Test Completeness | P2 | ✅ Closed | -| TEST-23 | ~~Max operator count (13) with EB~~ | Unit Test Completeness | P2 | ✅ Closed | -| TEST-24 | ~~Idempotency and double-operation checks~~ | Unit Test Completeness | P2 | ✅ Closed | -| TEST-25 | ~~Upgrade path (reinitializer) tests~~ | Unit Test Completeness | P2 | ✅ Closed | -| TEST-26 | ~~Zero-validator cluster operations~~ | Unit Test Completeness | P2 | ✅ Closed | -| TEST-27 | ~~Operator at max validator limit~~ | Unit Test Completeness | P2 | ✅ Closed | -| TEST-28 | ~~Uncomment SSV reentrancy test assertions~~ | Unit Test Completeness | P0 | ✅ Closed (Addressed in PR #454) | -| TEST-29 | ~~Add contract ETH balance delta assertions to deposit tests~~ | Unit Test Completeness | P1 | ✅ Done | -| TEST-30 | ~~Resolve TODO comments with deferred assertions~~ | Unit Test Completeness~~ | P1 | ✅ Done | -| TEST-31 | ~~Expand onCSSVTransfer test coverage~~ | Unit Test Completeness | P1 | ✅ Done | -| TEST-32 | ~~Add access control tests for DAO governance functions~~ | Unit Test Completeness | P1 | ✅ Closed (covered by unit tests) | -| TEST-33 | Mainnet governance config validation & edge-case tests | Unit Test Completeness | P1 | M | -| TEST-34 | ~~Staking solvency invariant: cSSV supply must not exceed SSV held by staking contract~~ | Unit Test Completeness | P1 | ✅ Done | -| ITEST-1 | ~~`commitRoot` → `updateClusterBalance` E2E flow~~ | Integration / E2E Tests | P1 | ✅ Closed | -| ITEST-2 | ~~Migration with multiple EB updates E2E~~ | Integration / E2E Tests | P1 | ✅ Closed | -| DEPLOY-1 | ~~Fix `deploy-all.ts` broken signature and constructor args~~ | Deployment & Scripts | P0 | ✅ Fixed — `deploy-all.ts` replaced by `deploy-fresh.ts` + `upgrade.ts` with correct `initializeSSVStaking(uint64,uint32[4],uint16)` signature | -| DEPLOY-2 | Verify `liquidationThresholdPeriod` config vs spec mismatch | Deployment & Scripts | P1 | S | -| DEPLOY-3 | ~~Verify `ethNetworkFee` rounding in config~~ | Deployment & Scripts | P2 | ✅ Closed (negligible) | -| DEPLOY-4 | ~~Remove unused error declarations in `ISSVNetworkCore.sol`~~ | Deployment & Scripts | P2 | ✅ Fixed | -| DEPLOY-5 | ~~Document `operatorMinFee` governance parameter in DIP-X~~ | Deployment & Scripts | P2 | ✅ Fixed | -| DEPLOY-6 | ~~DIP-X unstaking description doesn't match implementation~~ | Deployment & Scripts | P2 | ✅ Closed (already correct in SPEC.md and FLOWS.md) | -| DEPLOY-7 | ~~Deploy scripts import from test files~~ | Deployment & Scripts | P2 | ✅ Fixed — `upgrade.ts` and `deploy-fresh.ts` import from `scripts/common/config.ts`, no test file imports | -| DEPLOY-8 | ~~Dedicated verification script~~ | Deployment & Scripts | P2 | ✅ Done — New verify-upgrade recipe | -| QUALITY-1 | ~~`operatorFeeChangeRequests` not cleared on operator removal~~ | Code Quality | P2 | ✅ Closed (dead storage, off-chain sees OperatorRemoved) | -| QUALITY-2 | ~~Redundant `SSVStorage.load()` calls in view function loops~~ | Code Quality | P2 | ✅ Fixed | -| QUALITY-3 | ~~`withdraw` in SSVClusters duplicates operator loop inline~~ | Code Quality | P2 | ✅ Fixed | -| QUALITY-4 | ~~`_resetOperatorState` returns unused `Operator memory`~~ | Code Quality | P3 | ✅ Closed (cosmetic) | -| QUALITY-5 | ~~Remove duplicate `MaxValueExceeded` error declaration~~ | Code Quality | P3 | ✅ Fixed | -| QUALITY-6 | Multiple fixture patterns across tests (E2E/unit/integration) | Code Quality | P1 | ⚠️ High Priority — standardize after PR #435 | -| QUALITY-7 | Harness contracts vs. real contracts in tests | Code Quality | P2 | ⚠️ Medium Priority — migrate E2E to real contracts (PR #435) | -| QUALITY-8 | Helper function duplication across test types | Code Quality | P3 | ℹ️ Low Priority — merge helpers after PR #435 | -| QUALITY-9 | ~~`removeOperator` should clear fee change requests~~ | Code Quality | P2 | ✅ Closed (cleanup added + unit test) | -| QUALITY-10 | ~~`removeOperator` does not clear `operatorEthVUnits` — orphaned deviation~~ | Code Quality | P1 | ✅ Fixed | -| QUALITY-11 | ~~`commitRoot` skips `WeightedRootProposed` on quorum-reaching vote~~ | Code Quality | P2 | ✅ Fixed | -| QUALITY-12 | ~~Unsafe `uint128 → uint64` casts in operator/DAO earnings accumulation~~ | Code Quality | P2 | ✅ Fixed | -| QUALITY-13 | ~~Refactor tests, fixtures, helpers and migrate e2e tests to full fixtures~~ | Code Quality | P2 | ✅ Done | -| OPS-1 | Create mainnet deployment runbook | Operational Readiness | P1 | M | -| OPS-2 | Create emergency rollback procedure | Operational Readiness | P1 | M | -| OPS-3 | Update `.env.example` for v2.0.0 | Operational Readiness | P2 | 🧹 Cleanup PR candidate | -| OPS-4 | ~~Multisig batch tx method untested in sequential stage/prod/mainnet pipeline~~ | Operational Readiness | P1 | ✅ Done | -| FUZZ-1 | ~~Strengthen 5 partially-covered echidna invariants~~ | Echidna Invariant Suite | P1 | ✅ Done | -| FUZZ-2 | ~~Add 16 high-priority new echidna invariants (oracle/EB/fees/liquidation/staking)~~ | Echidna Invariant Suite | P1 | ✅ Done | -| FUZZ-3 | ~~Add 8 medium-priority echidna invariants (Merkle proof, operator fee gov, legacy SSV)~~ | Echidna Invariant Suite | P2 | ✅ Done | -| FUZZ-4 | ~~Add 6 lower-priority echidna invariants (vUnit aggregation, migration, overflow)~~ | Echidna Invariant Suite | P2 | ✅ Closed | -| FUZZ-5 | ~~ETH contract balance accounting invariant: `address(this).balance == Σ cluster.balance + Σ operator.ethEarnings + ethDaoBalance + stakingEthPoolBalance`~~ | Echidna Invariant Suite | P1 | ✅ Done | -| MAINNET-READINESS-1 | Mainnet playbook ready and send to m-sig | Mainnet Readiness | P0 | M | -| MAINNET-READINESS-2 | Full mainnet -> staking upgrade flow | Mainnet Readiness | P0 | M | -| MAINNET-READINESS-3 | Deep testing on staking | Mainnet Readiness | P0 | M | -| MAINNET-READINESS-4 | Audit complete | Mainnet Readiness | P2 | M | -| MAINNET-READINESS-5 | Cssv token outside of the ssv protocol | Mainnet Readiness | P1 | M | -| MAINNET-READINESS-6 | PR merging (Marco) | Mainnet Readiness | P1 | M | - - - - - - ---- - -## Critical Bug Fix - -### [BUG-1] `ensureETHDefaults` overwritten by stale memory copy -- **Type:** Critical Bug Fix -- **Priority:** P0 -- **Status:** Fixed (verified on `ssv-staking`) -- **Owner:** (resolved) -- **Timeline:** (complete) -- **Github Link:** (empty) - -**Requirement:** -Fix `updateClusterOperatorsOnRegistration` so that the memory copy of an operator is taken AFTER `ensureETHDefaults` writes to storage, not before. The stale memory copy currently overwrites the ETH defaults that were just set. - -**Context:** -In `OperatorLib.sol:185`, the operator is loaded into memory. At line 201, `ensureETHDefaults` correctly writes to storage. But at line 239, `s.operators[operatorId] = operator` overwrites storage with the stale memory copy where `ethFee == 0` and `ethSnapshot.block == 0`. For pre-v2 operators that never had ETH fields initialized, this means they silently get zero ETH fees and cluster liquidation thresholds use an incorrect burn rate. This is the highest-severity bug in the codebase. - -**Resolution:** -Code refactored on `ssv-staking` — the function now uses a storage reference (`operatorSt`), calls `ensureOperatorExist` and `ensureETHDefaults` on it, and only then copies to memory. See `OperatorLib.sol:197-201`. - -**Acceptance Criteria:** -- [x] Operator loaded into memory AFTER `ensureETHDefaults` is called, or `ensureETHDefaults` is called on the memory copy and then written back -- [x] Pre-v2 operators get correct `ethFee` (default ETH fee) after first validator registration -- [x] Pre-v2 operators get correct `ethSnapshot.block` (current block) after first registration -- [x] `cumulativeFee` accumulates correctly (not zero) for clusters with pre-v2 operators -- [ ] Existing unit tests still pass -- [ ] New unit test covers registering a validator with a pre-v2 operator and verifying `ethFee != 0` - -**Agent Instructions:** -1. Read `contracts/libraries/OperatorLib.sol` fully, focusing on `updateClusterOperatorsOnRegistration` (line 162). -2. The fix: Move the memory copy (`Operator memory operator = s.operators[operatorId]` at line 185) to AFTER the `ensureETHDefaults(s.operators[operatorId])` call at line 201. Alternatively, call `ensureETHDefaults` on the storage reference first, then load into memory. -3. Ensure the loop structure still works — `ensureETHDefaults` must be called on the storage reference, and then the memory copy should reflect the updated storage. -4. Do NOT change the `ensureETHDefaults` function itself. -5. Do NOT change `updateClusterOperators` or `updateClusterOperatorsOnReactivation` — they are separate code paths. -6. Add a unit test in `test/unit/SSVValidator/` that registers a validator using operators whose `ethFee` and `ethSnapshot.block` are both zero (simulating pre-v2 state), then verifies: - - `operator.ethFee` is set to the default ETH fee after registration - - `operator.ethSnapshot.block` is the current block - - The cluster's cumulative fee correctly includes the operator's ETH fee -7. Run `npm run test:unit` to verify all tests pass. - -#### Sub-items: -- [ ] Sub-task 1: Reorder memory load to after `ensureETHDefaults` in `updateClusterOperatorsOnRegistration` -- [ ] Sub-task 2: Write unit test for pre-v2 operator ETH fee initialization during validator registration -- [ ] Sub-task 3: Run full unit test suite and verify no regressions - ---- - -### [BUG-2] `_resetOperatorState` doesn't clear `operator.owner` -- **Type:** ~~Critical Bug Fix~~ Informational — Won't Fix -- **Priority:** ~~P0~~ N/A -- **Status:** Closed (by design) -- **Owner:** (resolved) -- **Timeline:** (complete) -- **Github Link:** (empty) - -**Original Requirement:** -When an operator is removed via `removeOperator`, the `_resetOperatorState` function must also clear `operator.owner` to ensure removed operators are consistently detectable across all code paths. - -**Resolution — Intentional Design:** -Preserving `operator.owner` after removal is intentional behavior, consistent since v1 (`main` branch). Reasons: - -1. **Off-chain queryability:** `getOperatorById` (SSVViews.sol:89) returns the preserved owner so explorers/UIs can display who owned a removed operator. Clearing it would lose this information on-chain. -2. **All on-chain guards are already safe:** - - `checkOwner` (OperatorLib.sol:131): catches removed operators via `snapshot.block == 0 && ethSnapshot.block == 0` — never reaches the owner check - - `ensureOperatorExist` (OperatorLib.sol:159): catches via `(ethSnapshot.block == 0 && snapshot.block == 0)` — second condition fires even though `owner != address(0)` - - `getSSVBurnRate` (SSVViews.sol:356): removed operators pass `owner != address(0)` but contribute zero fee (fee is already zeroed) — no impact -3. **No exploit path:** there is no code path where a non-zero owner on a removed operator leads to incorrect state mutation or access control bypass. - -Updated documentation in `docs/FLOWS.md` section 4.2 to reflect this design with a full detection-method table. - -#### Sub-items: -- [ ] Sub-task 1: Add `operator.owner = address(0)` to `_resetOperatorState` -- [ ] Sub-task 2: Audit all `operator.owner` references for compatibility -- [ ] Sub-task 3: Add unit test verifying owner is cleared after removal -- [ ] Sub-task 4: Run full test suite - ---- - -### [BUG-3] `ensureETHDefaults` resurrects removed operators -- **Type:** ~~Critical Bug Fix~~ Mitigated -- **Priority:** ~~P0~~ N/A -- **Status:** Closed (mitigated by upstream guards on `ssv-staking`) -- **Owner:** (resolved) -- **Timeline:** (complete) -- **Github Link:** (empty) - -**Original Requirement:** -`ensureETHDefaults` must not set `ethSnapshot.block` on removed operators. Add a guard to skip operators that have been removed. - -**Resolution — All call sites are already guarded:** -While `ensureETHDefaults` itself has no removed-operator guard, no code path can reach it with a removed operator: - -1. **`updateClusterOperatorsOnRegistration` (line 200):** `ensureOperatorExist` (line 198) reverts first for removed operators (both snapshot blocks are 0). -2. **`declareOperatorFee` (SSVOperators.sol:107):** `checkOwner` (line 100) reverts first for removed operators (both snapshot blocks are 0). -3. **`updateClusterOperatorsMigration` (line 395):** Explicit `continue` at line 380 skips removed operators (`snapshot.block == 0 && ethSnapshot.block == 0`). Only operators with at least one non-zero snapshot block reach `ensureETHDefaults`. - -**Acceptance Criteria:** -- [x] `ensureETHDefaults` does not modify removed operators (unreachable via all call sites) -- [x] Removed operators keep `ethSnapshot.block == 0` after any call path -- [x] New validators cannot be registered to clusters containing removed operators (enforced by `ensureOperatorExist`, PR #410) -- [x] Existing migration and registration tests still pass - ---- - -### [BUG-4] ~~Double deviation cleanup on liquidated cluster validator removal~~ -- **Type:** Critical Bug Fix -- **Priority:** P0 -- **Status:** ✅ Fixed -- **Owner:** N/A -- **Timeline:** Merged 2026-02-17 -- **Github Link:** [PR #429](https://github.com/ssvlabs/ssv-network/pull/429) (merged) - -**Requirement:** -Fix `_bulkRemoveValidator` so that when removing the last validators from a liquidated cluster with explicit EB tracking, deviation is not double-subtracted from `operatorEthVUnits` and `daoTotalEthVUnits`. - -**Context:** -In `SSVValidators.sol:164-247`, when a cluster is liquidated (`!cluster.active`), the `if (cluster.active)` guard at line 194 skips the operator update. However, the EB deviation cleanup block at lines 211-240 still runs. If the cluster had explicit EB tracking and was liquidated, the deviation was already cleaned up during `_executeLiquidation` (`SSVClusters.sol:554-614`). When `_bulkRemoveValidator` subtracts deviation again at lines 230 and 233, this double-subtracts from `operatorEthVUnits` and `daoTotalEthVUnits`, potentially causing underflow and reverting — which blocks validator removal entirely. - -**Acceptance Criteria:** -- [ ] Removing validators from a liquidated cluster with explicit EB tracking does NOT double-subtract deviation -- [ ] `operatorEthVUnits` and `daoTotalEthVUnits` are correct after removing validators from a liquidated cluster -- [ ] Removing validators from a liquidated cluster without explicit EB tracking still works -- [ ] Removing validators from an active cluster is unchanged -- [ ] New test: liquidate a cluster with explicit EB → remove validators → verify no revert and correct deviation values - -**Agent Instructions:** -1. Read `contracts/modules/SSVValidators.sol`, focus on `_bulkRemoveValidator` (line 164), particularly the EB deviation cleanup block at lines 211-240. -2. Read `contracts/modules/SSVClusters.sol`, focus on `_executeLiquidation` (line 554) to understand what deviation cleanup liquidation already performs. -3. The fix: Add a guard in the deviation cleanup block (around line 218-237) that skips the `operatorEthVUnits` and `daoTotalEthVUnits` subtraction when `!cluster.active`. The `ebSnapshot.vUnits` zeroing can remain (it's per-cluster and not double-counted). -4. Alternatively, wrap the deviation cleanup in `if (cluster.active || ...)` to only clean up deviation for active clusters. -5. Follow the existing pattern in the codebase where `cluster.active` guards are used. -6. Add a test in `test/unit/SSVValidator/` that: creates a cluster with EB tracking → liquidates it → removes validators → verifies `operatorEthVUnits` and `daoTotalEthVUnits` are correct (not underflowed). -7. Run `npm run test:unit`. - -#### Sub-items: -- [x] Sub-task 1: Add `cluster.active` guard around deviation cleanup in `_bulkRemoveValidator` -- [x] Sub-task 2: Write test for validator removal from liquidated cluster with explicit EB (`test/unit/SSVValidator/bug4-double-deviation-liquidated.test.ts`) -- [ ] Sub-task 3: Run full test suite - ---- - -### [BUG-5] `_liquidateAfterEBUpdateIfNeeded` condition too strict for ETH-only operators -- **Type:** Critical Bug Fix -- **Priority:** P1 -- **Status:** Fixed -- **Owner:** (resolved) -- **Timeline:** (complete) -- **Github Link:** (empty) - -**Requirement:** -Fix the condition at `SSVClusters.sol:543` so that `ethValidatorCount` is decremented for ETH-only operators (those with `ethSnapshot.block != 0` but `snapshot.block == 0`). - -**Context:** -In `_liquidateAfterEBUpdateIfNeeded` at `SSVClusters.sol:521-552`, line 543 checks `op.ethSnapshot.block != 0 && op.snapshot.block != 0` before decrementing `ethValidatorCount`. Operators registered after the v2.0.0 migration may have `snapshot.block == 0` (never had SSV activity), so the decrement is skipped — leaving `ethValidatorCount` inflated. - -**Acceptance Criteria:** -- [ ] `ethValidatorCount` is decremented for operators with `ethSnapshot.block != 0` regardless of `snapshot.block` -- [ ] Operators with `ethSnapshot.block == 0` (removed) are still skipped -- [ ] No change to the `_executeLiquidation` call - -**Agent Instructions:** -1. Read `contracts/modules/SSVClusters.sol`, focus on `_liquidateAfterEBUpdateIfNeeded` (line 521). -2. Change the condition at line 543 from `op.ethSnapshot.block != 0 && op.snapshot.block != 0` to just `op.ethSnapshot.block != 0`. -3. Verify this doesn't break the removed-operator skip (removed operators have `ethSnapshot.block == 0` after `_resetOperatorState`). -4. Run `npm run test:unit`. - -#### Sub-items: -- [ ] Sub-task 1: Fix condition in `_liquidateAfterEBUpdateIfNeeded` -- [ ] Sub-task 2: Add test for EB auto-liquidation with ETH-only operators -- [ ] Sub-task 3: Run full test suite - ---- - -### [BUG-6] Rewards lost when `totalStaked == 0` in staking `_syncFees` -- **Type:** Critical Bug Fix -- **Priority:** P1 -- **Status:** ✅ Mitigated (deployment) -- **Owner:** (deployment team) -- **Timeline:** At upgrade -- **Github Link:** Mitigated via [PR #431](https://github.com/ssvlabs/ssv-network/pull/431) (upgrade batch includes initial DAO stake) -- **DIP-X Review Source:** SSV Staking review findings DIP-18, DIP-19 - -**Requirement:** -When `totalStaked == 0` in `_syncFees`, ETH rewards must not be silently lost. Either accumulate them for the next sync when stakers exist, or redirect them to the DAO. - -**Context:** -`SSVStaking.sol:179-203`: When `totalStaked == 0`, line 196 skips the `accEthPerShare` increment but line 201 still advances `stakingEthPoolBalance`. The fees earned during the zero-staked period are permanently locked in the contract — they can never be distributed to future stakers. - -**Additional context from DIP-X review (DIP-19):** The `_syncFees` function also has a related edge case when `current <= previous` (DAO earnings decrease). At `SSVStaking.sol:187-190`, if `current.lte(previous)`, the function silently updates `stakingEthPoolBalance` to the lower value and returns without distributing. This can happen after reward claims reduce `sp.ethDaoBalance`. While `claimEthRewards` reduces both `stakingEthPoolBalance` and `sp.ethDaoBalance` by the same packed amount (so `current == previous` after normal claims), this edge case acts as a safety valve. The fix for BUG-6 should also consider this interaction to ensure no fees are lost in either direction. - -**Mitigation:** -This is mitigated by deployment procedure rather than a code fix. The DAO multisig (Safe) upgrade batch transaction includes an SSV `approve` + `stake(1 SSV)` call immediately after `upgradeToAndCall`. This ensures `totalStaked > 0` before any network fees can accrue, making the zero-staked window impossible in practice. The 1 SSV stake goes to the DAO address, so the tokens are not lost. The full upgrade batch is: -1. `upgradeToAndCall` (proxy upgrade + `initializeSSVStaking` with quorumBps=7500) -2. `updateModule` × 7 (all module addresses) -3. SSV token `approve` (SSVNetwork contract as spender) -4. `stake(1_000_000_000)` (1 SSV minimum stake from DAO) -5. Governance parameter updates (`updateNetworkFee`, `updateLiquidationThresholdPeriod`, etc.) - -All executed atomically in a single Safe multisig batch transaction. - -**Acceptance Criteria:** -- [x] Deployment runbook includes DAO stake as part of upgrade batch -- [x] `initializeSSVStaking` now validates `quorumBps` (PR #431) -- [ ] Verify Safe batch transaction encoding before mainnet execution -- [ ] Post-upgrade: confirm `totalStaked > 0` on-chain - -#### Sub-items: -- [x] Sub-task 1: Document deployment mitigation in MAINNET-READINESS.md -- [x] Sub-task 2: Add quorumBps to initializer (PR #431) -- [ ] Sub-task 3: Encode and test Safe batch transaction before mainnet - ---- - -### [BUG-7] ~~`DEFAULT_OPERATOR_ETH_FEE` value deviates from DIP-X spec~~ -- **Type:** ~~Critical Bug Fix~~ -- **Priority:** ~~P1~~ Closed -- **Status:** ✅ Closed (negligible) -- **Owner:** N/A -- **Timeline:** N/A -- **Github Link:** N/A - -**Resolution:** Difference is ~0.31% (~0.0000143 ETH/year per validator). Negligible. Mainnet config uses the DIP-X intended value adjusted for packability. -- **DIP-X Review Source:** ETH Payments review findings ETH-7, ETH-14 - -**Requirement:** -The `DEFAULT_OPERATOR_ETH_FEE` constant is set to `1,770,000,000` wei (1.77 gwei) but the DIP-X specifies `0.000000001775464912 ETH` (1,775,464,912 wei = 1.775464912 gwei). The DIP value is not packable (not divisible by `ETH_DEDUCTED_DIGITS = 100,000`), so a rounded value must be used. The implementation chose `1,770,000,000` which is further from the spec than necessary. The closest packable value rounding up is `1,775,500,000`. - -**Context:** -`SSVCoreTypes.sol:14`: `DEFAULT_OPERATOR_ETH_FEE = 1770_000_000`. The DIP value `1,775,464,912 % 100,000 = 64,912` (not divisible), so it would revert with `MaxPrecisionExceeded`. The closest valid values are `1,775,400,000` (rounding down) or `1,775,500,000` (rounding up). The current value under-delivers by ~0.31% on the stated fee. Per-block difference: 5,464,912 wei. Annual impact per validator: ~0.0000143 ETH less than DIP target. - -**Acceptance Criteria:** -- [ ] `DEFAULT_OPERATOR_ETH_FEE` updated to `1_775_500_000` (closest packable value rounding up) or team explicitly documents acceptance of the current rounded value -- [ ] Value is verified to be divisible by `ETH_DEDUCTED_DIGITS` (100,000) -- [ ] DIP-X document updated to note the rounding constraint if current value is kept -- [ ] Existing unit tests still pass with updated constant - -**Agent Instructions:** -1. Read `contracts/libraries/SSVCoreTypes.sol`, find the `DEFAULT_OPERATOR_ETH_FEE` constant. -2. Verify `1_775_500_000 % 100_000 == 0` (it is). -3. Change `DEFAULT_OPERATOR_ETH_FEE = 1770_000_000` to `DEFAULT_OPERATOR_ETH_FEE = 1_775_500_000`. -4. Run `npx hardhat compile` to verify compilation. -5. Run `npm run test:unit` to verify no regressions. -6. If tests fail due to hardcoded expectations, update test constants to match. - -#### Sub-items: -- [ ] Sub-task 1: Update `DEFAULT_OPERATOR_ETH_FEE` constant or document acceptance of current value -- [ ] Sub-task 2: Verify packability and run tests -- [ ] Sub-task 3: Update DIP-X if needed - ---- - -### [BUG-8] ~~Cooldown duration uses `block.timestamp` but DIP specifies blocks~~ -- **Type:** ~~Critical Bug Fix~~ -- **Priority:** ~~P1~~ Closed -- **Status:** ✅ Closed (not a bug) -- **Owner:** N/A -- **Timeline:** N/A -- **Github Link:** N/A -- **DIP-X Review Source:** SSV Staking review finding DIP-8 - -**Resolution:** Implementation correctly uses `block.timestamp` (seconds). The deployment config (`deployments/hoodi-prod/config.json`) already has `cooldownDuration: 604800` (7 days in seconds). The DIP spec wording saying "blocks" was imprecise — team confirmed (Yurii) it's seconds. The spreadsheet value `50120` was a blocks-equivalent reference, not the actual config value. - -**Requirement:** -The DIP-X governance table explicitly states `cooldownDuration` is "in blocks" with initial value "50120 (7 days)" and setter `updateUnstakeCooldownDuration(uint64 blocks)`. However, the implementation uses `block.timestamp` (seconds-based), not `block.number`. This creates a critical configuration risk: if `cooldownDuration` is initialized to 50120 thinking it's blocks, the actual cooldown would be ~13.9 hours instead of 7 days. - -**Context:** -`SSVStaking.sol:88`: `uint64 unlockTime = uint64(block.timestamp + s.cooldownDuration)`. The `UnstakeRequest` struct field is named `unlockTime` (timestamp-like), and `SSVStaking.sol:232` checks `requests[i].unlockTime <= block.timestamp`. Using `block.timestamp` is actually more reliable for user-facing cooldowns (block times can vary), so the implementation choice is reasonable — but the DIP/spec and the initial value must align. If using seconds, the correct 7-day value is 604,800, not 50,120. - -**Acceptance Criteria:** -- [ ] Either: DIP-X updated to say "in seconds" and initial value changed to `604800` (7 days in seconds) -- [ ] Or: implementation changed to use `block.number` instead of `block.timestamp` to match DIP -- [ ] The upgrade initializer sets the correct value for whichever unit is chosen -- [ ] `updateUnstakeCooldownDuration` parameter is documented with correct units -- [ ] Existing tests verified to use the correct unit - -**Agent Instructions:** -1. Read `contracts/modules/SSVStaking.sol`, focus on `requestUnstake` (line 66) and `calculateTotalUnfrozenBalance` (line 226). -2. Read `contracts/modules/SSVDAO.sol`, focus on `updateUnstakeCooldownDuration` (line 245). -3. Read `contracts/upgrades/stage/hoodi/SSVNetworkSSVStakingUpgrade.sol` for the initial value set during upgrade. -4. Recommended fix (simpler): Keep `block.timestamp` usage (it's better UX), but: - a. Update the DIP-X governance table to say "in seconds" instead of "in blocks" - b. Ensure the upgrade initializer sets `cooldownDuration = 604800` (7 days in seconds) - c. Update `updateUnstakeCooldownDuration` parameter name from `blocks` to `duration` in the interface -5. Check deployment configs (`deployments/hoodi-prod/config.json`, `deployments/hoodi-stage/config.json`) for the cooldown value and verify it matches the chosen unit. -6. Run `npm run test:unit`. - -#### Sub-items: -- [ ] Sub-task 1: Decide on units (seconds vs blocks) and align implementation + DIP -- [ ] Sub-task 2: Verify upgrade initializer sets correct value for chosen unit -- [ ] Sub-task 3: Update interface parameter name if needed -- [ ] Sub-task 4: Run full test suite - ---- - -### [BUG-9] ~~`uint64(delta)` silent truncation in operator earnings accumulation~~ -- **Type:** ~~Critical Bug Fix~~ -- **Priority:** ~~P1~~ Closed -- **Status:** ✅ Closed (not realistic) -- **Owner:** N/A -- **Timeline:** N/A -- **Github Link:** N/A - -**Resolution:** Overflow is not realistic under DAO-enforced fee caps. Worst case with `maxOperatorEthFee = 5,326,300,000` wei/block (DAO cap), 500 validators at max EB (2048 ETH), and 1 year without any snapshot update: `delta ≈ 4.48e15`, which is **4,100x below** `uint64.max` (1.845e19). Even at 10 years with zero snapshot updates (impossible in practice — every cluster operation triggers a snapshot), delta would still be 400x below the threshold. The original audit example used an unrestricted fee value not bounded by the DAO's `maxOperatorEthFee`. - -**Original context (for reference):** -In `OperatorLib.sol:68-69` (also lines 93-94, 326-327), `PackedETH.wrap(uint64(delta))` silently truncates when delta exceeds `uint64.max` (1.845e19). With 500 validators at max EB (2048 ETH), 2.7 years between snapshots: `delta = 4.078e21`, which is 221x larger than `uint64.max`. The operator loses ~99.5% of accumulated earnings. - -**Concrete example:** Operator with `effectiveVUnits=320,000,000`, `ethFee=17,700` packed, `7,200,000` block gap → `delta = 320_000_000 * 17_700 * 7_200_000 = 4.078e16 * 100_000 = 4.078e21`, which overflows `uint64.max` and silently truncates. - -**Acceptance Criteria:** -- [ ] `delta` exceeding `uint64.max` either reverts with a clear error or is safely handled -- [ ] Use `SafeCast.toUint64(delta)` or add `require(delta <= type(uint64).max)` at all three locations -- [ ] Existing tests pass -- [ ] New test: operator with high vUnits and long gap → verify no silent truncation - -**Agent Instructions:** -1. Read `contracts/libraries/OperatorLib.sol`, focus on lines 68-69, 93-94, and 326-327. -2. Import OpenZeppelin's `SafeCast` or add manual bounds checks. -3. Replace `uint64(delta)` with `SafeCast.toUint64(delta)` at all three locations. -4. Add a unit test with high vUnits and long block gap to verify the fix catches overflow. -5. Run `npm run test:unit`. - -#### Sub-items: -- [ ] Sub-task 1: Replace `uint64(delta)` with SafeCast at all three locations in OperatorLib.sol -- [ ] Sub-task 2: Add unit test for operator earnings overflow scenario -- [ ] Sub-task 3: Run full test suite - ---- - -### [BUG-17] `commitRoot` quorum can become unreachable due to truncation in per-oracle weight math -- **Type:** Critical Bug Fix -- **Priority:** P0 -- **Status:** Open -- **Owner:** (unassigned) -- **Timeline:** Before mainnet launch -- **Github Link:** (empty) - -**Requirement:** -Fix `commitRoot` so that the configured oracle quorum remains reachable even when the frozen cSSV supply for a voting round is not divisible by the oracle count. - -**Context:** -`commitRoot` freezes `cSSV.totalSupply()` on the first vote of a `(blockNum, merkleRoot)` round to prevent inter-vote supply drift. That mitigation is correct and must remain in place. However, the function then computes: -- `weight = totalStaked / defaultOracleIds.length` -- `threshold = (totalStaked * quorumBps) / 10_000` - -This mixes two separately-truncated quantities. With 4 oracle slots and 75% quorum, if the frozen supply is `4q + 2` or `4q + 3`, three votes accumulate only `3q` weight while the threshold becomes `3q + 1`, so 3-of-4 consensus is mathematically unreachable. At 100% quorum, even 4 votes fail whenever the frozen supply is not divisible by 4. - -This is distinct from the already-mitigated front-running issue tracked in SEC-5. Freezing supply removes the moving-target quorum problem between votes; it does not remove truncation mismatch inside the fixed round arithmetic. - -**Vulnerability Details:** -- The bug is present in `contracts/modules/SSVDAO.sol` where vote weight and threshold are derived from the same frozen supply but rounded in different ways. -- The current specs mirror the same arithmetic, so documentation does not currently protect against the edge case. -- A minimal regression test now demonstrates the issue in `test/unit/SSVDAO/commitRoot.test.ts`: with `totalSupply = 1_000_000_002` and `quorumBps = 7500`, the third oracle vote should commit under intended 3-of-4 semantics, but does not. - -**Proposed Fix:** -Do not add new storage. Keep `roundFrozenSupply` and `rootCommitments` unchanged, and compute the quorum threshold in oracle-vote space instead of raw token space: - -```solidity -uint256 oracleCount = s.defaultOracleIds.length; -uint256 weight = totalStaked / oracleCount; - -seb.rootCommitments[commitmentKey] += weight; -uint256 accumulatedWeight = seb.rootCommitments[commitmentKey]; - -uint256 votesNeeded = (oracleCount * s.quorumBps + BPS_DENOMINATOR - 1) / BPS_DENOMINATOR; -uint256 threshold = votesNeeded * weight; -``` - -This preserves: -- frozen per-round supply -- current storage layout -- current `WeightedRootProposed` event shape -- current behavior where quorum updates between votes affect the next vote - -It also restores the intended semantics: -- 75% quorum with 4 oracles requires 3 votes -- 100% quorum with 4 oracles requires 4 votes - -**Acceptance Criteria:** -- [ ] With 4 oracles and `quorumBps = 7500`, the third vote commits even when frozen supply is not divisible by 4 -- [ ] With 4 oracles and `quorumBps = 10000`, the fourth vote commits even when frozen supply is not divisible by 4 -- [ ] `roundFrozenSupply` logic remains unchanged and still fixes inter-vote supply drift -- [ ] No storage layout changes are introduced -- [ ] Existing quorum behavior for low thresholds (for example `quorumBps = 1`) remains intact -- [ ] Unit test coverage includes at least one truncation regression case - -**Agent Instructions:** -1. Read `contracts/modules/SSVDAO.sol`, focusing on `commitRoot`. -2. Keep the existing frozen-supply logic (`roundFrozenSupply`) exactly as-is. -3. Do not add a new storage mapping such as `rootVotes`. -4. Change quorum threshold computation to use `ceil(oracleCount * quorumBps / 10_000)` votes, then compare in the same truncated weight domain already used by `rootCommitments`. -5. Update or extend unit tests in `test/unit/SSVDAO/commitRoot.test.ts` to cover: - - 75% quorum with non-divisible frozen supply - - 100% quorum with non-divisible frozen supply -6. Update `docs/SPEC.md` and `docs/FLOWS.md` to describe vote-based quorum thresholding over equal oracle slots while still noting that supply is frozen per round. - -#### Sub-items: -- [x] Add failing regression test demonstrating unreachable 3-of-4 quorum with non-divisible supply -- [ ] Patch `commitRoot` threshold math without storage-layout changes -- [ ] Add regression test for 100% quorum with non-divisible supply -- [ ] Update SPEC/FLOWS to reflect corrected quorum calculation -- [ ] Run targeted DAO/oracle tests and verify no regressions - ---- - -### [BUG-18] Staking Rewards Accumulator Precision Loss - -**File:** `contracts/modules/SSVStaking.sol` L202 -**Severity:** Low - -**Description:** The `accEthPerShare` accumulator increment can round to zero when `newFeesWei * PRECISION < totalStaked`. Those fees are absorbed into `stakingEthPoolBalance` but never distributed to stakers. With the minimum packed fee increment of 100,000 wei (`ETH_DEDUCTED_DIGITS`) and PRECISION of 1e18, any `totalStaked > 1e23` (100,000 SSV tokens at 18 decimals) causes the smallest fee increment to round to zero. - -**Code:** -```solidity -s.accEthPerShare += uint128((newFeesWei * PRECISION) / totalStaked); -// When newFeesWei * 1e18 < totalStaked, this adds 0 -``` - -**Recommendation:** This is inherent to the accumulator pattern. The dust loss per sync is bounded by `totalStaked / PRECISION` wei (~0.0001 ETH for 100k SSV staked). For production parameters this is negligible, but consider documenting this as a known limitation. Alternatively, accumulate un-distributed remainders: -```solidity -uint256 scaledFees = newFeesWei * PRECISION; -uint256 distributed = (scaledFees / totalStaked) * totalStaked; -s.accEthPerShare += uint128(scaledFees / totalStaked); -s.undistributedDust += scaledFees - distributed; // carry forward -``` -**Resolution:** -BUG-18 is a standard accumulator dust issue. SSV supply is mintable, so we should not frame this as mathematically impossible forever. But under the current fee path, full zero-rounding only becomes reachable in the absolute smallest live case above 3.55B SSV staked, which is more than 200x current supply scale, and realistic operating conditions push the threshold far higher. Even with substantial token growth, the worst-case annual dust remains negligible and in the safe direction as tiny contract surplus. - ---- - -### [BUG-19] Aggregate vs per-cluster rounding causes conservation law violation - -**Severity:** MEDIUM -**Functions:** `OperatorLib.updateSnapshotSt()` at [`OperatorLib.sol:52-72`](contracts/libraries/OperatorLib.sol#L52-L72), `ProtocolLib.networkTotalEarnings()` at [`ProtocolLib.sol:84-90`](contracts/libraries/ProtocolLib.sol#L84-L90), `ClusterLib.updateBalanceWithEB()` at [`ClusterLib.sol:306-321`](contracts/libraries/ClusterLib.sol#L306-L321) -**Invariant:** `Σ(operator_earnings) + DAO_earnings == Σ(cluster_fees_paid)` (ETH Conservation) - -**Mechanism:** - -Each cluster pays fees proportional to its own `vUnits`: -```solidity -// Per-cluster payment (ClusterLib.updateBalanceWithEB) -networkFeeUnits = (idxNet * units_cluster) / BPS_DENOMINATOR; // floor division -operatorFeeUnits = (idxOp * units_cluster) / BPS_DENOMINATOR; // floor division -``` - -But operators earn proportional to their **aggregate** `effectiveVUnits` across ALL clusters: -```solidity -// Per-operator earnings (OperatorLib.updateSnapshotSt) -delta = (blockDiffEthFee * effectiveVUnits_total) / BPS_DENOMINATOR; // floor division -``` - -And the DAO earns proportional to aggregate `daoTotalEthVUnits`: -```solidity -// DAO earnings (ProtocolLib.networkTotalEarnings) -earningsUnits = (idx * ethNetworkFee * daoTotalEthVUnits) / BPS_DENOMINATOR; -``` - -Due to the mathematical property `floor(a×x/n) + floor(a×y/n) ≤ floor(a×(x+y)/n)`: - -``` -Σ(cluster_i_payment) ≤ operator_aggregate_earnings -Σ(cluster_i_network_fee) ≤ DAO_aggregate_earnings -``` - -**Impact:** - -Operators and the DAO **virtually earn slightly more** than clusters collectively pay. This creates a slow insolvency drift where the sum of all claimable balances (operator earnings + DAO rewards) exceeds the ETH actually deposited by cluster owners. - -**Bounded magnitude:** -- Per settlement: at most `(numClusters - 1) × ETH_DEDUCTED_DIGITS` wei = `(N-1) × 100,000 wei` -- Per year (2.5M blocks): with 1,000 clusters = ~0.00025 ETH/year - -**Recommendation:** -This is a known DeFi pattern and the drift is negligible in practice. For completeness, consider documenting this as an accepted known issue. No code change required unless operating at extreme scale (>100K clusters sustained for years). - -**Resolution:** -BUG-19 is a real but negligible rounding issue. It is completely inactive while clusters remain at default `32 ETH` effective balance, and only activates once post-Pectra effective-balance diversity appears. In a contract-faithful mainnet-scale simulation (`150,000` validators, `1,100` clusters, `1,900` operators), the yearly net drift stays on the order of tens of nano-ETH, and even under doubled growth scenarios remains operationally irrelevant. The practical recommendation is to treat BUG-19 as a known precision limitation, not a meaningful mainnet risk or a blocker to launch. - ---- - -### [BUG-20]: ~~Dust permanently trapped on reward claim with zero cSSV balance~~ - -**Severity:** LOW -**Function:** `SSVStaking.claimEthRewards()` at [`SSVStaking.sol:109-139`](contracts/modules/SSVStaking.sol#L109-L139) -**Invariant:** `Σ(user.accrued) + Σ(claimed) = total distributed via accEthPerShare` - -**Mechanism:** - -```solidity -uint256 payout = claimable - (claimable % ETH_DEDUCTED_DIGITS); -// ... -uint256 remainder = claimable - payout; -s.accrued[msg.sender] = (remainder != 0 && userBalance == 0) ? 0 : remainder; -// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -// Dust zeroed without returning to pool -``` - -When a user has zero cSSV and a sub-precision remainder (`< ETH_DEDUCTED_DIGITS = 100,000 wei`), the remainder is deleted from `accrued` but NOT returned to `stakingEthPoolBalance` or `ethDaoBalance`. The dust remains in both virtual accounting variables and in the contract's actual ETH balance, permanently locked. - -**Impact:** -- Maximum dust per user: 99,999 wei (~0.0000001 ETH) -- Cumulative impact over thousands of users: could reach a few cents to a few dollars total -- The contract slowly accumulates a tiny amount of unclaimable ETH - -**Recommendation:** -Accept as known behavior (trivial magnitude) or return dust to the pool: -```solidity -if (remainder != 0 && userBalance == 0) { - s.accrued[msg.sender] = 0; - // Optionally: redistribute dust back to pool for other stakers -} -``` - -**Resolution:** ✅ Closed — The SEC-16b fix covers this exact code path. Maximum dust per user (99,999 wei) is accepted as negligible. Cross-referenced in CONSOLIDATED-AUDIT-FINDINGS CA-17. - ---- - -## Security Hardening - -### [SEC-1] `updateQuorumBps(0)` allows zero-threshold oracle commits -- **Type:** Security Hardening -- **Priority:** P2 (downgraded from P0) -- **Status:** ✅ Mitigated (owner-only) -- **Owner:** N/A -- **Timeline:** N/A -- **Github Link:** N/A - -**Requirement:** -Add a minimum quorum validation to `updateQuorumBps`. A quorum of 0 allows a single oracle vote to commit any root. - -**Context:** -`SSVDAO.sol:234-239`: The function only checks `quorum > BPS_DENOMINATOR` (max bound). Setting `quorumBps = 0` makes the threshold in `commitRoot` (line 186) equal to 0, meaning any single oracle can unilaterally commit roots. Combined with SEC-2 (quorum defaults to 0 after upgrade), this is an immediate post-upgrade vulnerability. - -**Mitigation:** Downgraded to P2. `updateQuorumBps` is owner-only (DAO multisig). A compromised or negligent owner can already upgrade the entire contract, so zero-quorum via the setter is not an independent attack vector. The critical path (SEC-2: quorum defaulting to 0 after upgrade) is already fixed in PR #431 by validating quorumBps in the initializer. - -**Acceptance Criteria:** -- [ ] `updateQuorumBps(0)` reverts with `InvalidQuorum()` -- [ ] A reasonable minimum is enforced (e.g., `quorum >= 2500` for 25%, or at minimum `quorum > 0`) -- [ ] Existing tests for `updateQuorumBps` updated to reflect new validation -- [ ] New test: call `updateQuorumBps(0)` → expect revert - -**Agent Instructions:** -1. Read `contracts/modules/SSVDAO.sol`, focus on `updateQuorumBps` (line 234). -2. Add `if (quorum == 0) revert InvalidQuorum();` before the existing check. Consider also adding a minimum like `if (quorum < 2500)` for stronger safety. -3. Read `test/unit/SSVDAO/updateQuorumBps.test.ts` for existing test patterns. -4. Add a test case for `updateQuorumBps(0)` expecting `InvalidQuorum` revert. -5. Run `npm run test:unit`. - -#### Sub-items: -- [ ] Sub-task 1: Add minimum quorum validation to `updateQuorumBps` -- [ ] Sub-task 2: Update/add unit tests for quorum boundary -- [ ] Sub-task 3: Run full test suite - ---- - -### [SEC-2] ~~`quorumBps` not initialized during upgrade — zero by default~~ -- **Type:** Security Hardening -- **Priority:** P0 -- **Status:** ✅ Fixed -- **Owner:** (resolved) -- **Timeline:** (complete) -- **Github Link:** [PR #431](https://github.com/ssvlabs/ssv-network/pull/431) - -**Requirement:** -Set `quorumBps` during the upgrade initializer (`reinitializer(3)`) to prevent a window where any oracle can unilaterally commit roots. - -**Context:** -`SSVNetworkSSVStakingUpgrade.sol` (line 8) initialized `cooldownDuration` and `defaultOracleIds` but NOT `quorumBps`. After upgrade, `quorumBps` was 0 in storage until the DAO manually called `updateQuorumBps()`. During this window, combined with SEC-1, a single oracle could commit arbitrary Merkle roots. Now fixed — see Resolution below. - -**Resolution:** -`initializeSSVStaking` now accepts `quorumBps` as a third parameter (`uint16`) and validates `if (quorumBps == 0 || quorumBps > 10_000) revert InvalidQuorum()` before writing to storage. Both `upgrade.ts` and `generate-safe-batch.ts` pass `quorumBps` from the deployment config. This closes the initialization window entirely. - -**Acceptance Criteria:** -- [x] `quorumBps` is set during the upgrade initializer to a safe default (7500 = 75% per DIP-X spec) -- [x] Initializer validates `quorumBps != 0` (rejects zero with `InvalidQuorum`) -- [x] Post-upgrade verification confirms `quorumBps != 0` - -**Agent Instructions:** -1. Read `contracts/upgrades/stage/hoodi/SSVNetworkSSVStakingUpgrade.sol` (line 8). -2. Option A (preferred): Add `SSVStorageStaking.load().quorumBps = 7500;` to the `initializeSSVStaking` function. Also add `quorumBps` as a parameter: `initializeSSVStaking(uint64 cooldownDuration, uint32[4] memory defaultOracleIds, uint16 quorumBps)`. Update the function signature in `scripts/upgrade.ts` and `scripts/generate-safe-batch.ts` accordingly. -3. Option B (simpler): Add a hardcoded `SSVStorageStaking.load().quorumBps = 7500;` directly in the initializer without adding a parameter. -4. Emit `QuorumUpdated(7500)` event after setting. -5. Update the initializer ABI references in deploy scripts. -6. Run `npm run test:unit` and `npm run test:integration`. - -#### Sub-items: -- [x] Sub-task 1: Add `quorumBps` initialization to upgrade initializer -- [x] Sub-task 2: Update deploy scripts to match new signature -- [ ] Sub-task 3: Add test verifying `quorumBps` is set after upgrade -- [ ] Sub-task 4: Run full test suite - ---- - -### [SEC-3] ~~`replaceOracle` doesn't invalidate pending votes~~ -- **Type:** Security Hardening -- **Priority:** ~~P1~~ P2 (downgraded) -- **Status:** ✅ Mitigated (owner-only + coordinated oracles) -- **Owner:** N/A -- **Timeline:** N/A -- **Github Link:** N/A - -**Resolution:** `replaceOracle` is owner-only (DAO multisig), and the oracle set is a small coordinated group working with the DAO. If an oracle is compromised and replaced mid-vote, the remaining honest oracles can simply propose and vote on a correct root — the compromised oracle's stale vote alone cannot reach quorum (needs 3-of-4). Any edge case is resolvable operationally by the DAO + oracle operators. - -**Original context (for reference):** -`SSVDAO.sol:205-229`: When `replaceOracle` is called, the old oracle's address is removed from `oracleIdOf` but the `oracleId` stays the same. The `hasVoted` mapping uses `oracleId`, so: (1) the old oracle's votes persist and count toward quorum, (2) the new oracle cannot re-vote on pending commitments since `hasVoted[commitmentKey][oracleId]` is already true. A compromised oracle replaced mid-vote still influences quorum. - -**Acceptance Criteria:** -- [ ] Either: pending votes for the replaced oracleId are reset when `replaceOracle` is called -- [ ] Or: this behavior is explicitly documented with risk analysis, and a mechanism exists to clear stale votes if needed -- [ ] Test: replace oracle mid-vote → verify new oracle can vote on pending commitments - -**Agent Instructions:** -1. Read `contracts/modules/SSVDAO.sol`, focus on `replaceOracle` (line 205) and `commitRoot` (line 155). -2. Read the `SSVStorageEB` storage struct to understand the `hasVoted` and `commitmentWeight` mappings. -3. To reset pending votes: after replacing the oracle, iterate over pending commitments and clear `hasVoted[commitmentKey][oracleId]` and subtract the old oracle's weight from `commitmentWeight[commitmentKey]`. However, this requires tracking pending commitments, which may not be stored. -4. Simpler alternative: add a `voteNonce` per oracleId. Increment it on replacement. Use `keccak256(commitmentKey, oracleId, voteNonce)` for the hasVoted key. This invalidates all old votes automatically. -5. Ensure the fix doesn't break the quorum mechanism for non-replaced oracles. -6. Run `npm run test:unit`. - -#### Sub-items: -- [ ] Sub-task 1: Design vote invalidation mechanism -- [ ] Sub-task 2: Implement in `replaceOracle` and `commitRoot` -- [ ] Sub-task 3: Write tests for oracle replacement mid-vote -- [ ] Sub-task 4: Run full test suite - ---- - -### [SEC-4] ~~`updateUnstakeCooldownDuration` allows zero cooldown~~ -- **Type:** Security Hardening -- **Priority:** ~~P1~~ P2 (downgraded) -- **Status:** ✅ Mitigated (owner-only, no accounting risk) -- **Owner:** N/A -- **Timeline:** N/A -- **Github Link:** N/A - -**Resolution:** `updateUnstakeCooldownDuration` is owner-only (DAO multisig). Zero cooldown allows instant unstaking but causes no accounting issues — `requestUnstake` still goes through `_syncFees`, `_settleWithBalance`, cSSV burn, and proper reward settlement. The "stake/vote/unstake" attack described below isn't viable because oracle voting is based on oracle addresses (not staking), and staking weight only affects quorum threshold which is DAO-controlled. Same owner-trust argument as SEC-1/SEC-3. - -**Original context (for reference):** -`SSVDAO.sol:245-248`: No minimum check. Zero cooldown allows stake/vote/unstake in one block, defeating the economic security mechanism. An attacker could stake, earn oracle voting rights, manipulate a vote, and immediately unstake. - -**Acceptance Criteria:** -- [ ] `updateUnstakeCooldownDuration(0)` reverts -- [ ] A reasonable minimum is enforced (e.g., 1 day = 86400 seconds) -- [ ] Existing tests updated - -**Agent Instructions:** -1. Read `contracts/modules/SSVDAO.sol`, focus on `updateUnstakeCooldownDuration` (line 245). -2. Add `if (duration == 0) revert InvalidCooldownDuration();` (define new error in `ISSVNetworkCore.sol` if needed, or reuse an existing generic error). -3. Consider adding a minimum like `if (duration < 86400) revert ...;` for 1-day minimum. -4. Update `test/unit/SSVDAO/updateUnstakeCooldownDuration.test.ts`. -5. Run `npm run test:unit`. - -#### Sub-items: -- [ ] Sub-task 1: Add minimum cooldown validation -- [ ] Sub-task 2: Update/add unit tests -- [ ] Sub-task 3: Run full test suite - ---- - -### [SEC-5] ~~`totalStaked` changes between oracle votes (front-running risk)~~ -- **Type:** Security Hardening -- **Priority:** ~~P1~~ P2 (downgraded) -- **Status:** ✅ Mitigated (impractical) - -**Resolution:** Oracles vote 3 times per day across separate blocks. To block quorum, an attacker would need to stake exponentially increasing amounts of SSV between each vote (e.g., 9K → 90K → 900K). This is economically impractical — the attacker's SSV is locked in cooldown, and the capital requirement grows exponentially per blocked commitment. Even if one commitment is blocked, oracles simply propose a new one. Pure liveness attack with no safety impact (can't force bad roots). -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Snapshot `totalStaked` at the start of a voting round (first proposal) and use the snapshotted value for all subsequent votes in that round, preventing front-running via stake/unstake between votes. - -**Context:** -`SSVDAO.sol:155-200` (`commitRoot`): Each oracle vote reads `totalStaked` fresh (line 172). Between votes, `totalStaked` can change via stake/unstake. This makes the quorum threshold inconsistent within a single voting round — someone could front-run oracle votes with large stake/unstake operations to either block legitimate quorum or force premature quorum. - -**Acceptance Criteria:** -- [ ] `totalStaked` is captured once per voting round and used for all votes in that round -- [ ] Weight calculation and threshold calculation use the same snapshotted value -- [ ] Test: oracle A votes, large stake change, oracle B votes → quorum uses consistent weight - -**Agent Instructions:** -1. Read `contracts/modules/SSVDAO.sol`, focus on `commitRoot` (line 155). -2. Read `contracts/libraries/storage/SSVStorageEB.sol` to understand what state is tracked per commitment. -3. Design: Add a `snapshotTotalStaked` field to the commitment state. On first vote for a new commitmentKey, snapshot `totalStaked`. On subsequent votes, use the snapshot instead of re-reading. -4. Store the snapshot in `SSVStorageEB` alongside `commitmentWeight`. -5. When a commitment is finalized (root committed), clean up the snapshot. -6. This is a more involved change — be careful not to break existing oracle voting logic. -7. Run `npm run test:unit` and `npm run test:integration`. - -#### Sub-items: -- [ ] Sub-task 1: Add `snapshotTotalStaked` to commitment state in SSVStorageEB -- [ ] Sub-task 2: Snapshot on first vote, use snapshot for subsequent votes -- [ ] Sub-task 3: Clean up snapshot on commitment finalization -- [ ] Sub-task 4: Write tests for consistent weight across votes -- [ ] Sub-task 5: Run full test suite - ---- - -### [SEC-6] Add `nonReentrant` to `migrateClusterToETH` -- **Type:** Security Hardening -- **Priority:** P2 -- **Status:** Open -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Add the `nonReentrant` modifier to `migrateClusterToETH` for defense-in-depth. The function calls `CoreLib.transferTokenBalance` (SSV ERC20 transfer) at line 341. - -**Context:** -`SSVClusters.sol:264`: While the SSV token is a standard ERC20 without transfer hooks (so reentrancy via token callback is unlikely), adding `nonReentrant` follows the codebase's established pattern for functions that make external calls. State changes happen before the transfer (checks-effects-interactions), but the modifier provides an additional safety layer. - -**Acceptance Criteria:** -- [ ] `migrateClusterToETH` has the `nonReentrant` modifier -- [ ] Existing migration tests still pass - -**Agent Instructions:** -1. Read `contracts/modules/SSVClusters.sol`, focus on `migrateClusterToETH` (line 264). -2. Add `nonReentrant` modifier to the function signature, following the pattern used by `liquidate`, `withdraw`, etc. -3. Run `npm run test:unit`. - -#### Sub-items: -- [ ] Sub-task 1: Add `nonReentrant` modifier to `migrateClusterToETH` -- [ ] Sub-task 2: Run full test suite - ---- - -### [SEC-7] Add `nonReentrant` to `onCSSVTransfer` -- **Type:** Security Hardening -- **Priority:** P2 -- **Status:** Open -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Add `nonReentrant` modifier to `onCSSVTransfer` for defense-in-depth consistency. - -**Context:** -`SSVStaking.sol:169`: The function makes external calls to `ICSSVToken.totalSupply()` and `ICSSVToken.balanceOf()`. While the cSSV token is trusted (deployed by the protocol), the modifier provides protection if cSSV is ever upgraded or replaced. All other staking functions already have `nonReentrant`. - -**Acceptance Criteria:** -- [ ] `onCSSVTransfer` has the `nonReentrant` modifier -- [ ] Existing staking tests still pass - -**Agent Instructions:** -1. Read `contracts/modules/SSVStaking.sol`, focus on `onCSSVTransfer` (line 169). -2. Add `nonReentrant` modifier. Import `SSVReentrancyGuard` if not already imported. -3. Run `npm run test:unit`. - -#### Sub-items: -- [ ] Sub-task 1: Add `nonReentrant` modifier to `onCSSVTransfer` -- [ ] Sub-task 2: Run full test suite - ---- - -### [SEC-8] `reactivate` not emitting warning for removed operators -- **Type:** Security Hardening -- **Priority:** P2 -- **Status:** Open -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -When a cluster is reactivated and one or more of its operators have been removed, emit an event indicating which operators are inactive so users and off-chain systems are aware. - -**Context:** -`SSVClusters.sol:133-185`: `reactivate` calls `updateClusterOperatorsOnReactivation` (line 151), which skips removed operators at `OperatorLib.sol:311`. The cluster is reactivated with fewer active operators, but no event signals this. Users may not realize their cluster is running with reduced operator coverage. - -**Acceptance Criteria:** -- [ ] A new event (e.g., `InactiveOperatorInCluster(uint64 operatorId)`) is emitted for each removed operator during reactivation -- [ ] OR: existing `ClusterReactivated` event includes information about skipped operators -- [ ] Test: reactivate a cluster with a removed operator → verify event emission - -**Agent Instructions:** -1. Read `contracts/modules/SSVClusters.sol`, focus on `reactivate` (line 133). -2. Read `contracts/libraries/OperatorLib.sol`, focus on `updateClusterOperatorsOnReactivation` (line 295), particularly the `ethSnapshot.block != 0` check at line 311. -3. Add return data from `updateClusterOperatorsOnReactivation` that indicates which operators were skipped, or emit events directly from the library function. -4. Define the new event in `ISSVClusters.sol`. -5. Add test in `test/unit/SSVClusters/reactivate.test.ts`. -6. Run `npm run test:unit`. - -#### Sub-items: -- [ ] Sub-task 1: Define and emit inactive operator event -- [ ] Sub-task 2: Write test for reactivation with removed operator event -- [ ] Sub-task 3: Run full test suite - ---- - -### [SEC-9] `operatorMaxFee` function signature differs from DIP-X spec -- **Type:** Security Hardening -- **Priority:** P2 -- **Status:** Open -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) -- **DIP-X Review Source:** ETH Payments review finding ETH-13 - -**Requirement:** -The DIP-X governance table specifies `updateMaximumOperatorFee(uint64 maxFee)` but the implementation uses `updateMaximumOperatorFee(uint256 maxFee)`. While the `uint256` parameter is more user-friendly (users pass the full wei value, packing handles conversion), the DIP and implementation should be aligned. - -**Context:** -`SSVDAO.sol:138`: `function updateMaximumOperatorFee(uint256 maxFee)`. The `uint256` value is packed into `PackedETH` (uint64) internally via `PackedETHLib.pack(maxFee)`. This is a cosmetic interface difference, not a functional issue. The `uint256` parameter prevents users from needing to pre-pack their values. However, ABIs and documentation should be consistent. - -**Acceptance Criteria:** -- [ ] Either: DIP-X updated to document `uint256` parameter type (recommended — matches implementation's user-friendly design) -- [ ] Or: implementation changed to `uint64` to match DIP (not recommended — less user-friendly) -- [ ] ABI documentation updated to match - -**Agent Instructions:** -1. This is primarily a documentation alignment task. -2. Read `contracts/modules/SSVDAO.sol`, focus on `updateMaximumOperatorFee` (line 138). -3. Read `contracts/interfaces/ISSVDAO.sol` for the interface declaration. -4. Update the DIP-X governance table to specify `uint256` instead of `uint64`. -5. No code change needed if DIP is updated. - -#### Sub-items: -- [ ] Sub-task 1: Align DIP-X and implementation on parameter type -- [ ] Sub-task 2: Update ABI documentation - ---- - -### [SEC-10] cSSV token lacks governance/voting extensions (ERC20Votes) -- **Type:** Security Hardening -- **Priority:** P2 -- **Status:** Open -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) -- **DIP-X Review Source:** SSV Staking review finding DIP-10 - -**Requirement:** -The DIP-X states: "Staked SSV, represented by cSSV, retains full governance and voting power. Holding cSSV does not reduce a user's ability to participate in DAO governance compared to holding unstaked SSV." However, `CSSVToken.sol` is a plain `ERC20` with no `ERC20Votes` or delegation mechanism. Whether governance rights are preserved depends entirely on off-chain configuration (e.g., Snapshot strategy). - -**Context:** -`CSSVToken.sol:10`: `contract CSSVToken is ERC20`. No `ERC20Votes`, no `ERC20VotesComp`, no delegation mechanism. The SSV DAO uses Snapshot (off-chain governance), which can be configured to count cSSV balances. If the Snapshot strategy includes cSSV, the DIP claim holds. If on-chain governance is ever needed, cSSV holders would lose voting power compared to SSV holders. - -**Acceptance Criteria:** -- [ ] Decision documented: is off-chain governance (Snapshot) the permanent governance mechanism? -- [ ] If yes: verify the Snapshot strategy is updated to include cSSV balances before mainnet launch -- [ ] If on-chain governance is planned: add `ERC20Votes` extension to `CSSVToken` -- [ ] DIP-X updated to clarify governance mechanism (on-chain vs off-chain) - -**Agent Instructions:** -1. Read `contracts/token/CSSVToken.sol` fully. -2. This is primarily a governance/product decision, not a pure code fix. -3. If the team confirms Snapshot is the permanent mechanism: - a. Ensure the Snapshot space strategy counts cSSV - b. Document this in the DIP and deployment runbook -4. If on-chain governance is needed: - a. Add `ERC20Votes` to `CSSVToken` inheritance - b. Override `_afterTokenTransfer` (or `_update` in OZ v5) to call `_transferVotingUnits` - c. Add `clock()` and `CLOCK_MODE()` overrides - d. This requires careful upgrade planning since `CSSVToken` is not upgradeable -5. Flag this for team decision before proceeding. - -#### Sub-items: -- [ ] Sub-task 1: Get team decision on governance mechanism -- [ ] Sub-task 2: Implement chosen approach (Snapshot config update or ERC20Votes addition) -- [ ] Sub-task 3: Update DIP-X governance section - ---- - -### [SEC-11] ~~`hasDeviation` reactivation optimization uses global counter for per-operator decision~~ -- **Type:** Security Hardening -- **Priority:** ~~P1~~ P3 (downgraded) -- **Status:** ✅ Closed (BUG-4 fix resolves root cause) -- **Owner:** N/A -- **Timeline:** N/A -- **Github Link:** N/A - -**Resolution:** The only known path to make `daoTotalEthVUnits` wrong was BUG-4 (double-subtraction on liquidated cluster validator removal), which is fixed in PR #429. The optimization is valid when the global counter is accurate. Removing it wouldn't provide a real safeguard — per-operator `operatorEthVUnits` values are updated by the same code paths as the global counter, so if a bug corrupts one, it likely corrupts both. - -**Original requirement:** -Replace the global `daoTotalEthVUnits` optimization in `updateClusterOperatorsOnReactivation` with per-operator `operatorEthVUnits` reads. - -**Context:** -In `OperatorLib.sol:305`, `bool hasDeviation = sp.daoTotalEthVUnits != uint64(sp.ethDaoValidatorCount) * BPS_DENOMINATOR` uses a global signal for per-operator decisions. While deviations are always non-negative (EB floor=32), this couples correctness to BUG-4's accounting accuracy. If `daoTotalEthVUnits` is ever incorrect (from BUG-4's double-subtraction), reactivation could skip reading actual per-operator deviation, leading to incorrect vUnit accounting. - -**Acceptance Criteria:** -- [ ] Reactivation always reads `seb.operatorEthVUnits[operatorId]` instead of relying on the global optimization -- [ ] No behavior change when global and per-operator values are consistent -- [ ] Correct behavior even when BUG-4 causes `daoTotalEthVUnits` to be incorrect -- [ ] Existing reactivation tests pass - -**Agent Instructions:** -1. Read `contracts/libraries/OperatorLib.sol`, focus on `updateClusterOperatorsOnReactivation` (line 295), particularly the `hasDeviation` check at line 305. -2. Remove the `hasDeviation` optimization and always read `seb.operatorEthVUnits[operatorId]` for each operator. -3. Run `npm run test:unit`. - -#### Sub-items: -- [ ] Sub-task 1: Remove global `hasDeviation` optimization, use per-operator reads -- [ ] Sub-task 2: Run full test suite - ---- - -### [SEC-12] `deposit()` accepts deposits to liquidated ETH clusters without fee settlement -- **Type:** Security Hardening -- **Priority:** P2 -- **Status:** Open -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Add `validateClusterIsNotLiquidated()` to the ETH `deposit()` function, or document the current behavior as intentional. - -**Context:** -In `SSVClusters.sol:190-205`, `deposit()` has no `validateClusterIsNotLiquidated()` check and no fee settlement. Compare with `withdraw()` at line 210 which does both. A user can deposit ETH into a liquidated cluster, but the deposit does not settle fees or reactivate the cluster. The event shows a misleading balance. The user must call `reactivate()` separately to resume the cluster. - -**Concrete example:** Cluster liquidated with `balance=0`, user deposits 1 ETH. No fee settlement occurs. Event shows misleading balance. User must call `reactivate()` separately. - -**Acceptance Criteria:** -- [ ] Either: `deposit()` reverts on liquidated clusters with `ClusterIsLiquidated()` -- [ ] Or: behavior is explicitly documented as intentional with rationale -- [ ] Test: deposit to liquidated cluster → verify defined behavior - -**Agent Instructions:** -1. Read `contracts/modules/SSVClusters.sol`, focus on `deposit` (line 190). -2. Compare with `withdraw()` at line 210 which validates cluster is not liquidated. -3. Add `cluster.validateClusterIsNotLiquidated()` before the balance update. -4. Add a test in `test/unit/SSVClusters/deposit.test.ts` for deposit to liquidated cluster. -5. Run `npm run test:unit`. - -#### Sub-items: -- [ ] Sub-task 1: Add liquidation check to `deposit()` or document as intentional -- [ ] Sub-task 2: Add test for deposit to liquidated cluster -- [ ] Sub-task 3: Run full test suite - ---- - -### [SEC-13] ~~`OperatorWithdrawn` event doesn't distinguish ETH vs SSV withdrawals~~ -- **Type:** Security Hardening -- **Priority:** P2 -- **Status:** ✅ Fixed -- **Owner:** (resolved) -- **Timeline:** (complete) -- **Github Link:** (empty) - -**Requirement:** -Keep `OperatorWithdrawn` for ETH withdrawals and introduce a new `OperatorWithdrawnSSV` event for SSV withdrawal earnings. This ensures 3rd-party SDKs and off-chain indexers can correctly track operator earnings by denomination without breaking existing integrations that already listen to `OperatorWithdrawn`. - -**Context:** -In `SSVOperators.sol:337-344`, both `_transferOperatorBalanceUnsafe` (ETH) and `_transferOperatorTokenBalanceUnsafe` (SSV) emit the same `OperatorWithdrawn` event. Off-chain indexers (SDK, oracle, dashboard) cannot distinguish between ETH and SSV withdrawal events, making it impossible to correctly calculate total accumulated operator earnings per denomination. - -**Decision:** -- `OperatorWithdrawn(operatorId, owner, value)` — **kept as-is**, emitted only by `_transferOperatorBalanceUnsafe` (ETH withdrawals) -- `OperatorWithdrawnSSV(operatorId, owner, value)` — **new event**, emitted only by `_transferOperatorTokenBalanceUnsafe` (SSV withdrawals) - -**Resolution:** -`OperatorWithdrawnSSV` event added to `contracts/interfaces/ISSVOperators.sol` with identical signature to `OperatorWithdrawn`. `_transferOperatorTokenBalanceUnsafe` now emits `OperatorWithdrawnSSV`; `_transferOperatorBalanceUnsafe` (ETH) is unchanged. Tests in `withdrawOperatorEarningsSSV.test.ts` updated to assert `OperatorWithdrawnSSV`. `OPERATOR_WITHDRAWN_SSV` constant added to `test/common/events.ts`. All 413 unit tests passing. - -**Acceptance Criteria:** -- [x] `OperatorWithdrawnSSV` event defined in `contracts/interfaces/ISSVOperators.sol` -- [x] `_transferOperatorBalanceUnsafe` emits `OperatorWithdrawn` (ETH) — no change -- [x] `_transferOperatorTokenBalanceUnsafe` emits `OperatorWithdrawnSSV` instead of `OperatorWithdrawn` -- [ ] Off-chain indexers and SDK updated to listen to `OperatorWithdrawnSSV` for SSV earnings -- [ ] ABI change impact documented for oracle and SDK clients - -#### Sub-items: -- [x] Sub-task 1: Define `OperatorWithdrawnSSV` event in `ISSVOperators.sol` -- [x] Sub-task 2: Update `_transferOperatorTokenBalanceUnsafe` to emit `OperatorWithdrawnSSV` -- [x] Sub-task 3: Update tests for new event signature -- [x] Sub-task 4: Run full test suite - ---- - -### [SEC-14] `commitRoot` accepts `bytes32(0)` as merkleRoot — permanently wastes block slot -- **Type:** Security Hardening -- **Priority:** P2 -- **Status:** Open -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Add a zero-root check to `commitRoot` to prevent permanently wasting a block slot with an unusable root. - -**Context:** -In `SSVDAO.sol:155`, `commitRoot` accepts `bytes32(0)` as a valid merkle root. The zero root is stored but unusable — `SSVClusters.sol:426` reverts on zero root during `updateClusterBalance`. Meanwhile, `latestCommittedBlock` advances, so the block slot is permanently consumed and cannot be reused. - -**Acceptance Criteria:** -- [ ] `commitRoot` reverts with `InvalidRoot()` when `merkleRoot == bytes32(0)` -- [ ] Define `InvalidRoot` error if it doesn't exist -- [ ] Test: commit zero root → expect revert - -**Agent Instructions:** -1. Read `contracts/modules/SSVDAO.sol`, focus on `commitRoot` (line 155). -2. Add `if (merkleRoot == bytes32(0)) revert InvalidRoot();` near the top of the function. -3. Define `InvalidRoot` error in `contracts/interfaces/ISSVNetworkCore.sol` if not already defined. -4. Add test in `test/unit/SSVDAO/commitRoot.test.ts`. -5. Run `npm run test:unit`. - -#### Sub-items: -- [ ] Sub-task 1: Add zero-root validation to `commitRoot` -- [ ] Sub-task 2: Add test for zero-root revert -- [ ] Sub-task 3: Run full test suite - ---- - -### [SEC-15] Min/max operator fee can be set to contradictory values -- **Type:** Security Hardening -- **Priority:** P2 -- **Status:** Open -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Add cross-validation between `updateMinimumOperatorEthFee` and `updateMaximumOperatorFee` to prevent contradictory values where `minFee > maxFee`. - -**Context:** -In `SSVDAO.sol:138-149`, neither setter cross-validates against the other. If `minFee > maxFee`, no valid non-zero fee exists for operator registration, effectively blocking all new operator registrations and fee changes. While both are owner-only functions, a configuration mistake could cause unexpected operational impact. - -**Acceptance Criteria:** -- [ ] `updateMinimumOperatorEthFee` reverts if the new min would exceed current max -- [ ] `updateMaximumOperatorFee` reverts if the new max would be below current min -- [ ] Test: set contradictory min/max → expect revert - -**Agent Instructions:** -1. Read `contracts/modules/SSVDAO.sol`, focus on `updateMinimumOperatorEthFee` (line 147) and `updateMaximumOperatorFee` (line 138). -2. In `updateMinimumOperatorEthFee`: add check `if (packed > sp.operatorMaxFeeETH) revert ...;`. -3. In `updateMaximumOperatorFee`: add check `if (packed < sp.operatorMinFeeETH) revert ...;`. -4. Add tests for both cross-validation directions. -5. Run `npm run test:unit`. - -#### Sub-items: -- [ ] Sub-task 1: Add cross-validation to both fee setters -- [ ] Sub-task 2: Add tests for contradictory fee values -- [ ] Sub-task 3: Run full test suite - ---- - -### [SEC-16] Missing zero-value/zero-address guards on deposit and withdraw -- **Type:** Security Hardening -- **Priority:** P2 -- **Status:** Open -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Add zero-value and zero-address guards to deposit and withdraw functions to prevent meaningless transactions. - -**Context:** -- `SSVClusters.sol:190` (`deposit`): no zero-address check for `clusterOwner`, no `msg.value > 0` check. -- `SSVClusters.sol:210` (`withdraw`): no zero-amount check. -- `SSVDAO.sol:52` (`withdrawNetworkSSVEarnings`): no zero-amount check. -These allow gas-wasting no-op transactions that emit misleading events with zero values. - -**Acceptance Criteria:** -- [ ] `deposit()` reverts when `msg.value == 0` -- [ ] `withdraw()` reverts when `amount == 0` -- [ ] `withdrawNetworkSSVEarnings()` reverts when `amount == 0` -- [ ] Tests added for each zero-value guard - -**Agent Instructions:** -1. Read `contracts/modules/SSVClusters.sol`, focus on `deposit` (line 190) and `withdraw` (line 210). -2. Read `contracts/modules/SSVDAO.sol`, focus on `withdrawNetworkSSVEarnings` (line 52). -3. Add `require(msg.value > 0)` to deposit, `require(amount > 0)` to withdraw functions. -4. Add tests for each guard. -5. Run `npm run test:unit`. - -#### Sub-items: -- [ ] Sub-task 1: Add zero-value guards to deposit and withdraw -- [ ] Sub-task 2: Add tests for zero-value reverts -- [ ] Sub-task 3: Run full test suite - ---- - -### [SEC-16b] ~~Dust ETH stranded in `accrued` after full cSSV transfer + claim~~ -- **Type:** Security Hardening -- **Priority:** P1 -- **Status:** ✅ Fixed -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -When a user transfers all their cSSV tokens and then calls `claimEthRewards`, a sub-`ETH_DEDUCTED_DIGITS` dust remainder is left in `s.accrued[msg.sender]`. Because the user holds no cSSV, `_settle` will never add to it again, so the dust is permanently unclaimable (any future `claimEthRewards` call hits the `payout == 0` revert). From the user's perspective the UI shows a non-zero claimable balance that can never be withdrawn. - -**Context:** -- `SSVStaking.sol:123`: `payout = claimable - (claimable % ETH_DEDUCTED_DIGITS)` — the remainder stays in `accrued`. -- `SSVStaking.sol:139` (original): `s.accrued[msg.sender] = claimable - payout` — remainder is preserved even when the user holds 0 cSSV. -- Reproduction: stake → transfer all cSSV to another address → call `claimEthRewards` → `accrued` contains dust that can never be claimed or grown. - -**Fix applied in `SSVStaking.sol:139-140`:** -```solidity -uint256 remainder = claimable - payout; -s.accrued[msg.sender] = (remainder != 0 && ICSSVToken(CSSV_ADDRESS).balanceOf(msg.sender) == 0) ? 0 : remainder; -``` -When `balanceOf == 0` and there is dust remainder, it is zeroed rather than preserved. The zeroed wei remains in `stakingEthPoolBalance` and `ethDaoBalance` — it is never deducted from the pool — so it is effectively redistributed to remaining stakers via future `accEthPerShare` increments in `_syncFees`. - -**Acceptance Criteria:** -- [x] `claimEthRewards` zeros `accrued` when caller holds 0 cSSV -- [x] After a full transfer + claim, `accrued[user] == 0` -- [x] Test: stake → transfer all cSSV → claim → assert `accrued == 0` -- [x] Test: user with cSSV still keeps remainder (no false positive) - -#### Sub-items: -- [x] Sub-task 1: Apply fix in `SSVStaking.sol` -- [x] Sub-task 2: Add regression tests (2 tests in `claimEthRewards.test.ts`) -- [x] Sub-task 3: Run full staking test suite — 64/64 passing - ---- - -### [SEC-17] DAO governance functions lack input guardrails (min/max/non-zero) -- **Type:** Security Hardening -- **Priority:** P1 -- **Status:** Open -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Add input validation guardrails (non-zero, min/max bounds) to all DAO-governed setter functions in `SSVDAO.sol`. Currently most functions accept any value including `0`, which can be harmful to the protocol. While the DAO multisig (5/7) mitigates the risk of accidental misconfiguration, defense-in-depth requires on-chain guardrails. - -**⚠️ Action required:** Consult Product/governance team to define the concrete min/max bounds for each parameter before implementation. The table below uses `TBD` placeholders. - -**Context:** -`SSVDAO.sol` contains 12 setter functions. Only 2 have any input validation today: -- `updateLiquidationThresholdPeriod` / `updateLiquidationThresholdPeriodSSV`: enforce `>= MINIMAL_LIQUIDATION_THRESHOLD` (21,480 blocks) -- `updateQuorumBps`: enforces `<= BPS_DENOMINATOR` (10,000) — but allows 0 (see SEC-1) - -All other setters accept any value, including 0 and extreme values that could break protocol invariants. - -**Affected functions and proposed guardrails:** - -| # | Function | Parameter | Current guard | Proposed guardrail | Risk if unguarded | -|---|---|---|---|---|---| -| 1 | `updateNetworkFee` | `fee` (wei/block) | None | `fee <= TBD_MAX_NETWORK_FEE` | Extreme fee drains all clusters rapidly | -| 2 | `updateNetworkFeeSSV` | `fee` (SSV/block) | None | `fee <= TBD_MAX_NETWORK_FEE_SSV` | Same as above for SSV clusters | -| 3 | `updateOperatorFeeIncreaseLimit` | `percentage` | None | `percentage > 0 && percentage <= TBD_MAX_INCREASE_LIMIT` | `0` blocks all operator fee increases forever; extreme value allows unlimited fee jumps | -| 4 | `updateDeclareOperatorFeePeriod` | `timeInSeconds` | None | `timeInSeconds >= TBD_MIN_DECLARE_PERIOD && timeInSeconds <= TBD_MAX_DECLARE_PERIOD` | `0` allows instant fee declarations (no review window); extreme value blocks fee changes | -| 5 | `updateExecuteOperatorFeePeriod` | `timeInSeconds` | None | `timeInSeconds >= TBD_MIN_EXECUTE_PERIOD && timeInSeconds <= TBD_MAX_EXECUTE_PERIOD` | `0` allows instant fee execution (no user reaction window); extreme value blocks fee changes | -| 6 | `updateLiquidationThresholdPeriod` | `blocks` | `>= 21,480` ✅ | Add max: `blocks <= TBD_MAX_LIQUIDATION_THRESHOLD` | ✅ Min exists. Extreme max could make liquidation economically unviable | -| 7 | `updateLiquidationThresholdPeriodSSV` | `blocks` | `>= 21,480` ✅ | Add max: `blocks <= TBD_MAX_LIQUIDATION_THRESHOLD_SSV` | Same as above for SSV | -| 8 | `updateMinimumLiquidationCollateral` | `amount` (wei) | None | `amount > 0 && amount <= TBD_MAX_MIN_COLLATERAL` | `0` allows clusters with no safety margin; extreme value blocks cluster creation | -| 9 | `updateMinimumLiquidationCollateralSSV` | `amount` (SSV) | None | `amount > 0 && amount <= TBD_MAX_MIN_COLLATERAL_SSV` | Same as above for SSV | -| 10 | `updateMaximumOperatorFee` | `maxFee` (wei) | None | `maxFee > 0 && maxFee >= sp.minimumOperatorEthFee` | `0` blocks all operator registrations; see also SEC-15 for cross-validation | -| 11 | `updateMinimumOperatorEthFee` | `minFee` (wei) | None | `minFee <= sp.operatorMaxFee` | Extreme value blocks operator registrations; see also SEC-15 for cross-validation | -| 12 | `updateQuorumBps` | `quorum` | `<= 10,000` | Add min: `quorum >= TBD_MIN_QUORUM_BPS` | `0` allows single-oracle root commits; see SEC-1 | -| 13 | `updateUnstakeCooldownDuration` | `duration` | None | `duration >= TBD_MIN_COOLDOWN && duration <= TBD_MAX_COOLDOWN` | `0` allows instant unstaking (no cooldown); see SEC-4 | - -**Note:** Items 10-11 overlap with SEC-15, and items 12-13 overlap with SEC-1/SEC-4. Those items can be closed as sub-items of this one, or this item can reference them as "already covered" — team's choice. - -**Acceptance Criteria:** -- [ ] Product/governance team provides concrete min/max values for all `TBD` placeholders -- [ ] Each function in the table above has the agreed guardrail implemented -- [ ] Existing guardrails (liquidation threshold min) are preserved -- [ ] Cross-validation between related parameters (min/max operator fee) is enforced -- [ ] All new guards revert with descriptive custom errors -- [ ] Unit tests cover each boundary: at min, at max, below min (revert), above max (revert) -- [ ] Existing tests updated where they set extreme/zero values that now revert -- [ ] No behavioral change for values within the accepted range - -**Agent Instructions:** -1. Read `contracts/modules/SSVDAO.sol` fully — all setter functions. -2. Read `contracts/libraries/ProtocolLib.sol` — `updateNetworkFee` and `updateNetworkFeeSSV` delegate here. -3. Read `contracts/libraries/storage/SSVStorageProtocol.sol` for the `StorageProtocol` struct fields. -4. Read `contracts/libraries/storage/SSVStorageStaking.sol` for the `StorageStaking` struct fields. -5. **Wait for Product to fill in `TBD` values before implementing.** If values are not yet defined, implement only the non-zero guards (where `0` is clearly harmful) and add `// TODO: add max bound per SEC-17` comments. -6. Define new custom errors in `contracts/interfaces/ISSVNetworkCore.sol` as needed (e.g., `InvalidParameter()`, `ValueOutOfRange()`). -7. For each function, add the guard at the top before any state changes. -8. Update tests in `test/unit/SSVDAO/` for each modified function. -9. Run `npm run test:unit`. - -#### Sub-items: -- [ ] Sub-task 1: Get Product sign-off on min/max bounds for all parameters -- [ ] Sub-task 2: Implement non-zero guards for all unguarded setters -- [ ] Sub-task 3: Implement min/max bounds once Product provides values -- [ ] Sub-task 4: Add unit tests for each boundary (at min, at max, below min, above max) -- [ ] Sub-task 5: Reconcile with SEC-1, SEC-4, SEC-15 (close or cross-reference) -- [ ] Sub-task 6: Run full test suite - ---- - -### [SEC-18] ETH-only operators can call `withdrawOperatorEarningsSSV` (no-op but wastes gas) -- **Type:** Security Hardening -- **Priority:** P3 -- **Status:** Open -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Add an early-exit guard in `withdrawOperatorEarningsSSV` (or its underlying helper) that reverts when called by the owner of an ETH-only operator, preventing a pointless transaction that wastes gas. - -**Context:** -Operators registered after the v2.0.0 migration may be ETH-only (`snapshot.block == 0`, `ethSnapshot.block != 0`). New validator registrations for these operators use the ETH payment path exclusively, so they can never accumulate SSV earnings. Despite this, nothing prevents their owner from calling `withdrawOperatorEarningsSSV`. The call will succeed (the SSV balance is 0, so no tokens move), but the user pays gas for a no-op. Echidna invariants already confirm that the accounting system cannot credit SSV earnings to ETH-only operators, so there is no risk of fund loss — this is purely a UX/gas waste issue. - -**Acceptance Criteria:** -- [ ] `withdrawOperatorEarningsSSV` reverts with a descriptive error (e.g., `NoSSVEarnings()`) when the operator has `snapshot.block == 0` (ETH-only) -- [ ] ETH-capable operators (both `snapshot.block != 0` and `ethSnapshot.block != 0`) are unaffected -- [ ] Confirm via Echidna that SSV balance of ETH-only operators cannot be artificially inflated - -**Agent Instructions:** -1. Read `contracts/modules/SSVOperators.sol`, focus on `withdrawOperatorEarningsSSV` and its internal helper. -2. After the `checkOwner` call, add: `if (operator.snapshot.block == 0) revert NoSSVEarnings();` -3. Define `NoSSVEarnings` error in `contracts/interfaces/ISSVNetworkCore.sol` if not already present. -4. Add a unit test: register an ETH-only operator → call `withdrawOperatorEarningsSSV` → expect revert with `NoSSVEarnings`. -5. Run `npm run test:unit`. - -#### Sub-items: -- [ ] Sub-task 1: Add ETH-only operator guard to `withdrawOperatorEarningsSSV` -- [ ] Sub-task 2: Define `NoSSVEarnings` custom error -- [ ] Sub-task 3: Add unit test for ETH-only operator calling SSV withdrawal -- [ ] Sub-task 4: Run full test suite - ---- - -### [SEC-19] `minBlocksBetweenUpdates` never initialized — EB update rate limit silently disabled -- **Type:** Security Hardening -- **Priority:** P1 -- **Status:** Open -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Initialize `minBlocksBetweenUpdates` to a non-zero value during the upgrade, and add a governance setter so it can be adjusted post-deployment. - -**Context:** -`StorageEB.minBlocksBetweenUpdates` is a `uint32` in diamond storage. It is read by `_verifyEBUpdateFrequency` to rate-limit how often a cluster's EB can be updated: - -```solidity -if (ebSnapshot.lastUpdateBlock != 0 && block.number < ebSnapshot.lastUpdateBlock + seb.minBlocksBetweenUpdates) { - revert UpdateTooFrequent(); -} -``` - -Because the field is never set — neither in the upgrade initializer nor via any governance function — it defaults to `0`. The condition `block.number < lastUpdateBlock + 0` is always `false`, so the rate limit is **completely inoperative**. Any caller can submit a valid `updateClusterBalance` proof every block for every cluster. - -The threat model (`docs/audit/07-trust-boundaries-integrations.md`) explicitly lists this rate limit as a mitigation against forced EB update spam and auto-liquidation attacks. With it disabled, an attacker holding a valid oracle proof of a cluster's reduced EB can trigger auto-liquidation in the same block as a root commitment, with no cooldown. - -**Acceptance Criteria:** -- [ ] `minBlocksBetweenUpdates` initialized to a non-zero value in the upgrade reinitializer (suggested: `7200` blocks ≈ 1 day, matching oracle sweep frequency) -- [ ] Governance setter added (e.g. `setMinBlocksBetweenUpdates(uint32)`, owner-only) -- [ ] Setter emits an event (e.g. `MinBlocksBetweenUpdatesUpdated(uint32)`) -- [ ] Unit test: second `updateClusterBalance` within the cooldown window reverts with `UpdateTooFrequent` -- [ ] Unit test: `updateClusterBalance` succeeds after cooldown window passes -- [ ] Governance parameter documented in SPEC.md §11 and FLOWS.md - -**Agent Instructions:** -1. In the upgrade reinitializer, add: `SSVStorageEB.load().minBlocksBetweenUpdates = 7200;` -2. Add a governance setter in `SSVDAO.sol` (or equivalent): `function setMinBlocksBetweenUpdates(uint32 blocks) external onlyOwner`. -3. Emit `MinBlocksBetweenUpdatesUpdated(blocks)` from the setter. -4. Add the event to `ISSVNetworkCore.sol` or the DAO interface. -5. Add unit tests covering both the cooldown revert and the post-cooldown success path. -6. Update SPEC.md §11 governance parameters table and FLOWS.md §3.3 preconditions. -7. Run `npm run test:unit`. - -#### Sub-items: -- [ ] Sub-task 1: Initialize `minBlocksBetweenUpdates` in upgrade reinitializer -- [ ] Sub-task 2: Add governance setter and event -- [ ] Sub-task 3: Unit tests for rate-limit enforcement -- [ ] Sub-task 4: Update SPEC.md and FLOWS.md - ---- - -### [SEC-20] ~~Oracle Quorum Can Be Set to Zero~~ -- **Type:** Security Hardening -- **Priority:** P2 -- **Status:** ✅ Fixed -- **Owner:** (resolved) -- **Timeline:** 2026-03-16 -- **Github Link:** (empty) - -**Resolution:** -`updateQuorumBps` now rejects zero quorum: `if (quorum == 0 || quorum > BPS_DENOMINATOR) revert InvalidQuorum()`. This prevents the owner from accidentally disabling the multi-oracle quorum threshold. Updated unit tests to expect revert on `updateQuorumBps(0)` and added a test for the minimum valid quorum of 1 bps. - -**Acceptance Criteria:** -- [x] `updateQuorumBps(0)` reverts with `InvalidQuorum()` -- [x] `updateQuorumBps(1)` succeeds (minimum valid quorum) -- [x] Existing tests for `updateQuorumBps` updated to reflect new validation ---- - -## Unit Test Completeness - -### [TEST-1] Validator register/remove with non-zero operator fees -- **Type:** Unit Test Completeness -- **Priority:** P0 -- **Status:** Open -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Add unit tests for validator registration and removal with operators that have non-zero ETH fees. Currently ALL SSVValidator tests use operators with `fee=0` (the default), leaving the entire fee settlement mechanism untested. - -**Context:** -This is the #1 systemic test gap. The fee settlement mechanism (`updateClusterOperators` / `settleClusterBalance`) during register/remove has zero real coverage with actual fee deductions. If fee settlement is wrong, clusters are overcharged or undercharged on every register/remove. The EB-weighted fee model (`vUnits`) makes this even more critical. - -**Acceptance Criteria:** -- [ ] Test: Register validator with 4 operators each charging different ETH fees → verify cluster balance deduction = `blocksDelta * sum(operatorFees) * vUnits / BPS_DENOMINATOR * ETH_DEDUCTED_DIGITS` -- [ ] Test: Register second validator after N blocks → verify fees from first validator settled correctly before adding second -- [ ] Test: Remove validator with non-zero fees → verify operator earnings accumulated match expected -- [ ] Test: Bulk register 10 validators with non-zero fees → verify total deduction -- [ ] All new tests pass - -**Agent Instructions:** -1. Read `test/unit/SSVValidator/registerValidator.test.ts` to understand existing patterns and test helpers. -2. Read `test/helpers/contract-helpers.ts` to understand how operators are registered and fees are set. Look for `registerOperator` helper and how `declareOperatorFee` / `executeOperatorFee` work. -3. Read `test/common/constants.ts` for fee-related constants. -4. Create a new test file or add a describe block to existing files. Use the existing `CONFIG` fixture pattern. -5. For each test: - - Register operators with non-zero ETH fees (use `declareOperatorFee` → advance blocks → `executeOperatorFee`) - - Register validators - - Advance blocks with `mine(N)` - - Perform the operation (register/remove) - - Calculate expected fees independently: `blocksDelta * sum(PackedETH.unwrap(fee)) * vUnits / BPS_DENOMINATOR * ETH_DEDUCTED_DIGITS` - - Assert cluster balance = initial deposit - expected fees - - Assert operator earnings match expected accumulation -6. Use `ethers.provider.getBalance` for ETH balance checks and the SSVViews contract for cluster/operator balance queries. -7. Run `npm run test:unit`. - -#### Sub-items: -- [ ] Sub-task 1: Register validator with non-zero operator fees — verify cluster balance deduction -- [ ] Sub-task 2: Sequential validator registration with fee settlement verification -- [ ] Sub-task 3: Remove validator with non-zero fees — verify operator earnings -- [ ] Sub-task 4: Bulk register with non-zero fees — verify total deduction - ---- - -### [TEST-2] ~~EB-weighted operator earnings accumulation~~ -- **Type:** Unit Test Completeness -- **Priority:** P0 -- **Status:** ✅ Closed -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Add unit tests verifying that operators earn proportionally more when serving clusters with higher effective balance. The EB settlement tests check fee deductions from the cluster side but don't verify operator earnings. - -**Context:** -The vUnit model is the core economic change in v2.0.0. If operator earnings don't scale with EB, the entire incentive model is broken. No unit test currently verifies the operator earnings side of EB-weighted accounting. - -**Acceptance Criteria:** -- [ ] Test: Operator serves two clusters, EB=32 and EB=64 → after N blocks, verify operator earnings = `(blocks * fee * 10000 + blocks * fee * 20000) / BPS_DENOMINATOR * ETH_DEDUCTED_DIGITS` -- [ ] Test: Operator fee change after EB update → verify earnings split correctly at boundary -- [ ] Test: `withdrawOperatorEarnings` after EB-weighted accrual → verify exact ETH withdrawn matches expected - -**Agent Instructions:** -1. Read `test/unit/SSVClusters/ebSettlement.test.ts` to understand EB test patterns. -2. Read `test/unit/SSVOperators/withdrawOperatorEarnings.test.ts` for withdrawal test patterns. -3. Read `contracts/libraries/OperatorLib.sol`, focus on `updateSnapshot` to understand how operator earnings accumulate with vUnits. -4. Create tests that: - - Register an operator - - Create two clusters with different EBs (use `updateClusterBalance` with Merkle proofs to set EB) - - Advance blocks - - Verify operator earnings via `SSVViews.getOperatorEarnings(operatorId)` - - Withdraw and verify exact ETH amount -5. Use the Merkle proof helpers in `test/helpers/` to create valid proofs for EB updates. -6. Run `npm run test:unit`. - -#### Sub-items: -- [ ] Sub-task 1: Operator earning from two clusters with different EBs -- [ ] Sub-task 2: Operator fee change boundary with EB-weighted clusters -- [ ] Sub-task 3: Withdraw operator earnings after EB-weighted accrual - ---- - -### [TEST-3] Balance delta assertions in liquidation paths -- **Type:** Unit Test Completeness -- **Priority:** P0 -- **Status:** Open -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Add balance delta assertions to liquidation tests. Current tests check events and state transitions but do not assert actual ETH/SSV token transfer amounts. - -**Context:** -A liquidation could emit the correct event but transfer the wrong amount (or nothing). Without balance delta assertions, incorrect transfer logic is invisible to the test suite. - -**Acceptance Criteria:** -- [ ] Test: Liquidate ETH cluster → assert `liquidator.balance.after - liquidator.balance.before == cluster.remainingBalance` (accounting for gas) -- [ ] Test: Liquidate SSV cluster → assert `SSVToken.balanceOf(liquidator).after - before == cluster.remainingSSVBalance` -- [ ] Test: Liquidate cluster with 0 remaining balance → assert no ETH transferred -- [ ] Test: Self-liquidation → assert owner receives remaining balance - -**Agent Instructions:** -1. Read `test/unit/SSVClusters/liquidate.test.ts` and `test/unit/SSVClusters/liquidateSSV.test.ts`. -2. Add balance capture before/after each liquidation call: - ```typescript - const balanceBefore = await ethers.provider.getBalance(liquidator.address); - const tx = await ssvNetwork.connect(liquidator).liquidate(...); - const receipt = await tx.wait(); - const gasCost = receipt.gasUsed * receipt.gasPrice; - const balanceAfter = await ethers.provider.getBalance(liquidator.address); - expect(balanceAfter - balanceBefore + gasCost).to.equal(expectedReward); - ``` -3. For SSV token liquidations, use `SSVToken.balanceOf()` instead of native balance. -4. Calculate expected remaining balance independently using the cluster balance formula. -5. Run `npm run test:unit`. - -#### Sub-items: -- [ ] Sub-task 1: ETH liquidation balance delta assertions -- [ ] Sub-task 2: SSV liquidation balance delta assertions -- [ ] Sub-task 3: Zero-balance liquidation -- [ ] Sub-task 4: Self-liquidation balance check - ---- - -### [TEST-4] ~~`updateClusterBalance` on liquidated clusters~~ -- **Type:** Unit Test Completeness -- **Priority:** P0 -- **Status:** ✅ **CLOSED** -- **Owner:** PR #447 + enhancements -- **Timeline:** Completed 2026-02-25 -- **Github Link:** [test/unit/SSVClusters/updateClusterBalance.test.ts](../test/unit/SSVClusters/updateClusterBalance.test.ts) (lines 293-653), [test/integration/SSVNetwork/clusters.test.ts](../test/integration/SSVNetwork/clusters.test.ts) (lines 753-817) - -**Requirement:** -Add tests for calling `updateClusterBalance` (EB oracle update) on an already-liquidated cluster. - -**Context:** -No test exists for this path. If the contract doesn't handle it, oracle updates on liquidated clusters could corrupt accounting or revert unexpectedly. - -**Acceptance Criteria:** -- [x] Test: Call `updateClusterBalance` with valid proof on a liquidated cluster → verify defined behavior (revert or update EB without settling fees) -- [x] Test: EB update that makes a liquidated cluster even more insolvent → verify no state corruption -- [x] **BONUS**: Multi-validator liquidated cluster EB update -- [x] **BONUS**: EB decrease on liquidated cluster (penalty scenario) -- [x] **BONUS**: Liquidated cluster with implicit EB → first EB update transitions to explicit tracking - -**Implementation Summary:** -1. **Unit tests** ([updateClusterBalance.test.ts](../test/unit/SSVClusters/updateClusterBalance.test.ts)): - - Line 293-337: Basic liquidated cluster EB update — verifies EB snapshot updated, cluster stays inactive, no fee settlement - - Line 339-416: EB increase on insolvent liquidated cluster — verifies no operator/DAO vUnit corruption - - Line 463-527: **NEW** Multi-validator liquidated cluster EB update - - Line 529-602: **NEW** EB decrease on liquidated cluster (penalty scenario) - - Line 604-653: **NEW** Implicit→explicit EB transition on liquidated cluster - -2. **Integration test** ([clusters.test.ts](../test/integration/SSVNetwork/clusters.test.ts)): - - Line 753-817: E2E flow with oracle quorum setup and multiple EB updates on liquidated cluster - -3. **Additional improvements**: - - Fixed loose comparators in integration tests — now uses exact formula-based assertions per SSV standards - - Added block number tracking for precise fee calculations - - All tests passing with 100% exact `.to.equal()` assertions - -#### Sub-items: -- [x] Sub-task 1: `updateClusterBalance` on liquidated cluster — basic behavior -- [x] Sub-task 2: EB increase on already-insolvent liquidated cluster -- [x] Sub-task 3: Multi-validator liquidated cluster EB update -- [x] Sub-task 4: EB decrease on liquidated cluster -- [x] Sub-task 5: Implicit→explicit EB transition - ---- - -### [TEST-5] Oracle quorum edge cases -- **Type:** Unit Test Completeness -- **Priority:** P0 -- **Status:** Open -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Add comprehensive edge case tests for the oracle quorum mechanism in `commitRoot`. - -**Context:** -Only basic quorum tests exist. Missing: boundary conditions, weight manipulation, oracle replacement during voting, quorum parameter changes mid-vote. - -**Acceptance Criteria:** -- [ ] Test: Quorum at exactly 100% — all 4 oracles must vote -- [ ] Test: Quorum at 1 bps — single oracle vote commits -- [ ] Test: Oracle replaced between proposing and committing — verify vote behavior -- [ ] Test: Quorum changed between votes — verify consistent threshold -- [ ] Test: Oracles propose different roots for same block number — verify correct root wins - -**Agent Instructions:** -1. Read `test/unit/SSVDAO/commitRoot.test.ts` for existing patterns. -2. Read `contracts/modules/SSVDAO.sol`, focus on `commitRoot` (line 155) for the voting/quorum logic. -3. Add tests for each scenario. For oracle replacement mid-vote, call `replaceOracle` between two `commitRoot` calls for the same block number. -4. Use `updateQuorumBps` to set boundary values before testing. -5. Run `npm run test:unit`. - -#### Sub-items: -- [ ] Sub-task 1: 100% quorum boundary test -- [ ] Sub-task 2: Minimal quorum (1 bps) test -- [ ] Sub-task 3: Oracle replacement mid-vote -- [ ] Sub-task 4: Quorum change mid-vote -- [ ] Sub-task 5: Conflicting root proposals - ---- - -### [TEST-6] EB decrease scenarios -- **Type:** Unit Test Completeness -- **Priority:** P0 -- **Status:** Open -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Add unit tests for effective balance decreases. All current EB tests only cover increases (32→higher). Validators can have EB decrease due to penalties. - -**Context:** -If EB decreases aren't handled correctly, vUnits could be wrong, operators could be overpaid, or liquidation thresholds could be miscalculated. EB decrease is a completely untested code path. - -**Acceptance Criteria:** -- [ ] Test: EB decrease from 64 ETH to 32 ETH → verify vUnits decrease, operator fees decrease, liquidation threshold recalculated -- [ ] Test: EB decrease below 32 ETH → should revert with `EBBelowMinimum` -- [ ] Test: EB decrease while cluster is near liquidation threshold → verify decrease triggers liquidation if below threshold -- [ ] Test: Operator deviation negative after EB decrease → verify `daoTotalEthVUnits` updated correctly - -**Agent Instructions:** -1. Read `test/unit/SSVClusters/ebSettlement.test.ts` and `test/unit/SSVClusters/updateClusterBalance.test.ts`. -2. Create test scenarios where EB starts high and is updated to a lower value via `updateClusterBalance` with a Merkle proof for the lower EB. -3. Use the Merkle tree helpers to generate proofs for decreased EB values. -4. Verify vUnits, deviation, burn rate, and liquidation threshold after decrease. -5. For the below-32-ETH case, verify the contract reverts with the correct error. -6. Run `npm run test:unit`. - -#### Sub-items: -- [ ] Sub-task 1: EB decrease from 64→32 ETH — vUnits and fee verification -- [ ] Sub-task 2: EB below minimum (< 32 ETH) — revert test -- [ ] Sub-task 3: EB decrease triggering liquidation -- [ ] Sub-task 4: Negative deviation after EB decrease - ---- - -### [TEST-7] Reentrancy in staking functions -- **Type:** Unit Test Completeness -- **Priority:** P0 -- **Status:** ✅ Complete -- **Owner:** Claude -- **Timeline:** 2026-02-26 -- **Github Link:** PR #452 - -**Requirement:** -Add reentrancy tests for SSVStaking functions that transfer ETH or tokens. These functions are marked `nonReentrant` but no test verifies the protection works. - -**Context:** -`claimEthRewards`, `withdrawUnlocked`, `stake`, `requestUnstake` all handle ETH or SSV token transfers. Reentrancy via a `receive()` hook could theoretically drain rewards. The `nonReentrant` modifier should prevent this, but it's untested. The existing SSVOperators reentrancy test (`test/unit/SSVOperators/reentrancy.test.ts`) can serve as a pattern. - -**Acceptance Criteria:** -- [x] Test: Attacker contract with `receive()` hook calls `claimEthRewards` reentrantly → verify reverts -- [x] ~~Test: Attacker calls `withdrawUnlocked` reentrantly during SSV token transfer~~ → **NOT NEEDED** (see resolution) -- [x] All reentrancy tests use a custom attacker contract deployed in the test - -**Resolution:** -✅ **`claimEthRewards` reentrancy test implemented:** -- Unit test: `test/unit/SSVStaking/reentrancy.test.ts` -- Integration test: `test/integration/SSVNetwork.test.ts` (line 3414-3447) -- Attacker contract: `contracts/test/mocks/MaliciousClaimEthRewards.sol` -- **This is a valid attack vector** because `claimEthRewards()` sends ETH which triggers `receive()` hooks - -❌ **`withdrawUnlocked`, `stake`, `requestUnstake` reentrancy tests NOT needed:** -- **Reason:** SSVToken (`contracts/token/SSVToken.sol`) is a standard ERC20 with **no callbacks** -- Standard ERC20 `transfer()` and `transferFrom()` do **not** call back to the recipient -- **No `receive()` hook is triggered** during token transfers -- **Reentrancy is impossible** during these operations in production -- The `nonReentrant` modifiers on these functions are **defensive programming** but protect against **no real attack vector** -- A reentrancy test would require a malicious token contract, which doesn't match the production SSVToken implementation - -**Conclusion:** -Only `claimEthRewards()` has a real reentrancy attack surface (ETH transfers trigger `receive()` hooks). The function is properly protected and tested. Other staking functions interact only with standard ERC20 tokens (SSV, cSSV) which have no callback mechanisms. - -#### Sub-items: -- [x] Sub-task 1: `claimEthRewards` reentrancy test ✅ -- [x] Sub-task 2: `withdrawUnlocked` reentrancy test → **Not needed** (no attack vector) - ---- - -### [TEST-8] Forbid creating clusters with removed operators -- **Type:** Unit Test Completeness -- **Priority:** P0 -- **Status:** Open -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Add explicit tests for PR #410 (forbid creating clusters with removed operators). Verify both `registerValidator` and `bulkRegisterValidator` revert when given a removed operator ID. - -**Context:** -PR #410 added a fix but no explicit test exists for this scenario. Creating clusters with removed operators would result in stuck funds with no one to service the validator. - -**Acceptance Criteria:** -- [ ] Test: Register validator using operatorIds where one operator was previously removed → should revert -- [ ] Test: Bulk register where one of the operator IDs belongs to a removed operator → should revert - -**Agent Instructions:** -1. Read `test/unit/SSVValidator/registerValidator.test.ts` and `test/unit/SSVValidator/bulkRegisterValidator.test.ts`. -2. Add a test that: registers 4 operators, removes one, then tries to register a validator with all 4 operator IDs → expect revert. -3. Add the same for bulk registration. -4. Identify the specific error that the contract reverts with (likely `OperatorDoesNotExist` — check `contracts/libraries/OperatorLib.sol`). -5. Run `npm run test:unit`. - -#### Sub-items: -- [ ] Sub-task 1: `registerValidator` with removed operator → revert test -- [ ] Sub-task 2: `bulkRegisterValidator` with removed operator → revert test - ---- - -### [TEST-9] ~~Migration balance accounting verification~~ -- **Type:** Unit Test Completeness -- **Priority:** P1 -- **Status:** ✅ Done -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Add tests that verify exact SSV refund amounts and ETH deposit amounts during migration, calculated independently from contract logic. - -**Context:** -Migration tests verify events and state but don't verify exact token transfer amounts against independently calculated values. - -**Acceptance Criteria:** -- [x] Test: Migrate after 1000 blocks → verify SSV refund = `initial_deposit - (blocks * sum(ssv_fees) * validatorCount) * DEDUCTED_DIGITS` -- [x] Test: Migrate with partial SSV balance remaining → verify exact token transfer amount -- [x] Test: Migrate cluster where operators have both SSV and ETH fees set → verify ETH side correctly initialized - -**Agent Instructions:** -1. Read `test/unit/SSVClusters/migrateClusterToETH.test.ts` for existing patterns. -2. Add independent balance calculations using JavaScript BigInt arithmetic matching the contract's formula. -3. Assert `SSVToken.balanceOf(owner).after - SSVToken.balanceOf(owner).before == expectedRefund`. -4. Run `npm run test:unit`. - -#### Sub-items: -- [x] Sub-task 1: Exact SSV refund after N blocks -- [x] Sub-task 2: Migration with partial balance -- [x] Sub-task 3: Migration with dual SSV/ETH fees - ---- - -### [TEST-10] ~~Operator fee change + EB burn rate interaction~~ -- **Type:** Unit Test Completeness -- **Priority:** P1 -- **Status:** ✅ Done -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Add tests combining operator fee changes (declare/execute/reduce) with EB-weighted clusters. - -**Context:** -No tests combine operator fee changes with EB-weighted clusters. The burn rate depends on both operator fee and vUnits, and fee changes must properly settle the old rate before applying the new one. - -**Acceptance Criteria:** -- [x] Test: Operator increases fee while serving EB=64 cluster → verify burn rate doubles -- [x] Test: Operator reduces fee with EB-weighted cluster → verify savings reflected -- [x] Test: Fee execution changes mid-block for EB-weighted cluster → verify boundary accounting - -**Agent Instructions:** -1. Read `test/unit/SSVOperators/declareOperatorFee.test.ts` and `test/unit/SSVOperators/executeOperatorFee.test.ts`. -2. Read `test/unit/SSVClusters/ebSettlement.test.ts`. -3. Create combined tests: register operator with fee, create cluster with EB, change fee, verify cluster balance reflects correct burn rate split. -4. Run `npm run test:unit`. - -#### Sub-items: -- [x] Sub-task 1: Fee increase with EB-weighted cluster -- [x] Sub-task 2: Fee reduction with EB-weighted cluster -- [x] Sub-task 3: Fee change boundary accounting - ---- - -### [TEST-11] Network fee update impact on active clusters -- **Type:** Unit Test Completeness -- **Priority:** P1 -- **Status:** ✅ Done -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Add tests verifying that `updateNetworkFee` changes the actual burn rate for existing active clusters. - -**Context:** -DAO parameter tests verify storage changes but not enforcement on active clusters. - -**Acceptance Criteria:** -- [x] Test: Increase ETH network fee with active ETH cluster → verify cluster burns faster -- [x] Test: Decrease ETH network fee → verify cluster burn rate decreases -- [x] Test: Update network fee with EB-weighted cluster → verify vUnit scaling applied - -**Agent Instructions:** -1. Read `test/unit/SSVDAO/updateNetworkFee.test.ts`. -2. Create cluster, advance blocks, check balance, then update network fee, advance more blocks, check balance again. -3. Verify the balance difference in each period matches the respective fee rates. -4. Run `npm run test:unit`. - -#### Sub-items: -- [x] Sub-task 1: Network fee increase enforcement -- [x] Sub-task 2: Network fee decrease enforcement -- [x] Sub-task 3: Network fee with EB scaling - ---- - -### [TEST-12] Multi-staker reward fairness -- **Type:** Unit Test Completeness -- **Priority:** P1 -- **Status:** ✅ Done -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Add comprehensive multi-staker scenarios testing proportional reward distribution and cSSV transfer settlement. - -**Context:** -`onCSSVTransfer` has only 2 tests. Staking integration tests have basic proportional distribution but don't test complex scenarios with multiple stakers entering/exiting at different times or transferring cSSV. - -**Acceptance Criteria:** -- [x] Test: 3 stakers with different amounts → each receives exactly proportional rewards -- [x] Test: Staker A stakes, rewards accrue, staker B stakes → A gets both periods, B gets only second -- [x] Test: cSSV transfer from A to B → verify reward settlement for both, B earns at higher rate -- [x] Test: Sequential cSSV transfers A→B→C → verify accumulated rewards at each step - -**Agent Instructions:** -1. Read `test/unit/SSVStaking/claimEthRewards.test.ts` and `test/unit/SSVStaking/onCSSVTransfer.test.ts`. -2. Read `test/integration/SSVNetwork/staking.test.ts` for integration patterns. -3. Use the `accEthPerShare` formula: `pendingReward = cSSVBalance * (accEthPerShare - userIndex) / 1e18`. -4. Calculate expected rewards independently and assert exact values (accounting for precision loss). -5. Run `npm run test:unit`. - -#### Sub-items: -- [x] Sub-task 1: Three-staker proportional distribution -- [x] Sub-task 2: Time-weighted staking (A early, B late) -- [x] Sub-task 3: cSSV transfer settlement -- [x] Sub-task 4: Sequential cSSV transfer chain - ---- - -### [TEST-13] Liquidation + reactivation multi-cycle accounting -- **Type:** Unit Test Completeness -- **Priority:** P1 -- **Status:** ✅ Done -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Add tests for multiple liquidation/reactivation cycles to verify no accounting drift accumulates. - -**Context:** -Only single liquidation/reactivation cycles are tested. Over multiple cycles, rounding errors or state leakage could accumulate. - -**Acceptance Criteria:** -- [x] Test: Liquidate → reactivate → operate → liquidate → reactivate → verify cumulative balances, no drift -- [x] Test: Operator earnings across multiple liquidation cycles → verify no double-counting - -**Agent Instructions:** -1. Read `test/unit/SSVClusters/liquidate.test.ts` and `test/unit/SSVClusters/reactivate.test.ts`. -2. Create a test that performs 3+ full cycles: deposit → advance blocks → liquidate → reactivate with deposit → repeat. -3. Track operator earnings and cluster balance at each step, verify consistency. -4. Run `npm run test:unit`. - -#### Sub-items: -- [x] Sub-task 1: Multi-cycle liquidation/reactivation accounting -- [x] Sub-task 2: Operator earnings across cycles - ---- - -### [TEST-14] Reactivation with EB deviation solvency check -- **Type:** Unit Test Completeness -- **Priority:** P1 -- **Status:** ✅ Done -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Test that reactivation solvency checks account for EB-weighted burn rate. - -**Context:** -Reactivate tests don't verify that the minimum deposit scales with vUnits. A cluster with EB=2048 has 64x the burn rate and should require a proportionally higher deposit. - -**Acceptance Criteria:** -- [x] Test: Reactivate cluster with EB=64 → verify minimum deposit requirement scales with 2x vUnits -- [x] Test: Reactivate with EB=2048 → verify high deposit requirement enforced - -**Agent Instructions:** -1. Read `test/unit/SSVClusters/reactivate.test.ts`. -2. Create clusters with different EBs, liquidate them, then try to reactivate with minimal deposits. -3. Verify that insufficient deposits for high-EB clusters revert. -4. Run `npm run test:unit`. - -#### Sub-items: -- [x] Sub-task 1: Reactivation solvency with EB=64 -- [x] Sub-task 2: Reactivation solvency with EB=2048 - ---- - -### [TEST-15] ~~SSV cluster operations completeness~~ -- **Type:** Unit Test Completeness -- **Priority:** P1 -- **Status:** ✅ Closed -- **Owner:** (resolved) -- **Timeline:** 2026-03-12 -- **Github Link:** (empty) - -**Requirement:** -Add comprehensive tests for SSV-denominated cluster operations. Most tests focus on ETH clusters, leaving SSV cluster paths undertested. - -**Context:** -The dual cluster system maintains parallel SSV and ETH records. SSV cluster operations should still work correctly during the transition period. - -**Resolution:** -Closed with focused legacy SSV accounting coverage across allowed SSV-cluster paths: -- `test/unit/SSVValidator/removeValidator.test.ts` already covers removal from active legacy SSV clusters, including a non-zero-fee balance-deduction check. -- `test/unit/SSVClusters/legacySSVAccounting.test.ts` adds exact settlement checks for: - - `removeValidator` with accrued legacy SSV operator fees - - `removeValidator` with a pending ETH fee change request — proves SSV settlement is isolated from ETH fee state - - `bulkRemoveValidator` with non-zero legacy SSV network fee -- Full verification run: `just test-unit` → `662 passing`. - -The previous "SSV cluster withdrawal" acceptance item was stale relative to the current code/spec. Direct `withdraw()` on an SSV cluster is intentionally blocked and is already covered by `test/unit/SSVClusters/withdraw.test.ts` expecting `IncorrectClusterVersion`. - -**Acceptance Criteria:** -- [x] Test: Register/remove validators in SSV cluster with non-zero SSV fees → verify fee deductions -- [x] Test: SSV cluster with non-zero network fee → verify fee deductions -- [x] Direct SSV cluster `withdraw()` is confirmed spec-blocked and covered as `IncorrectClusterVersion`; no positive-path withdraw test is required - -**Agent Instructions:** -1. Read existing SSV-related tests: `test/unit/SSVClusters/liquidateSSV.test.ts`, `test/integration/SSVNetwork/legacy-ssv.test.ts`. -2. Create tests that operate entirely in the SSV version (VERSION_SSV = 0). -3. Set non-zero SSV fees on operators before creating clusters. -4. Verify SSV token balance changes match expected fee deductions. -5. Run `npm run test:unit`. - -#### Sub-items: -- [x] Sub-task 1: Legacy SSV validator removal path with fees -- [x] Sub-task 2: SSV cluster network fee deductions -- [x] Sub-task 3: Confirm direct SSV cluster withdrawal is intentionally blocked by spec/code - ---- - -### [TEST-16] View function coverage (SSVViews) -- **Type:** Unit Test Completeness -- **Priority:** P1 -- **Status:** ✅ Fixed -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Add dedicated unit tests for SSVViews functions. Currently view functions are tested only indirectly. - -**Context:** -No dedicated unit test file exists for SSVViews. Functions like `getBalance`, `isLiquidatable`, `getBurnRate`, `getOperatorEarnings` are used as helpers in other tests but their correctness is never directly asserted. - -**Acceptance Criteria:** -- [x] Test: `getBalance` / `getEffectiveBalance` return correct values for active ETH clusters -- [x] Test: liquidated cluster view behavior is validated (`isLiquidated` true; `getBalance` / `getEffectiveBalance` revert) -- [x] Test: `isLiquidatable` at exact boundary returns correct boolean -- [x] Test: `getBurnRate` with EB-weighted cluster scales with vUnits -- [x] Test: `getOperatorEarnings` dual-version behavior is validated in ETH-only state (`ETH > 0`, `SSV == 0`) -- [x] Test: ETH-only (migration-equivalent) views return expected split (`SSV` views return 0, `ETH` views return correct values) - -**Agent Instructions:** -1. Read `contracts/modules/SSVViews.sol` to understand all view functions. -2. Create `test/unit/SSVViews/views.test.ts` (or similar) following existing test patterns. -3. Set up various cluster states (active, liquidated, migrated) and verify view function return values. -4. Run `npm run test:unit`. - -#### Sub-items: -- [x] Sub-task 1: `getBalance` basic and edge cases -- [x] Sub-task 2: `isLiquidatable` boundary tests -- [x] Sub-task 3: `getBurnRate` with EB -- [x] Sub-task 4: `getOperatorEarnings` dual-version -- [x] Sub-task 5: View functions after migration - ---- - -### [TEST-17] Staking rewards from EB-weighted cluster fees -- **Type:** Unit Test Completeness -- **Priority:** P1 -- **Status:** Closed -- **Owner:** (unassigned) -- **Timeline:** 2026-03-02 -- **Github Link:** (empty) - -**Requirement:** -Test that EB-weighted clusters produce proportionally more staking rewards via the network fee. - -**Context:** -Staking integration tests use basic network fees but don't verify that higher-EB clusters contribute proportionally more to the staking pool. - -**Acceptance Criteria:** -- [x] Test: Cluster with EB=64 generates 2x network fees vs EB=32 → verify staking pool receives 2x rewards -- [x] Test: Multiple clusters with different EBs → verify cumulative staking rewards match sum of EB-weighted network fees - -**Agent Instructions:** -1. Read `test/integration/SSVNetwork/staking.test.ts`. -2. Create two clusters with different EBs, advance blocks, sync fees, verify `accEthPerShare` increment matches EB-weighted expectation. -3. Run `npm run test:unit`. - -#### Sub-items: -- [x] Sub-task 1: EB=64 vs EB=32 staking reward comparison -- [x] Sub-task 2: Multi-cluster cumulative staking rewards - ---- - -### [TEST-18] `withdrawNetworkETHEarnings` (DAO ETH withdrawal) -- **Type:** Unit Test Completeness -- **Priority:** P1 -- **Status:** Open -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Add unit tests for DAO ETH earnings withdrawal. Only SSV DAO withdrawal (`withdrawNetworkSSVEarnings`) is currently tested. - -**Context:** -There is no test for `withdrawNetworkETHEarnings`. The function should exist for withdrawing accumulated ETH network fees. - -**Acceptance Criteria:** -- [ ] Test: Withdraw ETH network earnings → verify balance, event, access control -- [ ] Test: Withdraw more than available → verify revert -- [ ] Test: Withdraw after multiple clusters accrue fees → verify cumulative amount - -**Agent Instructions:** -1. Read `test/unit/SSVDAO/withdrawNetworkSSVEarnings.test.ts` for the SSV withdrawal pattern. -2. Search for `withdrawNetworkETHEarnings` or similar function in `contracts/modules/SSVDAO.sol`. -3. Create equivalent tests for the ETH version. -4. Run `npm run test:unit`. - -#### Sub-items: -- [ ] Sub-task 1: Basic ETH withdrawal test -- [ ] Sub-task 2: Over-withdrawal revert test -- [ ] Sub-task 3: Cumulative multi-cluster accrual test - ---- - -### [TEST-19] Operator removal impact on active ETH clusters -- **Type:** Unit Test Completeness -- **Priority:** P1 -- **Status:** ✅ Complete -- **Owner:** (unassigned) -- **Timeline:** 2026-02-26 -- **Github Link:** (empty) - -**Requirement:** -Test the impact of operator removal on active ETH clusters' fee calculations. - -**Context:** -`removeOperator` tests don't test the downstream effect on active ETH clusters' fee calculations. - -**Acceptance Criteria:** -- [x] Test: Remove operator from set of 4 while cluster has active validators → verify fee calculation excludes removed operator -- [x] Test: Verify removed operator stops earning from both ETH and SSV clusters - -**Resolution:** -- Added `/Users/venimir/Desktop/ssv/contracts-latest/ssv-network/test/unit/SSVClusters/removedOperatorImpact.test.ts` with coverage for: - - ETH cluster settlement after removed-operator simulation (fee deduction excludes removed operator; removed operator ETH earnings frozen) - - SSV cluster settlement via `liquidateSSV` (removed operator SSV earnings frozen while active operators continue earning) -- Aligned `/Users/venimir/Desktop/ssv/contracts-latest/ssv-network/contracts/test/harness/SSVClustersHarness.sol` `mockRemoveOperator()` with real `removeOperator` reset semantics (preserve snapshot indices, clear blocks/balances/fees/counts) so downstream accounting tests model production behavior. -- Verified with `npx hardhat test test/unit/SSVClusters/removedOperatorImpact.test.ts` and `npm run test:unit` (`405 passing`). - -**Agent Instructions:** -1. Read `test/unit/SSVOperators/removeOperator.test.ts`. -2. Read `test/sanity/removed-operator.test.ts` for the existing removed operator scenario. -3. Create a cluster with 4 operators, remove one, advance blocks, verify cluster balance only decreases by 3 operators' fees. -4. Verify the removed operator's earnings are frozen (no new earnings after removal). -5. Run `npm run test:unit`. - -#### Sub-items: -- [x] Sub-task 1: Fee calculation after operator removal -- [x] Sub-task 2: Removed operator earnings freeze - ---- - -### [TEST-19a] Operator removal impact on active ETH clusters -1. Multiple Removed Operators -// Missing test: -it("handles multiple removed operators (2 of 4) correctly", async () => { - // Remove operators[1] and operators[3] - // Verify only operators[0] and operators[2] accrue earnings - // Verify cluster balance reflects 2 operators only -}); -2. EB-Weighted Cluster with Removed Operator -// Missing test: -it("excludes removed operator vUnits from EB-weighted fee calculation", async () => { - // Set cluster EB to 64 ETH (2x vUnits) - // Remove one operator - // Verify active operators earn fees scaled by 2x vUnits - // Verify removed operator's vUnits are excluded -}); -3. Reactivation with Removed Operator -// Missing test: -it("reactivation excludes removed operator from fee calculation", async () => { - // Create cluster with 4 operators - // Remove operator[2] - // Liquidate cluster - // Reactivate cluster (FLOWS.md notes this skips removed operators) - // Verify reactivation fee calculation uses 3 operators only -}); -4. Operator Removal During Validator Lifecycle -// Missing test: -it("handles operator removal between register and remove validator", async () => { - // Register 2 validators with 4 operators - // Advance 100 blocks - // Remove operator[1] - // Advance 100 blocks - // Remove 1 validator - // Verify fees split correctly across 2 periods -}); -5. All Operators Removed -// Missing test: -it("handles cluster with all operators removed", async () => { - // Remove all 4 operators one by one - // Attempt cluster operations - // Verify correct reverts or handling -}); -6. Network Fee Impact -// Missing test: -it("network fees continue accruing after operator removal", async () => { - // Don't zero network fee - // Remove operator - // Verify cluster balance includes network fees + (3 operator fees) - // Verify DAO balance increases correctly -}); -7. Removed Operator Fee Withdrawal -// Missing test: -it("removed operator can withdraw frozen earnings", async () => { - // Accrue earnings for operator - // Remove operator - // Verify operator can still withdraw frozen balance - // Verify no new earnings after withdrawal -}); - ---- - - -### [TEST-20] ~~Cooldown duration changes affecting pending requests~~ -- **Type:** Unit Test Completeness -- **Priority:** P1 -- **Status:** ✅ Closed -- **Owner:** (resolved) -- **Timeline:** 2026-03-03 -- **Github Link:** (empty) - -**Requirement:** -Test how changes to `cooldownDuration` affect pending unstake withdrawal requests. - -**Context:** -`updateUnstakeCooldownDuration` is tested for storage but not for impact on existing pending requests. - -**Resolution:** -Added direct coverage for cooldown-change behavior on existing pending unstake requests in staking unit tests: -- cooldown reduction after request creation does not unlock existing request early -- cooldown increase after request creation preserves original unlock time - -This matches the `test(staking): cover cooldown updates on pending unstake requests` change and validates that `unlockTime` is fixed at request creation. - -**Resolution:** -Added direct coverage in `test/unit/SSVStaking/withdrawUnlocked.test.ts` under `describe("Cooldown duration changes and existing pending requests")`: -- `Does not unlock an existing request earlier when cooldown is reduced after request creation` -- `Keeps original unlock time for existing request when cooldown is increased after request creation` - -Both tests create a pending unstake request first, then update cooldown via the staking harness (`mockSetCooldownDuration`) to simulate DAO-config changes. They verify previously stored `unlockTime` remains unchanged and withdrawal eligibility still follows the original request timestamp. -Validation run: `npx hardhat test test/unit/SSVStaking/withdrawUnlocked.test.ts` (13 passing). - -**Resolution:** -Added direct coverage for cooldown-change behavior on existing pending unstake requests in staking unit tests: -- cooldown reduction after request creation does not unlock existing request early -- cooldown increase after request creation preserves original unlock time - -This matches the `test(staking): cover cooldown updates on pending unstake requests` change and validates that `unlockTime` is fixed at request creation. - -**Acceptance Criteria:** -- [x] Test: User requests unstake, DAO reduces cooldown → can user withdraw earlier? -- [x] Test: User requests unstake, DAO increases cooldown → does user's original unlock time hold? - -**Agent Instructions:** -1. Read `test/unit/SSVStaking/requestUnstake.test.ts` and `test/unit/SSVStaking/withdrawUnlocked.test.ts`. -2. Read `contracts/modules/SSVStaking.sol` to understand how `unlockTime` is stored (is it absolute timestamp or relative?). -3. Create tests: stake → request unstake → change cooldown → attempt withdraw → verify behavior. -4. Run `npm run test:unit`. - -#### Sub-items: -- [x] Sub-task 1: Cooldown reduction — earlier withdrawal test -- [x] Sub-task 2: Cooldown increase — original unlock time test - ---- - -### [TEST-21] ~~EB boundary values (min/max per validator)~~ -- **Type:** Unit Test Completeness -- **Priority:** P2 -- **Status:** ✅ Closed -- **Owner:** (resolved) -- **Timeline:** (complete) -- **Github Link:** (empty) - -**Requirement:** -Add boundary tests for EB values at minimum (32 ETH) and maximum (2048 ETH) per validator. - -**Context:** -Limited boundary testing exists. The sanity tests cover conversions but not the full cluster accounting at boundaries. - -**Resolution:** -All three boundary cases are covered in `test/unit/SSVClusters/updateClusterBalance.test.ts`: -- EB=32 baseline (10000 vUnits): pre-existing test "Updates cluster balance when proof is valid" -- EB=2049 revert: pre-existing test "Is reverted with 'EBExceedsMaximum' when effective balance exceeds 2048 ETH per validator" -- EB=2048 max (640000 vUnits): new test with full vUnit/deviation/DAO accounting assertions -- EB=4096 max for 2-validator cluster (1,280,000 vUnits): new test with per-operator deviation assertions -- EB=4097 revert for 2-validator cluster: new multi-validator max-exceeded test - -**Acceptance Criteria:** -- [x] Test: EB exactly 32 ETH per validator (10000 vUnits) — baseline behavior -- [x] Test: EB exactly 2048 ETH per validator (640000 vUnits) — max behavior -- [x] Test: EB at 2049 per validator — verify revert - -#### Sub-items: -- [x] Sub-task 1: EB=32 baseline test -- [x] Sub-task 2: EB=2048 maximum test -- [x] Sub-task 3: EB>2048 revert test - ---- - -### [TEST-22] ~~Dust/precision edge cases~~ -- **Type:** Unit Test Completeness -- **Priority:** P2 -- **Status:** ✅ Closed -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Add precision edge case tests for packed type boundaries and tiny values. - -**Acceptance Criteria:** -- [x] Test: Withdraw amount of exactly 1 * ETH_DEDUCTED_DIGITS (minimum non-zero) -- [x] Test: Cluster balance that rounds to 0 after fee deduction -- [x] Test: Operator earnings of exactly 1 packed unit — verify withdrawable -- [x] Test: accEthPerShare with tiny fee and large totalStaked — verify no rounding to zero - -**Resolution:** -4 tests added across 3 files (416 total, all passing): -- `test/unit/SSVOperators/withdrawOperatorEarnings.test.ts` — "Withdraws exactly 1 * ETH_DEDUCTED_DIGITS (minimum non-zero precision unit) and zeroes balance" (covers criteria 1 & 3) -- `test/unit/SSVClusters/withdraw.test.ts` — "Cluster balance becomes 0 when accumulated fees exceed the remaining balance (no underflow)" (criteria 2) -- `test/unit/SSVStaking/syncFees.test.ts` — "Produces non-zero accEthPerShare update with minimum possible fee (1 packed unit) and standard stake" (criteria 4; verifies `accDelta = 10_000 > 0` for `newFees = 1` packed unit with `STAKE_AMOUNT = 10 ETH`) - -**Agent Instructions:** -1. Read `test/unit/packedLib.test.ts` for packed type patterns. -2. Create edge case tests using minimum possible values. -3. Run `npm run test:unit`. - -#### Sub-items: -- [x] Sub-task 1: Minimum withdrawal amount -- [x] Sub-task 2: Zero-rounding cluster balance -- [x] Sub-task 3: Minimum operator earnings -- [x] Sub-task 4: Precision in accEthPerShare - ---- - -### [TEST-23] Max operator count (13) with EB -- **Type:** Unit Test Completeness -- **Priority:** P2 -- **Status:** ✅ Closed -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Add tests for 13-operator clusters with high EB values to verify no overflow. - -**Acceptance Criteria:** -- [x] Test: 13 operators with EB=2048 — verify no overflow, correct accounting -- [x] Test: Liquidation with 13 operators and high EB — verify threshold calculation - -**Agent Instructions:** -1. Read existing gas tests for 13 operators in `test/unit/SSVValidator/`. -2. Create tests combining 13 operators with maximum EB. -3. Run `npm run test:unit`. - -#### Sub-items: -- [x] Sub-task 1: 13 operators + EB=2048 accounting -- [x] Sub-task 2: 13 operators + high EB liquidation - -**Resolution:** -Two tests added to `test/unit/SSVClusters/updateClusterBalance.test.ts`: -1. **"Updates vUnit accounting correctly for 13 operators at maximum EB (2048 ETH per validator)"** — registers a cluster with 13 operators, updates EB to 2048, verifies: clusterVUnits = 640,000; daoTotalEthVUnits = 640,000; each operator deviation = 630,000; each operator effective vUnits = 640,000. No overflow. -2. **"Auto-liquidates cluster with 13 operators when EB increase to maximum makes it insolvent"** — verifies that the liquidation threshold calculation with 13 operators at EB=2048 (vUnits=640,000) correctly triggers auto-liquidation inside `updateClusterBalance`. Deposit is solvent at EB=32 (threshold ≈ 0.000014 ETH) but insolvent at EB=2048 (threshold ≈ 0.000896 ETH). After auto-liquidation, all 13 operator vUnit deviations are cleaned up to 0. - ---- - -### [TEST-24] Idempotency and double-operation checks -- **Type:** Unit Test Completeness -- **Priority:** P2 -- **Status:** ✅ Closed -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Add tests verifying that double-calling operations either reverts or is safely idempotent. - -**Acceptance Criteria:** -- [x] Test: `exitValidator` twice on same validator → verify second succeeds -- [x] Test: `syncFees` twice in same block → verify no double-counting -- [x] Test: `updateClusterBalance` with same proof twice → verify stale block revert - -**Agent Instructions:** -1. Read relevant test files for each operation. -2. Call each operation twice and verify the second call either reverts with the correct error or is safely no-op. -3. Run `npm run test:unit`. - -#### Sub-items: -- [x] Sub-task 1: Double `exitValidator` -- [x] Sub-task 2: Double `syncFees` in same block -- [x] Sub-task 3: Double `updateClusterBalance` with same proof - -**Resolution:** -- **`exitValidator` twice** (`test/unit/SSVValidator/exitValidator.test.ts`): `exitValidator` does not mutate validator state (only emits an event after validating the stored operator hash), so calling it twice is safely idempotent — both calls succeed and emit `ValidatorExited`. Test added: "Calling exitValidator twice on the same validator succeeds both times without reverting". -- **`syncFees` twice** (`test/unit/SSVStaking/syncFees.test.ts`): After the first call, the staking pool balance is updated to match the DAO balance. The second call sees no delta (current == previous), emits no `FeesSynced` event, and leaves `accEthPerShare` unchanged. Test added: "Calling syncFees twice does not double-count fees — second call is a no-op". -- **`updateClusterBalance` same proof** (`test/unit/SSVClusters/updateClusterBalance.test.ts`): Already covered by the existing test "Is reverted with 'StaleUpdate' when blockNum is not increasing" — calling with the same (or lower) `blockNum` reverts with `StaleUpdate`. No new test needed. - ---- - -### [TEST-25] Upgrade path (reinitializer) tests -- **Type:** Unit Test Completeness -- **Priority:** P2 -- **Status:** ✅ Closed -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Add tests for the upgrade initializer (`reinitializer(3)`) behavior. - -**Acceptance Criteria:** -- [x] Test: Call initializer with `reinitializer(3)` → verify new state set correctly -- [x] Test: Call initializer again → verify reverts (already initialized) -- [x] Test: Verify `UPGRADE_TIMESTAMP` immutable prevents pre-migration fee declarations - -**Agent Instructions:** -1. Read `contracts/upgrades/stage/hoodi/SSVNetworkSSVStakingUpgrade.sol`. -2. Read `test/setup/` for how upgrades are performed in tests. -3. Create tests that upgrade the proxy and verify the initializer runs correctly, then fails on re-call. -4. Run `npm run test:unit`. - -#### Sub-items: -- [x] Sub-task 1: Successful reinitializer(3) execution -- [x] Sub-task 2: Re-initialization revert -- [x] Sub-task 3: UPGRADE_TIMESTAMP fee declaration guard - -**Resolution:** -- **Sub-task 1 (state set correctly):** Already covered by `test/integration/SSVNetwork.test.ts` — "Configures SSVNetwork correctly" verifies `cooldownDuration`, `defaultOracleIds`, `quorumBps`, and all governance params post-upgrade. -- **Sub-task 2 (re-initialization revert):** Added to `test/integration/SSVNetwork.test.ts` under "Constructor, initializer and upgrades": "Calling initializeSSVStaking again reverts with already-initialized error". Attaches `SSVNetworkSSVStakingUpgrade` factory to the already-upgraded proxy and calls `initializeSSVStaking` again — reverts with OZ v4 string error `"Initializable: contract is already initialized"`. -- **Sub-task 3 (UPGRADE_TIMESTAMP guard):** Already covered by `test/unit/SSVOperators/executeOperatorFee.test.ts` — "Is reverted with 'LegacyOperatorFeeDeclarationInvalid' when executing a pre-upgrade fee declaration". Deploys SSVOperators with a future `upgradeTimestamp`, mocks a fee declaration with `approvalBeginTime <= upgradeTimestamp`, verifies `executeOperatorFee` reverts. - ---- - -### [TEST-26] Zero-validator cluster operations -- **Type:** Unit Test Completeness -- **Priority:** P2 -- **Status:** ✅ Closed -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Add tests for clusters with 0 validators. - -**Acceptance Criteria:** -- [x] Test: Deposit into cluster with 0 validators → verify no fees accrue -- [x] Test: Withdraw from cluster with 0 validators → verify full balance withdrawable -- [x] Test: EB update on cluster with 0 validators → verify no vUnits change -- [x] Test: Oracle EB report (`effectiveBalance = 0`) on active cluster with `validatorCount == 0` (all validators removed, cluster not deleted) → verify: (a) `_verifyEBLimits` passes (`0 >= 0 * 32`), (b) `ebToVUnits(0)` returns `0`, (c) `clusterEB.vUnits` written as `0` (resets any prior explicit EB back to implicit-EB sentinel), (d) no `operatorEthVUnits` or `daoTotalEthVUnits` changes, (e) no auto-liquidation triggered, (f) `ClusterBalanceUpdated` emitted with `effectiveBalance = 0` - -**Agent Instructions:** -1. Read `test/unit/SSVClusters/deposit.test.ts` and `test/unit/SSVClusters/withdraw.test.ts`. -2. Create a cluster, remove all validators, then perform operations. -3. For sub-task 4: register a cluster with explicit EB (run one `updateClusterBalance` with non-zero EB first), then remove all validators, then submit a valid oracle proof with `effectiveBalance = 0`. Assert all storage fields and events per acceptance criteria above. -4. Run `npm run test:unit`. - -#### Sub-items: -- [x] Sub-task 1: Deposit with 0 validators -- [x] Sub-task 2: Withdrawal with 0 validators -- [x] Sub-task 3: EB update with 0 validators (generic) -- [x] Sub-task 4: Oracle EB report with `effectiveBalance = 0` on active zero-validator cluster — full state assertion (see DISC.md §2.2) - -**Resolution:** -- **Sub-task 1** (`test/unit/SSVClusters/deposit.test.ts`): "Deposit into zero-validator cluster accrues no fees over elapsed blocks" — uses non-zero operator fee fixture, registers then removes the only validator, mines 100 blocks, deposits, verifies balance = removal_balance + deposit_amount exactly (no fee deduction since vUnits = 0). -- **Sub-task 2** (`test/unit/SSVClusters/withdraw.test.ts`): "Zero-validator cluster allows full balance withdrawal without fee deduction" — non-zero fee + network fee, removes last validator, mines 100 blocks, withdraws full balance, verifies cluster balance = 0 and cluster still active. -- **Sub-task 3** (`test/unit/SSVClusters/updateClusterBalance.test.ts`): "EB update with effectiveBalance = 0 on zero-validator cluster succeeds without modifying vUnit state" — basic case (no prior explicit EB), verifies ClusterBalanceUpdated emitted with effectiveBalance = 0, clusterVUnits = 0, no vUnit changes. -- **Sub-task 4** (`test/unit/SSVClusters/updateClusterBalance.test.ts`): "Oracle EB report effectiveBalance = 0 on active zero-validator cluster resets explicit EB to implicit-EB sentinel" — full state assertion: first sets EB = 64 ETH (explicit vUnits = 20000), removes last validator (vUnits cleared to 0), then submits effectiveBalance = 0 via updateClusterBalance; verifies all (a)-(f): limits pass, vUnits = 0, operatorEthVUnits = 0, daoTotalEthVUnits unchanged, no auto-liquidation, ClusterBalanceUpdated emitted with effectiveBalance = 0, cluster still active. - ---- - -### [TEST-27] Operator at max validator limit -- **Type:** Unit Test Completeness -- **Priority:** P2 -- **Status:** ✅ Closed -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Test `VALIDATORS_PER_OPERATOR_LIMIT` (3000) boundary. - -**Acceptance Criteria:** -- [x] Test: Register validator pushing operator to limit+1 → verify revert -- [x] Test: Remove validator then re-register at limit → verify succeeds - -**Resolution:** -Added two tests to `test/unit/SSVValidator/registerValidator.test.ts`: -- Used `mockValidatorsPerOperatorLimit(5)` to avoid bulk-registering 3000 validators -- Used `bulkRegisterValidator` to fill all operators to the limit (5 validators) -- Sub-task 1: 6th `registerValidator` call reverts with `ExceedValidatorLimitWithData(operatorIds[0])` -- Sub-task 2: After removing one validator (back to 4), re-register succeeds and emits `ValidatorAdded` - -#### Sub-items: -- [x] Sub-task 1: Exceed operator validator limit — revert -- [x] Sub-task 2: Re-register at limit after removal - ---- - -### [TEST-28] Uncomment SSV reentrancy test assertions -- **Type:** Unit Test Completeness -- **Priority:** P0 -- **Status:** Open -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Uncomment the three commented-out assertions in the SSV operator reentrancy test and verify they pass. - -**Context:** -In `test/unit/SSVOperators/reentrancy.test.ts:101-107`, three assertions are commented out inside `/* */`. The SSV token reentrancy guard is effectively untested. The ETH reentrancy test in the same file IS properly asserted. This means the SSV withdrawal path has no verified reentrancy protection. - -**Acceptance Criteria:** -- [ ] Lines 101-107 uncommented -- [ ] All three assertions pass -- [ ] If assertions fail, fix the mock contract or reentrancy guard to make them pass - -**Agent Instructions:** -1. Read `test/unit/SSVOperators/reentrancy.test.ts`, focus on lines 95-110. -2. Uncomment the three assertions at lines 101-107. -3. Run `npm run test:unit` to verify they pass. -4. If they fail, investigate whether the mock reentrancy contract or the reentrancy guard needs fixing. - -#### Sub-items: -- [ ] Sub-task 1: Uncomment SSV reentrancy assertions -- [ ] Sub-task 2: Verify test passes (fix if needed) - ---- - -### [TEST-29] ~~Add contract ETH balance delta assertions to deposit tests~~ -- **Type:** Unit Test Completeness -- **Priority:** P1 -- **Status:** ✅ Done -- **Owner:** (unassigned) -- **Timeline:** 2026-02-26 -- **Github Link:** (empty) - -**Requirement:** -Add `address(contract).balance` before/after assertions to ETH deposit tests. Currently tests verify cluster balance in events but never check the actual contract ETH balance change. - -**Context:** -In `test/unit/SSVClusters/deposit.test.ts`, tests verify cluster balance in events but never check `address(contract).balance` before and after the deposit. This means the contract could emit the correct event but not actually receive the ETH. - -**Concrete test:** Register with 10 ETH, deposit 5 ETH, assert `contractBalance_after - contractBalance_before == 5 ETH`. - -**Resolution:** -Added explicit `address(clusters).balance` delta assertions in `test/unit/SSVClusters/deposit.test.ts` for a single deposit and for a multi-deposit ("bulk" sequential deposits) scenario. The multi-deposit test asserts per-deposit deltas and cumulative ETH balance growth across two deposits (owner + third-party depositor). Validation run: `npx hardhat test test/unit/SSVClusters/deposit.test.ts` (6 passing) and `npm run test:unit` (414 passing). - -**Acceptance Criteria:** -- [x] At least one deposit test captures contract ETH balance before and after -- [x] Asserts `balanceAfter - balanceBefore == msg.value` -- [x] Both single and bulk deposit scenarios covered - -**Agent Instructions:** -1. Read `test/unit/SSVClusters/deposit.test.ts` for existing patterns. -2. Add balance capture: `const before = await ethers.provider.getBalance(ssvNetwork.address)`. -3. After deposit: `const after = await ethers.provider.getBalance(ssvNetwork.address)`. -4. Assert: `expect(after - before).to.equal(depositAmount)`. -5. Run `npm run test:unit`. - -#### Sub-items: -- [x] Sub-task 1: Add ETH balance delta assertion to deposit test -- [x] Sub-task 2: Run full test suite - ---- - -### [TEST-30] Resolve TODO comments with deferred assertions -- **Type:** Unit Test Completeness -- **Priority:** P1 -- **Status:** Open -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Resolve the 12 TODO comments across test files that indicate event args not verified against computed expected values. - -**Context:** -In `test/unit/SSVValidator/registerValidator.test.ts:56`, `bulkRegisterValidator.test.ts:58`, and 10 other locations, TODO comments indicate that event arguments are not being verified against independently computed expected values. These represent deferred test assertions that should be completed. - -**Acceptance Criteria:** -- [ ] All 12 TODO comments identified and resolved -- [ ] Each TODO replaced with actual assertion or removed with justification -- [ ] No new test failures introduced - -**Agent Instructions:** -1. Grep for `TODO` across all test files to identify the 12 locations. -2. For each TODO: read the surrounding test context, compute the expected value, add the assertion. -3. Run `npm run test:unit` after each batch of changes. - -#### Sub-items: -- [ ] Sub-task 1: Identify all 12 TODO locations -- [ ] Sub-task 2: Resolve each TODO with actual assertions -- [ ] Sub-task 3: Run full test suite - ---- - -### [TEST-31] Expand onCSSVTransfer test coverage -- **Type:** Unit Test Completeness -- **Priority:** P1 -- **Status:** Open -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Expand `onCSSVTransfer` tests from the current 2 tests to cover multi-transfer sequences, transfers after fee accruals, and transfers between users with pending rewards. - -**Context:** -In `test/unit/SSVStaking/onCSSVTransfer.test.ts`, only 2 tests exist. Missing scenarios: multi-transfer sequences, transfer after fee accruals, transfer between users with pending rewards. The `onCSSVTransfer` hook is critical for correct reward settlement during cSSV transfers. - -**Concrete test:** User A (100 cSSV) transfers 50 to User B (200 cSSV) after fee sync. Verify both parties' rewards settled correctly using `pendingReward = cSSVBalance * (accEthPerShare - userIndex) / 1e18`. - -**Acceptance Criteria:** -- [ ] Test: multi-transfer sequence (A→B→C) with reward verification at each step -- [ ] Test: transfer after fee accruals — verify accumulated rewards settled before transfer -- [ ] Test: transfer between users with pending rewards — verify both rewards correct -- [ ] At least 5 total test cases for `onCSSVTransfer` - -**Agent Instructions:** -1. Read `test/unit/SSVStaking/onCSSVTransfer.test.ts` for existing patterns. -2. Read `contracts/modules/SSVStaking.sol`, focus on `onCSSVTransfer` (line 169). -3. Add multi-transfer, fee-accrual, and pending-reward test scenarios. -4. Calculate expected rewards independently using the accumulator formula. -5. Run `npm run test:unit`. - -#### Sub-items: -- [ ] Sub-task 1: Multi-transfer sequence test -- [ ] Sub-task 2: Transfer after fee accrual test -- [ ] Sub-task 3: Transfer with pending rewards test - ---- - -### [TEST-32] Add access control tests for DAO governance functions -- **Type:** Unit Test Completeness -- **Priority:** P1 -- **Status:** ✅ Complete -- **Owner:** (unassigned) -- **Timeline:** 2026-02-26 -- **Github Link:** (empty) - -**Requirement:** -Add non-owner revert tests for all DAO governance functions. Currently all SSVDAO test files only test happy path from owner. - -**Context:** -All 11+ governance functions (`updateNetworkFee`, `updateLiquidationThresholdPeriod`, `replaceOracle`, `updateQuorumBps`, `updateUnstakeCooldownDuration`, `updateMaximumOperatorFee`, `updateMinimumOperatorEthFee`, etc.) are tested only from the owner account. No test verifies that non-owner calls are rejected. - -**Acceptance Criteria:** -- [x] Each governance function has a test calling from non-owner that expects revert -- [x] Revert reason matches expected access control error (legacy branch behavior: `Ownable: caller is not the owner`) -- [x] All 11+ functions covered - -**Resolution:** -- Added `/Users/venimir/Desktop/ssv/contracts-latest/ssv-network/test/unit/SSVDAO/accessControl.test.ts` with non-owner access-control tests for 15 owner-only DAO governance wrappers on `SSVNetwork`: - - `updateNetworkFee`, `updateNetworkFeeSSV`, `withdrawNetworkSSVEarnings` - - `updateOperatorFeeIncreaseLimit`, `updateDeclareOperatorFeePeriod`, `updateExecuteOperatorFeePeriod` - - `updateLiquidationThresholdPeriod`, `updateLiquidationThresholdPeriodSSV` - - `updateMinimumLiquidationCollateral`, `updateMinimumLiquidationCollateralSSV` - - `updateMaximumOperatorFee`, `updateMinimumOperatorEthFee` - - `updateUnstakeCooldownDuration`, `replaceOracle`, `updateQuorumBps` -- Verified non-owner calls revert with the legacy Ownable string on this branch (`Ownable: caller is not the owner`), rather than OZ's newer `OwnableUnauthorizedAccount` custom error. -- Verified with `npx hardhat test test/unit/SSVDAO/accessControl.test.ts` and `npm run test:unit` (`428 passing`). - -**Agent Instructions:** -1. Read `test/unit/SSVDAO/` directory for all existing DAO test files. -2. For each governance function, add a test that calls from a non-owner signer. -3. Assert revert with the expected access control error. -4. Run `npm run test:unit`. - -#### Sub-items: -- [x] Sub-task 1: Identify all governance functions requiring access control tests -- [x] Sub-task 2: Add non-owner revert test for each function -- [x] Sub-task 3: Run full test suite - ---- - -### [TEST-33] Mainnet governance config validation & edge-case tests -- **Type:** Unit Test Completeness -- **Priority:** P1 -- **Status:** Open -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Add a dedicated test suite that uses the exact mainnet governance parameters and validates system behavior at the boundaries implied by those values. This ensures the production config is safe before deployment. - -**Mainnet Config (from deployment spreadsheet):** -| Param | Value | Wei/Raw | -|-------|-------|---------| -| ethNetworkFee | 0.000000003550929823 ETH/block | 3,550,929,823 | -| minimumLiquidationCollateral | 0.00094 ETH | 940,000,000,000 | -| minimumBlocksBeforeLiquidation | ~5 days | 35,800 | -| operatorMinFee | 0.000000001065278947 ETH/block | 1,065,278,947 | -| operatorMaxFee | 0.000000005326394735 ETH/block | 5,326,394,735 | -| defaultOperatorETHFee | 0.000000001775464912 ETH/block | 1,775,464,912 | -| quorumBps | 75% | 7,500 | -| cooldownDuration | 7 days | 50,120 | - -**Test scenarios:** -1. **Packability** — verify all fee values survive pack/unpack round-trip without precision loss (divisible by `ETH_DEDUCTED_DIGITS`). If a value isn't packable, document the closest packable equivalent. -2. **Liquidation threshold math** — with 4 operators at defaultOperatorETHFee + ethNetworkFee, calculate exactly how many blocks / how much balance keeps a cluster solvent vs liquidatable. Verify `isLiquidatable` agrees. -3. **Operator fee boundaries** — declare fees at operatorMinFee and operatorMaxFee, verify both accepted. Declare fee at operatorMinFee-1 and operatorMaxFee+1, verify both rejected. -4. **Cluster burn rate** — with mainnet fees and varying validator counts (1, 4, 13), compute expected burn rate per block. Verify `getBalance` returns correct remaining balance after N blocks. -5. **Cooldown duration** — set cooldownDuration to 50,120. Request unstake, verify cannot claim before 50,120 blocks/seconds elapse, can claim after. (Also clarifies the blocks-vs-seconds question from BUG-8.) -6. **Quorum** — with 4 oracles and quorumBps=7500, verify exactly 3 votes are needed to commit a root. 2 votes should fail, 3 should succeed. -7. **Liquidation collateral** — deposit exactly minimumLiquidationCollateral, verify cluster is NOT liquidatable at block 0. Verify it IS liquidatable after enough blocks to exhaust balance below threshold. -8. **Long-running clusters** — with mainnet fees, simulate a cluster running for 1 year (~2,628,000 blocks). Verify no overflow in fee index calculations and balance accounting remains correct. - -**Acceptance Criteria:** -- [ ] Test file `test/unit/mainnet-config-validation.test.ts` (or similar) created -- [ ] All 8 test scenarios above implemented with exact mainnet values -- [ ] Each test includes numeric assertions (expected vs actual) with comments showing the math -- [ ] All tests pass -- [ ] Any packability issues documented (values that need rounding for on-chain use) - -**Agent Instructions:** -1. Read `test/setup/fixtures.ts` and `test/common/` for test patterns and constants. -2. Create a new test file for mainnet config validation. -3. Use the exact wei values from the table above as test constants. -4. For each scenario, include a comment with the expected math (e.g., "4 operators × 1,775,464,912 wei/block × 35,800 blocks = X wei burn"). -5. For packability tests, use `SSVPackedLib` to pack/unpack each value and assert round-trip equality. -6. Run `npm run test:unit`. - -#### Sub-items: -- [ ] Sub-task 1: Create test file with mainnet config constants -- [ ] Sub-task 2: Implement packability round-trip tests -- [ ] Sub-task 3: Implement liquidation/solvency boundary tests -- [ ] Sub-task 4: Implement operator fee boundary tests -- [ ] Sub-task 5: Implement burn rate and long-running cluster tests -- [ ] Sub-task 6: Implement cooldown and quorum tests -- [ ] Sub-task 7: Run full test suite - ---- - -### [TEST-34] ~~Staking solvency invariant: cSSV supply must not exceed SSV held by staking contract~~ -- **Type:** Unit Test Completeness -- **Priority:** P1 -- **Status:** ✅ Done -- **Owner:** (unassigned) -- **Timeline:** 2026-02-26 -- **Github Link:** (empty) - -**Requirement:** -Add invariant coverage for staking solvency: `cSSV.totalSupply() <= SSV.balanceOf(SSVStaking)` at all times. - -**Product concern:** -Product asked for explicit safety validation to ensure cSSV issuance cannot exceed backing SSV even if future changes introduce bugs. Current implementation is by-construction (SSV transfer happens before cSSV mint), but the invariant should be continuously enforced by tests. - -**Context:** -`SSVStaking.stake()` transfers SSV to staking contract before minting cSSV, and `requestUnstake()` burns cSSV before eventual SSV withdrawal. This implies the solvency relationship should always hold, but there is no explicit invariant test guarding against regressions. - -**Invariant to test:** -`cSSV.totalSupply() <= SSV.balanceOf(address(SSVStaking))` - -**Resolution:** -Added explicit Echidna invariant `echidna_cssv_supply_lte_ssv_backing()` in `test/echidna/SSVStakingEchidna.sol` and deterministic regression coverage in `test/unit/SSVStaking/solvencyInvariant.test.ts` for single-user ordering, multi-user partial unstake requests, and full unstake/withdraw flows. Also aligned the Echidna harness `MAX_PENDING_REQUESTS` constant with `SSVStaking` (`2000`) to avoid a harness-only false failure in `echidna_pending_requests_bounded`. Validation run: `npx hardhat test test/unit/SSVStaking/solvencyInvariant.test.ts` (3 passing) and `echidna ... SSVStakingEchidna ...` (12/12 invariants passing, including solvency invariant). - -**Acceptance Criteria:** -- [x] Add an Echidna invariant test that continuously asserts `cSSV.totalSupply() <= SSV.balanceOf(address(staking))` across stake/unstake/transfer/withdraw flows -- [x] Add at least one deterministic unit regression test for the invariant around `stake` and `requestUnstake` ordering -- [x] Include edge scenarios: multiple users, partial unstake requests, full unstake + withdraw cycle -- [x] No invariant violations in fuzz runs - -**Agent Instructions:** -1. Read `contracts/modules/SSVStaking.sol` and `contracts/token/CSSVToken.sol` for mint/burn ordering. -2. Extend the Echidna suite under `test/echidna/` with a dedicated solvency invariant check. -3. Add a deterministic unit test in `test/unit/SSVStaking/` asserting the invariant before/after `stake`, `requestUnstake`, and `withdrawUnlocked`. -4. Run the relevant unit tests and Echidna target. - -#### Sub-items: -- [x] Sub-task 1: Add Echidna solvency invariant -- [x] Sub-task 2: Add deterministic unit regression tests -- [x] Sub-task 3: Cover multi-user + partial/full unstake scenarios -- [x] Sub-task 4: Run unit + Echidna checks - ---- - -## Integration / E2E Tests - -### [ITEST-1] ~~`commitRoot` → `updateClusterBalance` E2E flow~~ -- **Type:** Integration / E2E Tests -- **Priority:** P1 -- **Status:** ✅ **CLOSED** -- **Owner:** Test coverage update -- **Timeline:** Completed 2026-03-03 -- **Github Link:** [test/integration/SSVNetwork/commitRootUpdateClusterBalance.test.ts](../test/integration/SSVNetwork/commitRootUpdateClusterBalance.test.ts) - -**Requirement:** -Create an end-to-end test connecting oracle voting → root commitment → cluster EB update → fee recalculation. - -**Context:** -Unit tests for `commitRoot` and `updateClusterBalance` exist separately but no test connects the full flow. This is the core oracle→cluster pipeline. - -**Acceptance Criteria:** -- [x] Test: 3 oracles propose same root → root committed → cluster calls `updateClusterBalance` with proof from committed root → verify fees recalculated with new EB -- [x] Test: Multiple clusters update EB from same root → verify independent accounting - -**Implementation Summary:** -1. Added a dedicated integration suite: [commitRootUpdateClusterBalance.test.ts](../test/integration/SSVNetwork/commitRootUpdateClusterBalance.test.ts). -2. Added E2E test for quorum flow (`3/4` oracle votes) that commits root and executes `updateClusterBalance` with valid Merkle proof. -3. Added exact-value assertion that EB update to `64` doubles post-update operator earnings accrual vs baseline. -4. Added multi-cluster scenario from one committed root and verified independent accounting with exact formula-based balance deltas per cluster. - -**Agent Instructions:** -1. Read `test/unit/SSVDAO/commitRoot.test.ts` and `test/unit/SSVClusters/updateClusterBalance.test.ts`. -2. Read `test/integration/SSVNetwork.test.ts` for integration test patterns. -3. Create a new integration test file or add to existing. -4. Build the full flow: deploy, create cluster, stake SSV for oracle weight, commit oracle root with Merkle tree, then call `updateClusterBalance` with proof from the committed root. -5. Verify the cluster's EB is updated and fee calculations reflect the new EB. -6. Run `npm run test:integration`. - -#### Sub-items: -- [x] Sub-task 1: Full oracle → cluster EB update flow -- [x] Sub-task 2: Multiple clusters from same root - ---- - -### [ITEST-2] ~~Migration with multiple EB updates E2E~~ -- **Type:** Integration / E2E Tests -- **Priority:** P1 -- **Status:** ✅ **CLOSED** -- **Owner:** Test coverage update -- **Timeline:** Completed 2026-03-03 -- **Github Link:** [test/integration/SSVNetwork/migrationMultipleEBUpdates.test.ts](../test/integration/SSVNetwork/migrationMultipleEBUpdates.test.ts) - -**Requirement:** -Test migration of a cluster that has had multiple EB updates, verifying the latest snapshot is used. - -**Context:** -Migration with EB snapshot is tested but edge cases with multiple prior EB updates are not. - -**Acceptance Criteria:** -- [x] Test: Migrate cluster that has had multiple EB updates → verify latest snapshot used -- [x] Test: Migrate cluster where EB was set and then validators were added → verify vUnits calculated correctly - -**Implementation Summary:** -1. Added dedicated ITEST-2 suite: [migrationMultipleEBUpdates.test.ts](../test/integration/SSVNetwork/migrationMultipleEBUpdates.test.ts). -2. Added scenario for multiple pre-migration EB updates (`64 -> 96`) and verified migration uses the latest EB snapshot in `ClusterMigratedToETH`. -3. Added scenario where EB is set, validator count is increased, and EB is updated again before migration; verified migrated vUnits/effective balance are calculated from the latest post-addition snapshot. -4. Added exact-value assertions for `daoTotalEthVUnits`, per-operator vUnits, and migrated cluster state. - -**Agent Instructions:** -1. Read `test/unit/SSVClusters/migrateClusterToETH.test.ts`. -2. Create a cluster, update EB multiple times via `updateClusterBalance`, then migrate to ETH. -3. Verify the migrated cluster uses the latest EB values. -4. Run `npm run test:integration`. - -#### Sub-items: -- [x] Sub-task 1: Migration after multiple EB updates -- [x] Sub-task 2: Migration after EB set + validators added - ---- - -## Deployment & Scripts - -### [DEPLOY-1] ~~Fix `deploy-all.ts` broken signature and constructor args~~ -- **Type:** Deployment & Scripts -- **Priority:** P0 -- **Status:** ✅ Fixed -- **Owner:** (resolved) -- **Timeline:** (complete) -- **Github Link:** [PR #431](https://github.com/ssvlabs/ssv-network/pull/431) - -**Requirement:** -Fix deployment scripts so that fresh deployments work. `deploy-all.ts` had wrong `initializeSSVStaking` signature and missing constructor args for 3 modules. - -**Context:** -`scripts/deploy-all.ts` (now deleted) used `"initializeSSVStaking(address,uint64)"` with `[cssvTokenAddr, cooldown]`. Actual contract signature is `initializeSSVStaking(uint64,uint32[4],uint16)`. Also, `SSVDAO`, `SSVViews`, `SSVStaking` all require `_cssv` address as constructor arg but were deployed without args. - -**Resolution:** -`deploy-all.ts` replaced by `deploy-fresh.ts` (fresh deployments) and `upgrade.ts` (upgrades). Both use the correct `initializeSSVStaking(uint64,uint32[4],uint16)` three-parameter signature and pass `quorumBps` from config. `CSSVToken` deployed before modules and its address passed as constructor arg. `generate-safe-batch.ts` handles Safe multisig batch encoding. - -**Acceptance Criteria:** -- [x] `initializeSSVStaking` signature is `"initializeSSVStaking(uint64,uint32[4],uint16)"` -- [x] `quorumBps` passed as third argument from deployment config -- [x] `CSSVToken` deployed before modules that need its address -- [x] `SSVDAO`, `SSVViews`, `SSVStaking` deployed with `cssvTokenAddr` as constructor arg - -**Agent Instructions:** -~~Obsolete — resolved by replacing `deploy-all.ts` with `deploy-fresh.ts` and `upgrade.ts`. See Resolution above.~~ - -#### Sub-items: -- [ ] Sub-task 1: Fix `initializeSSVStaking` call signature and params -- [ ] Sub-task 2: Fix constructor args for SSVDAO, SSVViews, SSVStaking -- [ ] Sub-task 3: Reorder CSSVToken deployment before modules -- [ ] Sub-task 4: Verify script runs against local Hardhat - ---- - -### [DEPLOY-2] Verify `liquidationThresholdPeriod` config vs spec mismatch -- **Type:** Deployment & Scripts -- **Priority:** P1 -- **Status:** Open -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Resolve the mismatch between `liquidationThresholdPeriod` in `deployments/hoodi-stage/config.json` (35,800) and the DIP-X spec (50,190 blocks). - -**Context:** -`deployments/hoodi-stage/config.json` sets `liquidationThresholdPeriod: 35800` but the DIP-X spec proposes 50,190 blocks (~7 days). This is a significant difference — 35,800 blocks is ~5 days. If this is intentional for the testnet, it should be documented. The mainnet config (`deployments/mainnet/config.json`) must use the correct value. - -**Acceptance Criteria:** -- [ ] Decision documented: is 35,800 intentional for Hoodi testnet? -- [ ] Mainnet config (when created) uses 50,190 or the final DIP-X approved value -- [ ] Comment added to config explaining the discrepancy if intentional - -**Agent Instructions:** -1. Read `deployments/hoodi-stage/config.json` and `deployments/mainnet/config.json`. -2. Read `docs/SPEC.md` section 11 for the governance parameters. -3. If this is a testnet-specific value, add a comment. If it's a bug, update to 50,190. -4. This is primarily a decision item — flag it for team review if uncertain. - -#### Sub-items: -- [ ] Sub-task 1: Verify intended value with team -- [ ] Sub-task 2: Update config or add documentation - ---- - -### [DEPLOY-3] Verify `ethNetworkFee` rounding in config -- **Type:** Deployment & Scripts -- **Priority:** P2 -- **Status:** Open -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) -- **DIP-X Review Source:** ETH Payments review finding ETH-10 - -**Requirement:** -Verify whether the rounding of `ethNetworkFee` (config: 3,550,900,000 vs spec: 3,550,929,823) is acceptable or needs correction. - -**Context:** -The config rounds to 3,550,900,000 while the spec says 3,550,929,823. The difference is ~30k wei, which over millions of blocks could accumulate to meaningful amounts. - -**Additional context from DIP-X review (ETH-10):** The DIP-specified value `3,550,929,823 % 100,000 = 29,823` — it is NOT divisible by `ETH_DEDUCTED_DIGITS (100,000)`, so the exact DIP value cannot be stored in `PackedETH`. The closest packable values are `3,550,900,000` (rounding down) or `3,551,000,000` (rounding up). The DIP should be updated to note this packing constraint. The initial value is set at deployment/upgrade time (not hardcoded), so the contract itself has no validation that a specific initial value is used — this is a governance responsibility. - -**Acceptance Criteria:** -- [ ] Decision documented: acceptable rounding or needs exact value -- [ ] If exact value needed, verify it passes `MaxPrecisionExceeded` check (divisible by ETH_DEDUCTED_DIGITS = 100,000) - -**Agent Instructions:** -1. Check if 3,550,929,823 is divisible by 100,000 (ETH_DEDUCTED_DIGITS). It's not (remainder = 29,823), so it may need rounding. -2. Verify what the contract's precision check allows. -3. The closest valid value is either 3,550,900,000 or 3,551,000,000. -4. Document the decision. - -#### Sub-items: -- [ ] Sub-task 1: Verify precision constraints -- [ ] Sub-task 2: Document accepted rounding - ---- - -### [DEPLOY-4] ~~Remove unused error declarations~~ -- **Type:** Deployment & Scripts -- **Priority:** P2 -- **Status:** ✅ Fixed -- **Owner:** (resolved) -- **Timeline:** (complete) -- **Github Link:** (empty) - -**Resolution:** -Removed `NotAuthorized()` and `InvalidContractAddress()` from `contracts/interfaces/ISSVNetworkCore.sol`. Both were declared but never referenced anywhere in the codebase. Compilation verified clean. - -**Acceptance Criteria:** -- [x] Both unused errors removed from `ISSVNetworkCore.sol` -- [x] No references to these errors exist in any contract -- [x] Compilation succeeds - -#### Sub-items: -- [x] Sub-task 1: Verify errors are unused -- [x] Sub-task 2: Remove declarations -- [x] Sub-task 3: Verify compilation - ---- - -### [DEPLOY-5] ~~Document `operatorMinFee` governance parameter in DIP-X~~ -- **Type:** Deployment & Scripts -- **Priority:** P2 -- **Status:** ✅ Fixed -- **Owner:** (resolved) -- **Timeline:** (complete) -- **Github Link:** (empty) -- **DIP-X Review Source:** ETH Payments review finding ETH-20 - -**Resolution:** -Updated `docs/SPEC.md` governance parameter table with initial values sourced from `deployments/hoodi-prod/config.json`: -- `minimumOperatorEthFee`: 0.000000001065200000 ETH/block (~0.0028 ETH/year), setter `updateMinimumOperatorEthFee(uint256)` -- `operatorMaxFee` (also TBD): 0.000000005326300000 ETH/block (~0.0140 ETH/year), setter `updateMaximumOperatorFee(uint256)` - -**Acceptance Criteria:** -- [x] DIP-X governance table updated with: update function = `updateMinimumOperatorEthFee(uint256 minFee)`, initial value from config -- [x] Deployment config (`deployments/hoodi-prod/config.json`) verified to include a reasonable initial value - -#### Sub-items: -- [x] Sub-task 1: Document `operatorMinFee` in DIP-X governance table -- [x] Sub-task 2: Verify deployment config includes the parameter - ---- - -### [DEPLOY-6] ~~DIP-X unstaking description doesn't match implementation~~ -- **Type:** Deployment & Scripts -- **Priority:** P2 -- **Status:** ✅ Closed (already correct in SPEC.md and FLOWS.md) -- **Owner:** (resolved) -- **Timeline:** (complete) -- **Github Link:** (empty) -- **DIP-X Review Source:** SSV Staking review finding DIP-7 - -**Resolution:** -Verified `docs/SPEC.md` and `docs/FLOWS.md` already correctly describe the burn-first mechanism. `SPEC.md §3 "Unstaking (Two-Step)"` states: *"`requestUnstake(amount)`: Burns cSSV, creates `UnstakeRequest{amount, unlockTime}`"* — no "lock cSSV → burn later" language exists. `FLOWS.md §5.2` likewise lists burn as step 4 within the same transaction. The original concern about the DIP wording was addressed when these spec documents were authored. No code or doc change needed. - -**Acceptance Criteria:** -- [x] DIP-X unstaking section updated to describe the actual burn-first mechanism -- [x] No code change needed — the implementation is correct and simpler - -#### Sub-items: -- [x] Sub-task 1: Verify SPEC.md and FLOWS.md describe correct burn-first flow -- [x] Sub-task 2: No user-facing doc change needed — spec is authoritative - ---- - -### [DEPLOY-7] ~~Deploy scripts import from test files~~ -- **Type:** Deployment & Scripts -- **Priority:** P2 -- **Status:** ✅ Fixed -- **Owner:** (resolved) -- **Timeline:** (complete) -- **Github Link:** (empty) - -**Requirement:** -Move shared constants out of test files so deploy scripts don't import from test directories. - -**Context:** -`scripts/deploy-all.ts`, `scripts/staking-upgrade.ts`, and `scripts/upgrade-fork.ts` (all now deleted/replaced) imported `DEFAULT_UNSTAKE_COOLDOWN` from `"../test/common/constants.ts"`. Deploy scripts should not depend on test files — this creates a fragile dependency where test refactors can break deployment. - -**Resolution:** -`upgrade.ts` and `deploy-fresh.ts` import all shared config from `scripts/common/config.ts` (new in this merge). No deploy script imports from `test/common/` any longer. The only remaining reference is `scripts/common/fork-test.ts` which uses a local env-var constant — not a cross-boundary import. - -**Acceptance Criteria:** -- [x] Shared constants in `scripts/common/config.ts` -- [x] Deploy scripts import from the new location -- [x] No deploy script imports from `test/common/` - -**Agent Instructions:** -~~Obsolete — resolved. `upgrade.ts` and `deploy-fresh.ts` import from `scripts/common/config.ts`. See Resolution above.~~ - -#### Sub-items: -- [ ] Sub-task 1: Create shared constants file -- [ ] Sub-task 2: Update deploy script imports -- [ ] Sub-task 3: Verify scripts still work - ---- - -## Operational Readiness - -### [OPS-1] Create mainnet deployment runbook -- **Type:** Operational Readiness -- **Priority:** P1 -- **Status:** Open -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Create a step-by-step runbook for the v2.0.0 mainnet upgrade, including pre-flight checks, deployment steps, post-deployment verification, and rollback triggers. - -**Context:** -No mainnet deployment checklist exists. The upgrade involves UUPS proxy upgrades, new module deployments, CSSVToken deployment, initializer execution, and governance parameter setup. The existing `scripts/deployment.md` covers generic deployment but not the v2.0.0-specific flow. - -**Acceptance Criteria:** -- [ ] Document includes pre-flight checks (contract sizes, gas estimates, parameter verification) -- [ ] Step-by-step deployment sequence matching `upgrade.ts` / `generate-safe-batch.ts` flow -- [ ] Post-deployment verification checklist (all parameters set, quorumBps != 0, oracle addresses correct) -- [ ] Rollback triggers and procedure for each step -- [ ] Links to relevant scripts for each step - -**Agent Instructions:** -1. Read `scripts/upgrade.ts` for the upgrade flow reference. -2. Read `scripts/generate-safe-batch.ts` for the mainnet Safe batch encoding flow. -3. Read `scripts/deployment.md` for existing documentation patterns. -4. Create `docs/MAINNET-UPGRADE-RUNBOOK.md` with: - - Pre-flight checklist - - Deployment sequence (numbered steps with exact commands) - - Post-deployment verification queries (using SSVViews) - - Rollback procedures - - Emergency contacts / escalation paths (placeholder) -5. Ensure the runbook explicitly states: "Call `updateQuorumBps(7500)` immediately after upgrade" (see SEC-2). - -#### Sub-items: -- [ ] Sub-task 1: Write pre-flight checks section -- [ ] Sub-task 2: Write deployment sequence -- [ ] Sub-task 3: Write post-deployment verification -- [ ] Sub-task 4: Write rollback procedures - ---- - -### [OPS-2] Create emergency rollback procedure -- **Type:** Operational Readiness -- **Priority:** P1 -- **Status:** Open -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Document how to downgrade/rollback modules if critical issues are found post-deployment. - -**Context:** -The UUPS proxy pattern allows module replacement. If a bug is found in a deployed module, the DAO owner can replace it with a patched version. But there's no documented procedure for this. - -**Acceptance Criteria:** -- [ ] Document covers: how to replace a module with a patched version -- [ ] Covers: how to pause operations if needed (does a pause mechanism exist?) -- [ ] Covers: which state is recoverable and which is not -- [ ] Covers: communication plan for operators/users - -**Agent Instructions:** -1. Read `contracts/SSVNetwork.sol` to understand `updateModule` function. -2. Read `scripts/upgrade.ts` for the module replacement / `updateModule` call pattern. -3. Document the rollback procedure for each module type. -4. Identify what state changes are irreversible (e.g., token transfers, oracle commits). - -#### Sub-items: -- [ ] Sub-task 1: Document module replacement procedure -- [ ] Sub-task 2: Document irrecoverable state changes -- [ ] Sub-task 3: Document communication plan template - ---- - -### [OPS-3] ~~Update `.env.example` for v2.0.0~~ -- **Type:** Operational Readiness -- **Priority:** P2 -- **Status:** ✅ Closed -- **Owner:** (resolved) -- **Timeline:** 2026-03-12 -- **Github Link:** (empty) - -**Requirement:** -Update `.env.example` with v2.0.0 parameter names and values. - -**Resolution:** -Updated `.env.example` to reflect the current v2.0.0 workflow: -- added the actual runtime env vars used by Hardhat and deployment scripts (`MAINNET_RPC_URL`, per-network RPC URLs, private keys, token overrides, `ETHERSCAN_KEY`) -- added fork/test overrides used by the fork runner and test helpers (`FORK_*`, `DEFAULT_ORACLE_IDS`, gas/test toggles) -- added a commented v2.0.0 protocol reference block with current ETH-era defaults (`NETWORK_FEE_ETH`, `MIN_OPERATOR_ETH_FEE`, `MAX_OPERATOR_ETH_FEE`, `DEFAULT_OPERATOR_ETH_FEE`, liquidation/cooldown/quorum values) - -The file now makes the split explicit: deploy/upgrade source of truth is `deployments//config.json`, while `.env` only carries runtime secrets and optional overrides. - -**Acceptance Criteria:** -- [x] All v1-only params removed or updated -- [x] ETH-specific params added: `NETWORK_FEE_ETH`, `MIN_OPERATOR_ETH_FEE`, `MAX_OPERATOR_ETH_FEE`, `DEFAULT_OPERATOR_ETH_FEE` -- [x] Values match DIP-X spec defaults -- [x] Comments explain each parameter - -**Agent Instructions:** -1. Read `.env.example`. -2. Read `deployments/hoodi-prod/config.json` for reference values. -3. Update the file with v2.0.0 parameters and inline comments. - -#### Sub-items: -- [x] Sub-task 1: Update existing params -- [x] Sub-task 2: Add ETH-specific params -- [x] Sub-task 3: Add inline comments - ---- - -### [OPS-4] Multisig batch transaction method untested in sequential stage/prod/mainnet pipeline -- **Type:** Operational Readiness -- **Priority:** P1 -- **Status:** Open -- **Owner:** (Gabriel / Andrew) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Context:** -On stage and prod, an EOA address owns the SSV Network contract — every upgrade was executed by sending transactions one by one. On mainnet, the plan is to upgrade contracts via multisig batch transactions following these steps: -1. Update `config.json` + `.env` -2. Deploy contracts -3. Create batch-txs JSON file -4. Execute the batch transactions with the DAO's multisig address - -This means a different method is being applied for stage/prod compared to mainnet. The batch transaction method was tested and approved by Gabriel, but it cannot be tested with exactly all the flows. It has not passed the test of time and breaks the rule of sequential exact upgrades on stage -> prod -> mainnet. - -**Acceptance Criteria:** -- [ ] Batch transactions are exactly the same transactions sent on stage/prod -- [ ] Jest commands for building the batch transactions JSON cannot be altered -- [ ] Manual review confirms this meets the correct procedure for upgrading the SSV Network contracts - ---- - -## Echidna Invariant Suite - -**Current state:** 73 invariants across 9 test contracts (see `test/echidna/README.md` for full master list). -**Source:** Evaluated from `ssv-review/planning/SSVNetwork — Enrich Invariant Suite.md` — cross-referenced all 50 proposed invariants against existing 73, identified 30 new + 5 strengthening items. - -### [FUZZ-1] ~~Strengthen 5 partially-covered echidna invariants~~ -- **Type:** Echidna Invariant Suite -- **Priority:** P1 -- **Status:** ✅ Done -- **Owner:** (unassigned) -- **Timeline:** 2026-03-03 -- **Github Link:** (empty) - -**Requirement:** -Upgrade 5 existing invariants from partial to full coverage: -1. `echidna_network_fee_matches_expected` → add explicit monotonicity tracking (ref A8) -2. `echidna_cssv_supply_matches_users` → add per-operation mint/burn delta assertions (ref A11) -3. `echidna_user_index_leq_acc` → strengthen to exact equality after `_settle` (ref A14) -4. `echidna_pool_matches_dao_balance` → add per-claim delta tracking (ref A16) -5. `echidna_accrued_within_pool` → add cumulative payout tracking (ref C2) - -**Resolution:** -Completed in the Echidna harnesses: -- `test/echidna/SSVDAOEchidna.sol`: strengthened network-fee invariants with explicit monotonicity bookkeeping (`prevEthFeeCurrentIndex`, `prevSsvFeeCurrentIndex`) and mutation-time checkpoints. -- `test/echidna/SSVStakingEchidna.sol`: added per-operation cSSV mint/burn delta checks, post-settle exact `userIndex == accEthPerShare` checks, per-claim pool/DAO delta validation, and cumulative ETH credited/paid-out tracking for payout safety. - -Validation run at the time FUZZ-2 landed: -- `echidna test/echidna/SSVStakingEchidna.sol --contract SSVStakingEchidna --config test/echidna/echidna.yaml` (12/12 passing) -- `echidna test/echidna/SSVDAOEchidna.sol --contract SSVDAOEchidna --config test/echidna/echidna.yaml` (13/13 passing) - -**Acceptance Criteria:** -- [x] Each upgraded invariant catches the class of bugs described in the ref -- [x] All echidna tests still pass after modifications -- [x] Harness bookkeeping added (prev-value tracking, per-claim deltas, cumulative payout counter) - ---- - -### [FUZZ-2] ~~Add 16 high-priority new echidna invariants~~ -- **Type:** Echidna Invariant Suite -- **Priority:** P1 -- **Status:** ✅ Done -- **Owner:** (unassigned) -- **Timeline:** 2026-03-03 -- **Github Link:** (empty) - -**Requirement:** -Add 16 new invariants covering critical gaps. Full list with descriptions in `test/echidna/README.md` under "High Priority — New Invariants". Summary: - -**Oracle / EB Governance (3):** Finalized weight cleared (A4), commitment weight ≤ supply (A5), finalization implies quorum (B1) - -**DAO Accounting (2):** DAO earnings monotonicity (A9), DAO index block ≤ current (A10) - -**Staking Rewards Precision (3):** cSSV transfer settles both (A15), claim payout precision (A17), no free rewards on transfer (C3) - -**EB Snapshot Safety (2):** Snapshot block ≤ current (A18), snapshot root monotonic per cluster (A19) - -**EB Update Correctness (3):** Update requires root (B3), frequency enforced (B4), staleness enforced (B5) - -**Fee Settlement (2):** Fee index current after settle (B9), fee uses old vUnits on EB change (B11) - -**Liquidation Completeness (2):** Liquidation clears EB snapshot (B13), liquidation pays exact balance (B14) - -**Resolution:** -Implemented high-priority coverage in the existing harnesses: -- `test/echidna/SSVDAOEchidna.sol`: added oracle/EB governance invariants (`echidna_finalized_weight_cleared`, `echidna_commitment_weight_lte_supply`, `echidna_finalization_implies_quorum`) and DAO accounting invariants (`echidna_dao_earnings_monotonic`, `echidna_dao_index_block_lte_current`) with touched-key and monotonic earnings/index bookkeeping. -- `test/echidna/SSVStakingEchidna.sol`: added staking precision invariants (`echidna_cssv_transfer_settles_both`, `echidna_claim_payout_precision`, `echidna_no_free_rewards_on_transfer`) with transfer-level settlement/accrual checks. -- `test/echidna/SSVClustersEchidna.sol`: added EB snapshot/update/fee/liquidation invariants (`echidna_eb_snapshot_block_lte_current`, `echidna_eb_snapshot_root_monotonic`, `echidna_eb_update_requires_root`, `echidna_eb_update_frequency`, `echidna_eb_update_staleness`, `echidna_fee_index_current_after_settle`, `echidna_fee_uses_old_vunits_on_eb_change`, `echidna_liquidation_clears_eb_snapshot`) and update actions with valid/invalid proof-root scenarios. -- `B14` ("liquidation pays exact balance") remains covered by the pre-existing `echidna_liquidation_cleans_state` payout checks. - -Validation run at the time FUZZ-2 landed: -- `echidna test/echidna/SSVStakingEchidna.sol --contract SSVStakingEchidna --config test/echidna/echidna.yaml` (15/15 passing) -- `echidna test/echidna/SSVDAOEchidna.sol --contract SSVDAOEchidna --config test/echidna/echidna.yaml` (18/18 passing) -- `echidna test/echidna/SSVClustersEchidna.sol --contract SSVClustersEchidna --config test/echidna/echidna.yaml` (17/17 passing) - -**Acceptance Criteria:** -- [x] All 16 invariants implemented and passing -- [x] Harness features added: prev-value tracking, touched-key arrays, 2-actor reward tracking -- [x] Each invariant documented in `test/echidna/README.md` - ---- - -### [FUZZ-3] Add 8 medium-priority echidna invariants -- **Type:** Echidna Invariant Suite -- **Priority:** P2 -- **Status:** Done -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Add 8 medium-priority invariants requiring more harness setup. Full list in `test/echidna/README.md` under "Medium Priority". Summary: - -**EB Proof (3):** Merkle proof verified (B6), EB bounds enforced (B7), snapshot fields exact (B8) - -**Operator Fee Gov (2):** Declare fee from zero reverts (B17), execute rejects legacy declarations (B19) - -**Legacy SSV (1):** SSV liquidation resets and pays (B15) - -**DAO Formula (1):** DAO earnings matches formula exactly (C4) - -**Acceptance Criteria:** -- [x] All 8 invariants implemented and passing -- [x] Merkle tree builder added to harness for valid proof happy paths -- [x] Each invariant documented in `test/echidna/README.md` - ---- - -### [FUZZ-4] Add 6 lower-priority echidna invariants (heavy harness) -- **Type:** Echidna Invariant Suite -- **Priority:** P2 -- **Status:** ✅ Closed -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Add 6 lower-priority invariants requiring significant harness work. Full list in `test/echidna/README.md` under "Lower Priority". Summary: - -**vUnit Aggregation (2):** DAO vUnits = sum of clusters (C5), operator vUnits matches clusters (C6) - -**Migration (1):** Migration one-way and returns SSV (C7) - -**Overflow/Extreme (3):** ETH accrual no overflow (X4), SSV accrual no overflow (X5), intermediate mul no overflow (X6), pack reverts on overflow (X7) - -**Acceptance Criteria:** -- [x] All invariants implemented and passing -- [x] Delta-block simulator added for overflow testing (`action_probe_max_eth_accrual`, `action_probe_max_ssv_accrual`) -- [x] Max-parameter configurator added (uses `sp.operatorMaxFee`, `sp.validatorsPerOperatorLimit`) -- [x] Per-cluster EB tracking arrays already present in `SSVAccountingEchidna` (`ethClusterIds`) -- [x] Each invariant documented in `test/echidna/README.md` - -**Resolution:** -- C5 (`echidna_vunits_deviation_consistent`): already existed in `SSVAccountingEchidna.sol` -- C6 (`echidna_operator_vunits_matches_clusters`): added to `SSVAccountingEchidna.sol` — sums cluster deviations per operator and compares to `operatorEthVUnits[opId]` -- C7 (`echidna_migration_one_way`): added to `SSVAccountingEchidna.sol` — tracks migrated clusters, asserts `s.clusters[cId] == 0` and `s.ethClusters[cId] != 0` after migration -- X4 (`echidna_eth_accrual_no_overflow`): added to `SSVEdgeCasesEchidna.sol` — `action_probe_max_eth_accrual` sets max fee/validators/EB and advances blocks; invariant checks balance is monotonic -- X5 (`echidna_ssv_accrual_no_overflow`): added to `SSVAccountingEchidna.sol` — same pattern for SSV -- X6 (`echidna_intermediate_mul_no_overflow`): added to `SSVEdgeCasesEchidna.sol` — view invariant asserting `maxFee * maxEffectiveVUnits <= type(uint128).max` -- X7 (`echidna_pack_reverts_on_overflow`): added to `SSVEdgeCasesEchidna.sol` — `action_pack_overflow_check` probes `pack(type(uint256).max)` and asserts it reverts - ---- - -### [FUZZ-5] ~~ETH contract balance accounting invariant~~ -- **Type:** Echidna Invariant Suite -- **Priority:** P1 -- **Status:** ✅ Done -- **Owner:** (unassigned) -- **Timeline:** 2026-03-03 -- **Github Link:** (empty) - -**Requirement:** -Add an Echidna invariant that continuously asserts the ETH accounting identity: - -``` -address(this).balance == Σ(cluster.balance) + Σ(operator.ethEarnings) + ethDaoBalance + stakingEthPoolBalance -``` - -**Context:** -Product raised the question of whether `withdraw` needs an explicit `amount <= address(this).balance` guard. The answer is: not as a runtime check — if accounting is correct, `cluster.balance` is always ≤ `address(this).balance` by construction. However, this invariant should be continuously enforced by fuzzing to catch any accounting divergence (rounding errors, missed fee settlement paths, ETH drain via another function). A violation means a protocol bug, not a user error. See FLOWS.md §1.8 for the full rationale. - -**Resolution:** -- Implemented `echidna_eth_balance_accounting` in `test/echidna/SSVClustersEchidna.sol`. -- Invariant enforces: `address(this).balance >= totalExpectedBalance + sumTrackedOperatorEthEarnings + ethDaoBalance + stakingEthPoolBalance`. -- Added supporting bookkeeping helpers in the cluster harness to sum tracked operator ETH earnings (`op1/op2/op3`), DAO ETH balance, and staking ETH pool balance. -- Extended the harness with real staking/operator actors plus `action_stake`, `action_claim_rewards`, and `action_withdraw_operator_eth` so the invariant is exercised across non-cluster ETH outflow paths as well. -- Updated `test/echidna/README.md` invariant inventory: `SSVClustersEchidna` now documents 18 invariants, including `echidna_eth_balance_accounting`. - -**Validation run:** -- `echidna test/echidna/SSVClustersEchidna.sol --contract SSVClustersEchidna --config test/echidna/echidna.yaml` (18/18 passing) -- `echidna test/echidna/SSVClustersEchidna.sol --contract SSVClustersEchidna --config test/echidna/echidna.yaml --seed 8525641213984558505` (18/18 passing) -- `echidna test/echidna/SSVClustersEchidna.sol --contract SSVClustersEchidna --config test/echidna/echidna.yaml --seed 985768268619296310` (18/18 passing) - -**Acceptance Criteria:** -- [x] Echidna invariant `echidna_eth_balance_accounting` implemented in the staking/cluster harness -- [x] Invariant asserts `address(this).balance >= sum_of_all_cluster_balances + sum_of_operator_eth_earnings + ethDaoBalance + stakingEthPoolBalance` after every operation -- [x] Harness exercises cluster, operator, and staking ETH outflow paths across `deposit` / `withdraw` / `liquidate` / `reactivate` / `stake` / `claimEthRewards` / `withdrawOperatorEarnings` -- [x] No invariant violations in fuzz runs - -**Agent Instructions:** -1. Read `test/echidna/` for existing harness patterns and how cluster/operator state is tracked. -2. Add a new invariant function that sums all tracked cluster balances and operator ETH earnings and compares to `address(this).balance`. -3. Ensure the harness exercises all ETH-moving operations exposed in the current codebase: `deposit`, `withdraw`, `liquidate`, `reactivate`, `stake`, `claimEthRewards`, `withdrawOperatorEarnings`. -4. Run Echidna and confirm no violations. - -#### Sub-items: -- [x] Sub-task 1: Implement `echidna_eth_balance_accounting` invariant -- [x] Sub-task 2: Extend harness to track all ETH-moving operations -- [x] Sub-task 3: Run Echidna and confirm no violations - ---- - -## Code Quality - -### [QUALITY-1] `operatorFeeChangeRequests` not cleared on operator removal -- **Type:** Code Quality -- **Priority:** P2 -- **Status:** Open -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Clear `operatorFeeChangeRequests[operatorId]` in `_resetOperatorState` when an operator is removed. - -**Context:** -In `SSVOperators.sol:324-335`, `_resetOperatorState` doesn't delete stale fee change requests for the removed operator. No functional impact since `declareOperatorFee` and `executeOperatorFee` both check `checkOwner()` first (which reverts for removed operators), but the stale data wastes storage and could confuse off-chain readers querying operator fee change requests. - -**Acceptance Criteria:** -- [ ] `delete s.operatorFeeChangeRequests[operatorId]` added to `_resetOperatorState` -- [ ] Existing removal tests pass -- [ ] New test: declare fee change, remove operator, verify fee change request is cleared - -#### Sub-items: -- [ ] Sub-task 1: Add fee change request cleanup to `_resetOperatorState` -- [ ] Sub-task 2: Add test verifying cleanup -- [ ] Sub-task 3: Run full test suite - ---- - -### [QUALITY-2] ~~Redundant `SSVStorage.load()` calls in view function loops~~ -- **Type:** Code Quality -- **Priority:** P2 -- **Status:** ✅ Fixed -- **Owner:** (resolved) -- **Timeline:** (complete) -- **Github Link:** (empty) - -**Resolution:** -Hoisted `SSVStorage.load()` to a single pre-loop `StorageData storage s` in all affected functions in `SSVViews.sol`: `isLiquidatable`, `isLiquidatableSSV`, `getBurnRate`, `getBurnRateSSV`, `getBalance`, `getBalanceSSV` (redundant in-loop calls), and `getOperatorById`, `getOperatorByIdSSV` (redundant double-load for whitelist access). Also fixed `getOperatorFeePeriods` which called `SSVStorageProtocol.load()` twice. All 516 unit tests pass. - -**Acceptance Criteria:** -- [x] `SSVStorage.load()` called once before each loop, stored in a local variable -- [x] Same pattern applied to `SSVStorageProtocol.load()` where it had the same issue -- [x] Existing view tests pass with identical return values - -#### Sub-items: -- [x] Sub-task 1: Identify all redundant `load()` calls in loops -- [x] Sub-task 2: Hoist to pre-loop variables -- [x] Sub-task 3: Run full test suite - ---- - -### [QUALITY-3] ~~`withdraw` in SSVClusters duplicates operator loop inline~~ -- **Type:** Code Quality -- **Priority:** P2 -- **Status:** ✅ Fixed -- **Owner:** (resolved) -- **Timeline:** (complete) -- **Github Link:** (empty) - -**Resolution:** -Fixed the immediate issue: `SSVClusters.withdraw()` was calling `SSVStorage.load()` on every loop iteration despite `s` already being loaded at the top of the function. Changed `SSVStorage.load().operators[operatorIds[i]]` to `s.operators[operatorIds[i]]`. The larger refactor (extracting the loop into a shared `OperatorLib` helper) was scoped out as it would require a more invasive interface change across multiple callers; the redundant-load bug is the actionable fix. All 516 unit tests pass. - -**Acceptance Criteria:** -- [x] Redundant `SSVStorage.load()` inside loop eliminated — uses already-loaded `s` -- [x] Behavior is identical before and after -- [x] All withdrawal tests pass - -#### Sub-items: -- [x] Sub-task 1: Replace `SSVStorage.load()` in loop with already-loaded `s` -- [x] Sub-task 2: Run full test suite - ---- - -### [QUALITY-4] `_resetOperatorState` returns unused `Operator memory` -- **Type:** Code Quality -- **Priority:** P3 -- **Status:** Open -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Remove the unused return value from `_resetOperatorState` to save gas. - -**Context:** -In `SSVOperators.sol:324`, `_resetOperatorState` returns `Operator memory` but the caller at line 82 discards the return value. The unnecessary SLOAD to populate the return struct wastes ~2100 gas per operator removal. - -**Acceptance Criteria:** -- [ ] `_resetOperatorState` changed to return `void` (no return value) -- [ ] Caller at line 82 updated to not expect a return value -- [ ] Existing operator removal tests pass - -#### Sub-items: -- [ ] Sub-task 1: Remove return value from `_resetOperatorState` -- [ ] Sub-task 2: Update caller -- [ ] Sub-task 3: Run full test suite - ---- - -### [QUALITY-5] ~~Remove duplicate `MaxValueExceeded` error declaration~~ -- **Type:** Code Quality -- **Priority:** P3 -- **Status:** ✅ Fixed -- **Owner:** (resolved) -- **Timeline:** (complete) -- **Github Link:** (empty) - -**Requirement:** -Remove the duplicate `MaxValueExceeded` error declaration that appears in both `ISSVNetworkCore.sol` and `SSVPackedLib.sol`, causing duplication in the generated ABI. - -**Context:** -The `MaxValueExceeded` error is declared in two places: -1. `ISSVNetworkCore.sol:205` - `error MaxValueExceeded(); // 0x91aa3017` -2. `SSVPackedLib.sol:10` - `error MaxValueExceeded();` - -This duplication results in the same error appearing twice in the generated ABI (`SSVNetwork.json:229-238`), which can cause confusion for tooling and integrations that expect unique error signatures. - -**Resolution:** -Removed the duplicate `error MaxValueExceeded()` from `PackingLib` in `SSVPackedLib.sol`. Added `import {ISSVNetworkCore} from "../interfaces/ISSVNetworkCore.sol"` and changed the revert to `revert ISSVNetworkCore.MaxValueExceeded()`. The canonical declaration remains in `ISSVNetworkCore.sol` where `ProtocolLib.sol` already references it. Both had identical selector `0x91aa3017`, so no ABI change. All 1188 tests pass. - -**Acceptance Criteria:** -- [x] Remove duplicate `MaxValueExceeded` declaration from `SSVPackedLib.sol` -- [x] Keep the declaration in `ISSVNetworkCore.sol` (canonical location for all protocol errors) -- [x] Verify the generated ABI no longer has duplicate entries -- [x] Ensure all existing tests still pass -- [x] Confirm no contracts rely on the specific error signature from the removed location - -#### Sub-items: -- [x] Sub-task 1: Determine which file should keep the `MaxValueExceeded` declaration — `ISSVNetworkCore.sol` -- [x] Sub-task 2: Remove the duplicate declaration from `SSVPackedLib.sol`, import interface, update revert -- [x] Sub-task 3: Verify compilation and ABI -- [x] Sub-task 4: Run full test suite to ensure no regressions - ---- - -### [BUG-10] Stale Merkle root vulnerability in `updateClusterBalance` -- **Type:** Critical Bug Fix -- **Priority:** P1 -- **Status:** Open -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -Fix the vulnerability where `updateClusterBalance` can accept stale Merkle roots when `minBlocksBetweenUpdates != 0`, allowing malicious actors to delay effective balance updates. - -**Context:** -In `SSVClusters.sol:353-371`, the `updateClusterBalance` function validates Merkle proofs against the current oracle root. However, if a cluster's effective balance hasn't changed for a long time, there's no incentive to call `updateClusterBalance` for that cluster. A malicious actor could intentionally use an old Merkle root to delay updating to the most recent effective balance when `minBlocksBetweenUpdates != 0`. - -**Vulnerability Details:** -1. The function validates the Merkle proof against the current oracle root -2. If `minBlocksBetweenUpdates > 0`, updates are rate-limited -3. For clusters with unchanged effective balances, no one calls `updateClusterBalance` -4. An attacker can submit stale proofs using old roots to prevent EB updates -5. This allows manipulation of when effective balance changes take effect - -**Current Mitigation:** -The issue is currently mitigated because `minBlocksBetweenUpdates` is always set to 0, meaning there's no rate limiting on updates. However, if the protocol intends to enable rate limiting in the future, this vulnerability becomes active. - -**Acceptance Criteria:** -- [ ] Product team confirms whether `minBlocksBetweenUpdates` will be enabled in future -- [ ] If yes: Implement validation to prevent stale Merkle root usage -- [ ] Consider adding a timestamp/block number check to ensure proofs use recent roots -- [ ] Add test coverage for this scenario -- [ ] Document the expected behavior when `minBlocksBetweenUpdates > 0` - -#### Sub-items: -- [ ] Sub-task 1: Confirm product requirements for `minBlocksBetweenUpdates` -- [ ] Sub-task 2: Design solution to prevent stale Merkle root usage -- [ ] Sub-task 3: Implement the fix -- [ ] Sub-task 4: Add comprehensive test coverage -- [ ] Sub-task 5: Update documentation - ---- - -### [BUG-11] Remove liquidation check in `withdraw` function -- **Type:** Code Quality -- **Priority:** P2 -- **Status:** ✅ Fixed -- **Owner:** (unassigned) -- **Timeline:** (complete) -- **Github Link:** (empty) - -**Requirement:** -Remove the `cluster.validateClusterIsNotLiquidated()` check from the `withdraw` function in `SSVClusters.sol`. - -**Context:** -In `SSVClusters.sol:215`, the `withdraw` function prevents withdrawals from liquidated clusters. This restriction is unnecessarily restrictive: users may deposit funds to prepare a liquidated cluster for reactivation but later decide not to reactivate. In this scenario, they should be able to withdraw their deposited funds without being forced to complete the reactivation. The liquidation check should be removed to allow this flexibility. - -**Rationale:** -- Users can deposit to liquidated clusters (allowed by design, see SEC-12) -- If users change their mind about reactivation, they should be able to retrieve their deposits -- The balance accounting is correct whether the cluster is liquidated or not -- **IMPORTANT:** Double-check this change with Product team before implementation to ensure it aligns with intended UX - -**Acceptance Criteria:** -- [x] Product team approval obtained for this change -- [x] Remove `cluster.validateClusterIsNotLiquidated()` from `withdraw` function (line 215) -- [x] Add test: deposit to liquidated cluster, then withdraw without reactivating -- [x] Verify existing withdrawal tests still pass -- [x] Update FLOWS.md to document that withdrawals are allowed on liquidated clusters - -#### Sub-items: -- [x] Sub-task 1: Get Product team approval -- [x] Sub-task 2: Remove `cluster.validateClusterIsNotLiquidated()` from `SSVClusters.sol:withdraw` (was line 215) -- [x] Sub-task 3: Added tests: `withdraw.test.ts` — "Withdraws deposited funds from a liquidated cluster without reactivating" and "Withdraws full balance from a liquidated cluster that received multiple deposits" -- [x] Sub-task 4: Updated `docs/FLOWS.md` §1.8 preconditions to explicitly allow liquidated clusters - ---- - -### [BUG-12] `removeValidator` / `bulkRemoveValidator` blocked for legacy SSV clusters -- **Type:** Critical Bug Fix -- **Priority:** P1 -- **Status:** ✅ Done (Product approved) -- **Owner:** (resolved) -- **Timeline:** (complete) -- **Github Link:** (empty) - -**Requirement:** -Allow `removeValidator` and `bulkRemoveValidator` to operate on legacy SSV clusters, not just ETH clusters. - -**Context:** -`_bulkRemoveValidator` in `SSVValidators.sol:177` calls `ClusterLib.validateClusterVersion(version, VERSION_ETH)`, which reverts with `IncorrectClusterVersion` for any SSV cluster. This means owners of legacy SSV clusters cannot remove individual validators — they can only exit (signal off-chain) or migrate the entire cluster to ETH. This is a UX regression from v1.x where `removeValidator` worked on all clusters. - -The SSV cluster removal path is distinct from the ETH path in two ways: -1. It uses `s.clusters` (SSV storage) instead of `s.ethClusters` -2. It does not involve ETH snapshot updates or EB deviation cleanup - -The fix requires branching `_bulkRemoveValidator` on `version`: for `VERSION_SSV`, use the legacy SSV cluster removal path (update SSV operator snapshots, decrement `operator.validatorCount`, update SSV cluster hash in `s.clusters`); for `VERSION_ETH`, keep the existing ETH path. - -**Rationale:** -- SSV cluster owners may want to remove specific validators without migrating the entire cluster -- Without this, the only way to reduce validator count in a legacy cluster is full migration -- The FLOWS.md and SPEC.md already document SSV cluster operations as including `removeValidator` (see FLOWS §1.10, SPEC §1 "Existing Clusters") -- **IMPORTANT:** Confirm with Product team whether this is intentionally blocked or an oversight - -**Acceptance Criteria:** -- [x] Product team approval obtained -- [x] `_bulkRemoveValidator` branches on `version`: `VERSION_SSV` uses SSV cluster path, `VERSION_ETH` uses ETH cluster path -- [x] SSV path: updates SSV operator snapshots (`operator.snapshot`), decrements `operator.validatorCount`, updates `s.clusters[hashedCluster]` -- [x] SSV path: does NOT touch ETH snapshots, `ethValidatorCount`, `ethClusters`, or EB storage -- [x] Add test: remove validator from active SSV cluster, verify SSV cluster hash updated and operator count decremented -- [x] Add test: remove validator from liquidated SSV cluster (should be allowed — no active-cluster check in current code) -- [x] Existing ETH removal tests still pass -- [x] Update FLOWS §1.3 and §1.4 to document SSV cluster support - -#### Sub-items: -- [x] Sub-task 1: Get Product team approval -- [x] Sub-task 2: Branch `_bulkRemoveValidator` on cluster version -- [x] Sub-task 3: Implement SSV cluster removal path -- [x] Sub-task 4: Add unit tests -- [x] Sub-task 5: Update FLOWS.md §1.3 and §1.4 - ---- - -### [BUG-13] Silent default ETH fee assignment for legacy operators during migration -- **Type:** Observability Fix -- **Priority:** P2 -- **Status:** ✅ Fixed -- **Owner:** Claude Code -- **Timeline:** 2026-03-04 -- **Github Link:** [PR #502](https://github.com/ssvlabs/ssv-network/pull/502) - -**Requirement:** -Emit `OperatorFeeExecuted` event when legacy SSV operators receive the default ETH fee (1_770_000_000 wei/vUnit/block) during migration to ETH operations. - -**Context:** -When legacy SSV operators (operators with `operator.ethSnapshot.block == 0` and `operator.fee != 0`) first interact with ETH clusters (via `registerValidator`, `migrateClusterToETH`, or `declareOperatorFee`), the `ensureETHDefaults` function in `OperatorLib.sol` automatically assigns `DEFAULT_OPERATOR_ETH_FEE` to `operator.ethFee`. Previously, this assignment was silent — no event was emitted. - -This created an observability gap for indexers and offchain services: -- No way to track when operators receive default ETH fees -- Difficult to distinguish between default fee assignment and explicit fee declarations -- Indexers had to infer fee values from storage rather than events - -**Solution (PR #502):** -Modified `ensureETHDefaults` to: -1. Accept `operatorId` as a parameter (previously had no params) -2. Emit `OperatorFeeExecuted(operator.owner, operatorId, block.number, DEFAULT_OPERATOR_ETH_FEE)` when assigning default fee -3. Updated all callsites to pass `operatorId`: - - `OperatorLib.updateClusterOperatorsOnRegistration` (line 201) - - `OperatorLib.updateClusterOperatorsMigration` (line 396) - - `SSVOperators.declareOperatorFee` (line 107) - -**Code Changes:** -- `contracts/libraries/OperatorLib.sol:143`: Modified function signature and added event emission -- `contracts/libraries/OperatorLib.sol:201,396`: Updated callsites -- `contracts/modules/SSVOperators.sol:107`: Updated callsite - -**Benefits:** -- ✅ Indexers can track all operator fee changes via events (consistent observability) -- ✅ Backward compatible (reuses existing `OperatorFeeExecuted` event signature) -- ✅ Idempotent (event emitted only once per operator due to `ethSnapshot.block` guard) -- ✅ Bug fix bonus: Removed duplicate `if (operator.ethSnapshot.block == 0)` check - -**Security Analysis:** -- ✅ No vulnerabilities (LOW risk) -- ✅ Idempotency guaranteed (guard prevents re-execution) -- ✅ State consistency (event emitted after state changes) -- ✅ No reentrancy risk (internal function, no external calls) -- ✅ Event parameters trustworthy (`operator.owner`, `operatorId`, `block.number`, constant) - -**Test Coverage:** -- ✅ Migration path: [migrateClusterToETH.test.ts:101-132](test/unit/SSVClusters/migrateClusterToETH.test.ts#L101-L132) -- ✅ Register validator path: [registerValidator.test.ts:65-81](test/unit/SSVValidator/registerValidator.test.ts#L65-L81) -- ✅ Declare fee path: [declareOperatorFee.test.ts:140-158](test/unit/SSVOperators/declareOperatorFee.test.ts#L140-L158) -- ✅ Idempotency: [migrateClusterToETH.test.ts:134-197](test/unit/SSVClusters/migrateClusterToETH.test.ts#L134-L197) — NEW TEST - -**Acceptance Criteria:** -- [x] `ensureETHDefaults` emits `OperatorFeeExecuted` when assigning default ETH fee to legacy operators -- [x] Event parameters correct: `(operator.owner, operatorId, block.number, DEFAULT_OPERATOR_ETH_FEE)` -- [x] Event emitted only once per operator (idempotent) -- [x] All three call paths tested (migration, register, declare) -- [x] Idempotency test added -- [x] Security analysis confirms LOW risk -- [x] Backward compatible (no event signature changes) -- [x] Gas impact acceptable (~1500 gas per operator, one-time) - -#### Sub-items: -- [x] Modify `ensureETHDefaults` to accept `operatorId` and emit event -- [x] Update all callsites (3 locations) -- [x] Add idempotency test -- [x] Security review (ssv-bug-fixer) -- [x] Test coverage review (ssv-test-writer) - ---- - -### [BUG-14] Removed operator SSV fees skipped during `migrateClusterToETH` fee settlement (double-payment) -- **Type:** Critical Bug Fix -- **Priority:** P1 -- **Status:** ⚠️ Open -- **Owner:** (unassigned) -- **Timeline:** (empty) -- **Github Link:** (empty) - -**Requirement:** -When migrating an SSV cluster to ETH, SSV fee settlement must include fee debt already accrued by operators that were removed before migration. - -**Context:** -`migrateClusterToETH` settles SSV balance using `cluster.updateBalanceSSV(clusterIndexSSV, sp.currentNetworkFeeIndexSSV())`, where `clusterIndexSSV` is returned by `OperatorLib.updateClusterOperatorsMigration`. - -In `updateClusterOperatorsMigration`, removed operators are skipped entirely: -- `if (operator.snapshot.block == 0 && operator.ethSnapshot.block == 0) continue;` - -If operator A is removed after accruing SSV fees: -1. `removeOperator` settles and pays A's SSV snapshot to A's owner. -2. Migration later skips A, so A's accrued index contribution is not included in `clusterIndexSSV`. -3. Cluster SSV usage is under-counted during migration. -4. Cluster owner receives inflated SSV refund. - -This creates an economic double-payment pattern: once to the removed operator owner, and again via inflated migration refund. - -**Reproduction (implemented):** -- `test/e2e/migration/migration-double-payment.test.ts` - - Test: `"Demonstrates double-payment with exact accounting: remove payout + inflated migration refund"` - - Uses exact formula assertions for expected correct refund vs actual buggy refund. - -**Acceptance Criteria:** -- [ ] Migration SSV settlement includes fee debt from removed operators that were part of the SSV cluster history -- [ ] Cluster owner migration refund equals exact expected amount from SPEC/FLOWS formulas (no under-deduction) -- [ ] No operator can be paid twice for the same SSV fee accrual window (direct earnings + inflated cluster refund) -- [ ] Regression test remains green and fails on old behavior: - - `test/e2e/migration/migration-double-payment.test.ts` - -**Agent Instructions:** -1. Read `contracts/libraries/OperatorLib.sol:updateClusterOperatorsMigration`. -2. Read `contracts/modules/SSVClusters.sol:migrateClusterToETH` SSV settlement path. -3. Ensure migration SSV settlement accounts for removed-operator historical debt correctly. -4. Keep existing valid behavior where removed operators do not receive new post-removal accrual. -5. Run targeted tests and `npm run test:unit`. - -#### Sub-items: -- [ ] Sub-task 1: Fix migration SSV fee-settlement accounting for removed operators -- [ ] Sub-task 2: Keep/extend exact-formula reproduction test -- [ ] Sub-task 3: Run unit + e2e migration suites - ---- - -### [BUG-14b] `reduceOperatorFee` / `declareOperatorFee` overwrite explicit zero ETH fees for legacy SSV operators -- **Type:** Critical Bug Fix -- **Priority:** P1 -- **Status:** ✅ Fixed -- **Owner:** Claude Code -- **Timeline:** 2026-03-06 -- **Github Link:** (embedded in `ssv-staking` branch, commit `8185b1c`) - -**Requirement:** -Allow legacy SSV operators (SSV fee > 0) to explicitly set ETH fee = 0 and preserve this choice during cluster migration and fee operations. - -**Context:** -When a legacy SSV operator (registered pre-v2.0.0) with SSV fee > 0 calls `reduceOperatorFee` or `declareOperatorFee` to set `ethFee = 0`, the system should remember this explicit choice. Previously, `ensureETHDefaults` could not distinguish between: - -1. **"Never set ETH fee"** (should get `DEFAULT_OPERATOR_ETH_FEE`) -2. **"Explicitly set ETH fee to zero"** (should keep zero) - -Both states resulted in `ethFee == 0 && ethSnapshot.block == 0`, causing `ensureETHDefaults` to overwrite explicit zero fees with `DEFAULT_OPERATOR_ETH_FEE` during subsequent operations (like cluster migration). - -**Root Cause:** -`reduceOperatorFee` and `declareOperatorFee` did not initialize `ethSnapshot.block` before updating fees, leaving the operator in an "uninitialized" state even after explicit fee changes. - -**Solution (ethSnapshot.block marker pattern):** - -1. **Marker Logic:** Use `ethSnapshot.block > 0` as a marker indicating "operator has explicitly interacted with ETH fee system" - -2. **Code Changes:** - - `SSVOperators.reduceOperatorFee` (line 187-189): Added `ensureETHDefaults` call if `ethSnapshot.block == 0` - - `SSVOperators.declareOperatorFee` (line 106-108): Already had `ensureETHDefaults` call - - `OperatorLib.ensureETHDefaults` (line 144-152): Only assigns default if `ethSnapshot.block == 0 && ethFee == 0 && SSV fee > 0` - -3. **Flow:** - - **First ETH interaction** (ethSnapshot.block == 0): - - Call `ensureETHDefaults` - - If SSV fee > 0: assigns `ethFee = DEFAULT_OPERATOR_ETH_FEE` - - Sets `ethSnapshot.block = block.number` (marker) - - Operator can then reduce to any value (including 0) - - - **Subsequent operations** (ethSnapshot.block > 0): - - `ensureETHDefaults` sees marker and **skips** (no overwrite) - - Explicit zero fees preserved during migration - -**Acceptance Criteria:** -- [x] `reduceOperatorFee` calls `ensureETHDefaults` before updating fee -- [x] `declareOperatorFee` calls `ensureETHDefaults` before declaring new fee -- [x] `ethSnapshot.block > 0` prevents `ensureETHDefaults` from overwriting explicit fees -- [x] Legacy SSV operator can set `ethFee = 0` via `reduceOperatorFee(operatorId, 0)` -- [x] Migration respects explicit zero fees (no overwrite to default) -- [x] Comprehensive test suite (15 unit tests + 3 E2E tests) -- [x] Documentation updated (SPEC.md §1, FLOWS.md §4.3 & §4.5) - -**Code Changes:** -- `contracts/modules/SSVOperators.sol:187-189` — Added `ensureETHDefaults` call in `reduceOperatorFee` -- `contracts/test/harness/SSVOperatorsHarness.sol:103-123` — Added mock functions for testing -- `test/unit/SSVOperators/reduceOperatorFee-ethSnapshot-init.test.ts` — **15 comprehensive tests (ALL PASSING)** -- `test/e2e/operators/operator-lifecycle.test.ts:582-699` — **3 integration tests** -- `docs/SPEC.md:257-279` — Documented `ensureETHDefaults` behavior -- `docs/FLOWS.md:631-704` — Updated operator fee flows - -**Test Coverage:** -- ✅ ethSnapshot initialization on first `reduceOperatorFee` -- ✅ Legacy SSV operator gets default fee before reduction -- ✅ Legacy SSV operator can reduce to zero (explicit zero fee) -- ✅ Zero-fee operator (SSV fee = 0) stays at zero -- ✅ `ethSnapshot.block > 0` prevents overwrite during migration -- ✅ Fee validation (too low, too high, same value) -- ✅ Event emission (dual events when default assigned) -- ✅ E2E: explicit zero fee preserved across operations - -**Benefits:** -- ✅ **Operator autonomy:** Operators can offer free ETH service while maintaining SSV presence -- ✅ **Predictable fees:** Cluster owners know exact fees during migration -- ✅ **Backward compatible:** No storage changes, uses existing field as marker -- ✅ **No gas overhead:** Initialization happens once per operator -- ✅ **Consistent behavior:** Same pattern across all fee operations - -**Security Analysis:** -- ✅ No vulnerabilities (LOW risk) -- ✅ Idempotency guaranteed (`ethSnapshot.block` guard) -- ✅ State consistency (marker set atomically with default assignment) -- ✅ No reentrancy risk (internal function, state writes before external calls) -- ✅ Marker cannot be manipulated (contract-controlled) - -**Documentation:** -- ✅ SPEC.md §1 "Operator Fee Transition" — Complete `ensureETHDefaults` behavior -- ✅ FLOWS.md §4.3 "Declare Operator Fee" — State mutations and events -- ✅ FLOWS.md §4.5 "Reduce Operator Fee" — Special cases and postconditions - -**Related Issues:** -- BUG-13: Event emission for default fee assignment (PR #502) — Complementary fix -- SEC-16b: Similar pattern (using storage field as marker for explicit behavior) - -#### Sub-items: -- [x] Add `ensureETHDefaults` call to `reduceOperatorFee` -- [x] Create comprehensive test suite (15 unit tests) -- [x] Add E2E integration tests (3 tests) -- [x] Update SPEC.md and FLOWS.md documentation -- [x] Verify all tests passing (18/18 tests ✅) -- [x] Document marker pattern and behavior - ---- - -### [BUG-15] `withdrawAllVersionOperatorEarnings` initializes ETH snapshot for legacy SSV-only operators -- **Type:** Critical Bug Fix -- **Priority:** P1 -- **Status:** ✅ Fixed -- **Owner:** Claude Code -- **Timeline:** 2026-03-12 -- **Github Link:** (embedded in `ssv-staking` branch) - -**Requirement:** -Fix `withdrawAllVersionOperatorEarnings` so it settles SSV and ETH earnings independently and never initializes ETH state for a legacy SSV-only operator. - -**Context:** -The previous implementation loaded the operator into memory, called `updateSnapshots(operatorId)`, then wrote the full struct back to storage. That helper always advanced `ethSnapshot.block`, even when the operator was legacy SSV-only with: -- `fee != 0` -- `ethFee == 0` -- `snapshot.block != 0` -- `ethSnapshot.block == 0` - -This created the inconsistent state `ethSnapshot.block != 0 && ethFee == 0` without any ETH-specific operator action. Once created, later migration logic treated the operator as already ETH-initialized and preserved the zero ETH fee. - -**Vulnerability Details:** -When `withdrawAllVersionOperatorEarnings` is called, the function should behave like `_withdrawOperatorEarnings` for each version separately, but without checking a requested `amount`: - -- If the operator has `snapshot.block != 0`: - - `OperatorLib.updateSnapshotStSSV(operator);` - - `PackedSSV ssvBalance = operator.snapshot.balance;` - - `operator.snapshot.balance = PACKED_SSV_ZERO;` -- If the operator has `ethSnapshot.block != 0`: - - `OperatorLib.updateSnapshotSt(operator, operatorId);` - - `PackedETH ethBalance = operator.ethSnapshot.balance;` - - `operator.ethSnapshot.balance = PACKED_ETH_ZERO;` - -The bug was that the combined `updateSnapshots` helper ignored version separation and unconditionally wrote a fresh ETH snapshot block into legacy SSV-only operator state. - -**Resolution:** -- `SSVOperators.withdrawAllVersionOperatorEarnings` now uses a storage reference and settles the SSV and ETH branches independently. -- `OperatorLib.updateSnapshots` was removed because this mixed-version memory helper was only used by the buggy path. -- `OperatorLib.updateSnapshotsSt` was kept unchanged pending broader review of its remaining call sites. - -**Acceptance Criteria:** -- [x] `withdrawAllVersionOperatorEarnings` only updates SSV snapshot when `snapshot.block != 0` -- [x] `withdrawAllVersionOperatorEarnings` only updates ETH snapshot when `ethSnapshot.block != 0` -- [x] Legacy SSV-only operators keep `ethSnapshot.block == 0` after `withdrawAllVersionOperatorEarnings` -- [x] ETH and SSV balances still withdraw correctly for operators with initialized state -- [x] Unit test added for the legacy SSV-only path - -**Code Changes:** -- `contracts/modules/SSVOperators.sol` — Inlined per-version settlement logic in `withdrawAllVersionOperatorEarnings` -- `contracts/libraries/OperatorLib.sol` — Removed obsolete `updateSnapshots` helper -- `test/unit/SSVOperators/withdrawAllVersionOperatorEarnings.test.ts` — Added legacy SSV-only regression coverage - -#### Sub-items: -- [x] Inline per-version settlement logic in `withdrawAllVersionOperatorEarnings` -- [x] Remove obsolete `OperatorLib.updateSnapshots` -- [x] Add unit test for legacy SSV-only withdrawal behavior -- [ ] Run broader suite if needed - ---- - -### [BUG-17] `commitRoot` quorum can become unreachable due to truncation in per-oracle weight math -- **Type:** Critical Bug Fix -- **Priority:** P0 -- **Status:** Open -- **Owner:** (unassigned) -- **Timeline:** Before mainnet launch -- **Github Link:** (empty) - -**Requirement:** -Fix `commitRoot` so that the configured oracle quorum remains reachable even when the frozen cSSV supply for a voting round is not divisible by the oracle count. - -**Context:** -`commitRoot` freezes `cSSV.totalSupply()` on the first vote of a `(blockNum, merkleRoot)` round to prevent inter-vote supply drift. That mitigation is correct and must remain in place. However, the function then computes: -- `weight = totalStaked / defaultOracleIds.length` -- `threshold = (totalStaked * quorumBps) / 10_000` - -This mixes two separately-truncated quantities. With 4 oracle slots and 75% quorum, if the frozen supply is `4q + 2` or `4q + 3`, three votes accumulate only `3q` weight while the threshold becomes `3q + 1`, so 3-of-4 consensus is mathematically unreachable. At 100% quorum, even 4 votes fail whenever the frozen supply is not divisible by 4. - -This is distinct from the already-mitigated front-running issue tracked in SEC-5. Freezing supply removes the moving-target quorum problem between votes; it does not remove truncation mismatch inside the fixed round arithmetic. - -**Vulnerability Details:** -- The bug is present in `contracts/modules/SSVDAO.sol` where vote weight and threshold are derived from the same frozen supply but rounded in different ways. -- The current specs mirror the same arithmetic, so documentation does not currently protect against the edge case. -- A minimal regression test now demonstrates the issue in `test/unit/SSVDAO/commitRoot.test.ts`: with `totalSupply = 1_000_000_002` and `quorumBps = 7500`, the third oracle vote should commit under intended 3-of-4 semantics, but does not. - -**Proposed Fix:** -Keep the `token weight` model, but normalize the frozen supply once on the first vote of the round and store the truncated voting supply in `roundFrozenSupply`: - -```solidity -uint256 oracleCount = s.defaultOracleIds.length; -uint256 rawSupply = ICSSVToken(CSSV_ADDRESS).totalSupply(); -if (rawSupply == 0) revert ZeroCSSVSupply(); - -uint256 totalStaked = rawSupply - (rawSupply % oracleCount); -if (totalStaked == 0) revert InsufficientCSSVSupply(); - -seb.roundFrozenSupply[commitmentKey] = totalStaked; - -uint256 weight = totalStaked / oracleCount; -seb.rootCommitments[commitmentKey] += weight; -uint256 threshold = (totalStaked * s.quorumBps) / BPS_DENOMINATOR; -``` - -This preserves: -- `token weight`-based quorum math -- current storage layout and event shape -- frozen per-round vote math using one stored value for all later votes -- current behavior where quorum updates between votes affect the next vote - -It also removes the truncation mismatch by ensuring both `weight` and `threshold` use the same stored voting supply, while treating `rawSupply % oracleCount` as non-voting dust. - -**Acceptance Criteria:** -- [ ] With 4 oracles and `quorumBps = 7500`, the third vote commits even when frozen supply is not divisible by 4 -- [ ] With 4 oracles and `quorumBps = 10000`, the fourth vote commits even when frozen supply is not divisible by 4 -- [ ] With 4 oracles and `quorumBps = 8000`, 3 votes do not commit and the fourth vote does -- [ ] `roundFrozenSupply` stores the truncated frozen voting supply and still fixes inter-vote supply drift -- [ ] No storage layout changes are introduced -- [ ] Rounds with `totalSupply == 0` revert with `ZeroCSSVSupply` -- [ ] Rounds with `0 < totalSupply < oracleCount` revert with `InsufficientCSSVSupply` -- [ ] Existing quorum behavior for low thresholds (for example `quorumBps = 1`) remains intact -- [ ] Unit test coverage includes truncation regression cases for 75%, 80%, and 100% quorum - -**Agent Instructions:** -1. Read `contracts/modules/SSVDAO.sol`, focusing on `commitRoot`. -2. Keep the current storage layout and do not add a new storage mapping such as `rootVotes`. -3. On the first vote of a round, read raw `cSSV.totalSupply()`, truncate it by `defaultOracleIds.length`, and store that truncated value in `roundFrozenSupply`. -4. Compute both `weight` and `threshold` from the stored truncated supply. -5. Update or extend unit tests in `test/unit/SSVDAO/commitRoot.test.ts` to cover: - - 75% quorum with non-divisible frozen supply - - 100% quorum with non-divisible frozen supply - - 80% quorum with non-divisible frozen supply - - `totalSupply < oracleCount` - - truncated value persisted in `roundFrozenSupply` -6. Update `docs/SPEC.md` and `docs/FLOWS.md` to describe truncated frozen voting supply in token-weight space while still noting that supply is frozen per round. - -#### Sub-items: -- [x] Add failing regression test demonstrating unreachable 3-of-4 quorum with non-divisible supply -- [ ] Patch `commitRoot` threshold math without storage-layout changes -- [ ] Add regression test for 100% quorum with non-divisible supply -- [ ] Update SPEC/FLOWS to reflect corrected quorum calculation -- [ ] Run targeted DAO/oracle tests and verify no regressions - ---- - -## Changes from DIP-X Review - -**Date:** 2026-02-17 -**Sources:** `ssv-review/planning/verified/dip-review-eth-payments.md`, `ssv-review/planning/verified/dip-review-effective-balance.md`, `ssv-review/planning/verified/dip-review-ssv-staking.md` - -### New Items Added (6) - -| ID | Title | Source Finding | Rationale | -|----|-------|---------------|-----------| -| BUG-7 | `DEFAULT_OPERATOR_ETH_FEE` value deviates from DIP-X spec | ETH-7, ETH-14 | Implementation uses 1,770,000,000 wei but closest packable value to DIP spec is 1,775,500,000 wei (~0.31% deviation) | -| BUG-8 | Cooldown duration uses `block.timestamp` but DIP specifies blocks | DIP-8 | HIGH risk: if initial value set as 50120 (blocks), actual cooldown would be ~13.9 hours instead of 7 days | -| SEC-9 | `operatorMaxFee` function signature differs from DIP-X spec | ETH-13 | DIP says `uint64`, implementation uses `uint256`; cosmetic but should be aligned | -| SEC-10 | cSSV token lacks governance/voting extensions | DIP-10 | DIP claims cSSV retains governance power, but `CSSVToken` has no `ERC20Votes`; depends on off-chain Snapshot config | -| DEPLOY-5 | Document `operatorMinFee` governance parameter in DIP-X | ETH-20 | DIP leaves update function and initial value blank; implementation has `updateMinimumOperatorEthFee(uint256)` | -| DEPLOY-6 | DIP-X unstaking description doesn't match implementation | DIP-7 | DIP says "lock cSSV → burn later"; code does "burn immediately → return SSV later"; same economics, different UX | - -### Existing Items Updated (2) - -| ID | Change | Source Finding | -|----|--------|---------------| -| BUG-6 | Added DIP-X review source tag; added context about `_syncFees` behavior when DAO earnings decrease (`current <= previous` edge case) | DIP-18, DIP-19 | -| DEPLOY-3 | Added DIP-X review source tag; added context explaining why DIP value is not packable (`3,550,929,823 % 100,000 = 29,823`) and noting this is a governance responsibility | ETH-10 | - -### DIP-X Findings Already Covered by Existing Items (4) - -| DIP Finding | Already Covered By | Notes | -|---|---|---| -| EB-OBS-1 (auto-liquidation operator decrement condition) | BUG-5 | Same issue: `_liquidateAfterEBUpdateIfNeeded` condition `op.ethSnapshot.block != 0 && op.snapshot.block != 0` is too strict vs `updateClusterOperators` which only checks `ethSnapshot.block != 0` | -| ETH-19 (migrateClusterToETH lacks nonReentrant) | SEC-6 | Exact same recommendation | -| DIP-18 (zero totalStaked fee loss) | BUG-6 | Exact same issue and recommended fix | -| DIP-23/DIP-24 (no bounds on cooldown/quorum) | SEC-4, SEC-1 | Already covered with same recommendations | - -### DIP-X Findings Not Requiring Action (informational only) - -| DIP Finding | Verdict | Reason No Action Needed | -|---|---|---| -| ETH-1 through ETH-6 | MATCH | Implementation matches DIP specification | -| ETH-8, ETH-9, ETH-11, ETH-12 | MATCH | Implementation matches DIP specification | -| ETH-15, ETH-16, ETH-21, ETH-22 | MATCH | Implementation matches DIP specification | -| ETH-17, ETH-18, ETH-23 | EXTRA | Implementation adds beneficial features beyond DIP | -| ETH-24 | MATCH | Liquidation check correctly uses vUnit model | -| ETH-25 (no SSV cluster withdrawal) | GAP (minor) | More restrictive than DIP but aligns with migration intent; users can migrate or self-liquidate to recover SSV | -| EB-01 through EB-25 (excl. OBS-1) | MATCH | All core EB accounting claims implemented correctly | -| DIP-1, DIP-2, DIP-4–6 | MATCH | Staking core mechanics implemented correctly | -| DIP-3 (auto-delegation) | PARTIAL | By-design for initial phase; future per-user delegation requires upgrade | -| DIP-9 (min staking amount) | GAP | Implementation adds reasonable dust-prevention constraint not in DIP | -| DIP-11–13, DIP-15–17 | MATCH | Oracle and reward mechanics correct | -| DIP-14 (uint128 overflow) | PARTIAL | Theoretically possible but practically impossible for realistic scenarios | -| DIP-20 (flash-loan prevention) | MATCH | Not vulnerable in current permissioned oracle model | -| DIP-25–28 | MATCH | Revenue source, views, ordering, minting ratio all correct | - ---- - -## Changes from New Audit Findings - -**Date:** 2026-02-17 -**Sources:** Research-driven gap analysis audit - -### Status Updates (4) - -| ID | Previous Status | New Status | Rationale | -|----|----------------|------------|-----------| -| BUG-1 | Fixed (verified on `ssv-staking`) | ✅ Fixed | Confirmed fixed in Monday.com | -| BUG-2 | Closed (by design) | Won't Fix (By Design) | Confirmed by-design in Monday.com | -| BUG-3 | Closed (mitigated) | ✅ Mitigated | Confirmed mitigated in Monday.com | -| BUG-5 | Open | ✅ Fixed | Confirmed fixed in Monday.com | - -### New Items Added (16) - -| ID | Title | Type | Priority | -|----|-------|------|----------| -| BUG-9 | `uint64(delta)` silent truncation in operator earnings accumulation | Critical Bug Fix | P1 | -| SEC-11 | `hasDeviation` reactivation optimization uses global counter for per-operator decision | Security Hardening | P1 | -| SEC-12 | `deposit()` accepts deposits to liquidated ETH clusters without fee settlement | Security Hardening | P2 | -| SEC-13 | `OperatorWithdrawn` event doesn't distinguish ETH vs SSV withdrawals | Security Hardening | P2 | -| SEC-14 | `commitRoot` accepts `bytes32(0)` as merkleRoot — permanently wastes block slot | Security Hardening | P2 | -| SEC-15 | Min/max operator fee can be set to contradictory values | Security Hardening | P2 | -| SEC-16 | Missing zero-value/zero-address guards on deposit and withdraw | Security Hardening | P2 | -| TEST-28 | Uncomment SSV reentrancy test assertions | Unit Test Completeness | P0 | -| TEST-29 | Add contract ETH balance delta assertions to deposit tests | Unit Test Completeness | P1 | -| TEST-30 | Resolve TODO comments with deferred assertions | Unit Test Completeness | P1 | -| TEST-31 | Expand onCSSVTransfer test coverage | Unit Test Completeness | P1 | -| TEST-32 | Add access control tests for DAO governance functions | Unit Test Completeness | P1 | -| DEPLOY-7 | Deploy scripts import from test files | Deployment & Scripts | P2 | -| QUALITY-1 | `operatorFeeChangeRequests` not cleared on operator removal | Code Quality | P2 | -| QUALITY-2 | Redundant `SSVStorage.load()` calls in view function loops | Code Quality | P2 | -| QUALITY-3 | `withdraw` in SSVClusters duplicates operator loop inline | Code Quality | P2 | -| QUALITY-4 | `_resetOperatorState` returns unused `Operator memory` | Code Quality | P3 | - ---- - -## Code Quality — New Tasks - -### [QUALITY-6] Multiple Fixture Patterns Across Tests -- **Type:** Code Quality -- **Priority:** P1 (High) -- **Status:** Open -- **Owner:** (unassigned) -- **Timeline:** After PR #435 -- **Github Link:** (empty) - -**Issue:** -Tests use different fixture approaches: -1. E2E tests: `ssvNetworkFullFixture(connection)` from `test/e2e/setup/fixtures.ts` -2. Unit tests: `ssvNetwork()` from `test/helpers/contract-helpers.ts` -3. Integration tests: mixed usage - -**Impact:** -- Harder to maintain -- Potential inconsistencies in setup state -- Confusing for new contributors - -**Recommendation:** -After PR #435 merges, standardize on a single fixture pattern. - -**Acceptance Criteria:** -- [ ] One fixture entrypoint used across E2E/unit/integration tests -- [ ] Old fixture helpers removed or thinly re-export the canonical fixture -- [ ] Documentation in `test/` updated to point to the single fixture - ---- - -### [QUALITY-7] Harness Contracts vs. Real Contracts in Tests -- **Type:** Code Quality -- **Priority:** P2 (Medium) -- **Status:** Open -- **Owner:** (unassigned) -- **Timeline:** After PR #435 -- **Github Link:** (empty) - -**Issue:** -Some tests use harness contracts (mocks for SSV clusters), while others use real deployments. - -**Impact:** -- Harness contracts may not catch production bugs -- Tests with real contracts are more trustworthy - -**Recommendation:** -Migrate all E2E tests to use real contracts (per PR #435). - -**Acceptance Criteria:** -- [ ] E2E tests run exclusively against real contract deployments -- [ ] Harness usage limited to unit tests where mocking is intentional and documented -- [ ] Any remaining harness usage in E2E is justified in test docs - ---- - -### [QUALITY-8] Helper Function Duplication Across Test Types -- **Type:** Code Quality -- **Priority:** P3 (Low) -- **Status:** Open -- **Owner:** (unassigned) -- **Timeline:** After PR #435 -- **Github Link:** (empty) - -**Issue:** -`test/e2e/helpers/` and `test/helpers/contract-helpers.ts` overlap in functionality. - -**Impact:** -- Minor maintenance burden -- Low risk of divergence - -**Recommendation:** -Merge helper utilities after PR #435. - -**Acceptance Criteria:** -- [ ] Single helper module owns shared test utilities -- [ ] Duplicates removed or consolidated -- [ ] Imports updated across test suites - ---- - -### [QUALITY-9] ~~Clear Operator Fee Change Requests on Removal~~ -- **Type:** Code Quality -- **Priority:** P2 (Medium) -- **Status:** ✅ Closed -- **Owner:** (resolved) -- **Timeline:** 2026-03-12 -- **Github Link:** (empty) - -**Resolution:** -`SSVOperators.removeOperator` now deletes `operatorFeeChangeRequests[operatorId]` before balances are withdrawn, so removal no longer leaves stale fee-change state behind. - -Added a unit test in `test/unit/SSVOperators/removeOperator.test.ts` that: -- creates a real pending fee declaration via `declareOperatorFee` -- verifies the exact stored request fields before removal -- removes the operator -- verifies `fee`, `approvalBeginTime`, and `approvalEndTime` are all exactly `0` - -**Acceptance Criteria:** -- [x] `removeOperator` clears `operatorFeeChangeRequests[operatorId]` -- [x] Unit test covers removal with an active fee change request - ---- - -### [QUALITY-10] ~~`removeOperator` does not clear `operatorEthVUnits` — orphaned deviation~~ -- **Type:** Code Quality -- **Priority:** P1 -- **Status:** ✅ Fixed -- **Owner:** (resolved) -- **Timeline:** 2026-03-16 -- **Github Link:** (empty) - -**Resolution:** -`removeOperator` now deletes `SSVStorageEB.load().operatorEthVUnits[operatorId]` alongside the existing `_resetOperatorState` call, ensuring no orphaned deviation remains for removed operators. - -Added a unit test in `test/unit/SSVOperators/removeOperator.test.ts` that: -- Registers an operator and sets `operatorEthVUnits` to a non-zero value via harness -- Removes the operator -- Verifies `operatorEthVUnits` is cleared to 0 - -**Acceptance Criteria:** -- [x] `removeOperator` clears `operatorEthVUnits[operatorId]` -- [x] Unit test covers removal with non-zero `operatorEthVUnits` - ---- - -### [QUALITY-11] ~~`commitRoot` skips `WeightedRootProposed` on quorum-reaching vote~~ -- **Type:** Code Quality -- **Priority:** P2 -- **Status:** ✅ Fixed -- **Owner:** (resolved) -- **Timeline:** 2026-03-16 -- **Github Link:** (empty) - -**Problem:** -When the final oracle vote reached quorum in `commitRoot`, the function emitted `RootCommitted` and returned early, skipping the `WeightedRootProposed` event. Off-chain consumers (oracle client, monitoring) that track per-vote weight progression would miss the final vote's weight data. - -**Resolution:** -Moved `emit WeightedRootProposed(...)` before the quorum threshold check in `SSVDAO.sol`, so every vote — including the one that triggers consensus — emits `WeightedRootProposed`. The quorum-reaching vote now emits both `WeightedRootProposed` and `RootCommitted`. - -Updated all tests that assert on quorum-reaching transactions: -- `test/unit/SSVDAO/commitRoot.test.ts` — 9 tests updated to expect both events -- `test/e2e/effective-balance/oracle-commits.test.ts` — 2 tests updated (lines 97 and 141 changed from `not.emit` to `emit`) - -**Acceptance Criteria:** -- [x] Every `commitRoot` call emits `WeightedRootProposed`, including the quorum-reaching vote -- [x] Quorum-reaching vote emits both `WeightedRootProposed` and `RootCommitted` -- [x] All unit and E2E tests pass with updated assertions - ---- - -### [QUALITY-12] ~~Unsafe `uint128 → uint64` casts in operator/DAO earnings accumulation~~ -- **Type:** Code Quality -- **Priority:** P2 -- **Status:** ✅ Fixed -- **Owner:** (resolved) -- **Timeline:** 2026-03-17 -- **Github Link:** (empty) - -**Problem:** -Operator earnings deltas and DAO earnings are computed as `uint128` but silently truncated to `uint64` via `PackedETH.wrap(uint64(delta))` in three locations in `OperatorLib.sol` (lines 69, 94, 307) and one in `ProtocolLib.sol` (line 89). If `delta` exceeds `type(uint64).max`, earnings silently vanish with no revert. While not reachable under current realistic parameters, the absence of a bounds check means pathological conditions (snapshot not updated for decades, extreme fee/validator values) would cause permanent fund loss. - -**Resolution:** -Added a lightweight `_safeUint64(uint128)` free function in `SSVCoreTypes.sol` with a custom `SafeCastOverflow` error — avoids importing OpenZeppelin's SafeCast to save gas and contract size. Replaced all 4 unsafe `uint64(delta)` / `uint64(earningsUnits)` casts with `_safeUint64(delta)` / `_safeUint64(earningsUnits)`. - -Files changed: -- `contracts/libraries/SSVCoreTypes.sol` — Added `_safeUint64` helper and `SafeCastOverflow` error -- `contracts/libraries/OperatorLib.sol` — 3 casts replaced (lines 69, 94, 307) -- `contracts/libraries/ProtocolLib.sol` — 1 cast replaced (line 89) -- `contracts/test/harness/PackedLibHarness.sol` — Harness wrapper for testing -- `test/unit/packedLib.test.ts` — 6 new tests (zero, in-range, boundary, overflow scenarios) - -**Acceptance Criteria:** -- [x] All `uint128 → uint64` casts in state-modifying earnings functions use `_safeUint64` -- [x] Overflow reverts with `SafeCastOverflow` instead of silent truncation -- [x] 6 unit tests verify correct behavior at zero, in-range, boundary, and overflow values -- [x] All 1209 existing tests pass with zero regressions - ---- - -## Mainnet Readiness - -### [MAINNET-READINESS-1] Mainnet playbook ready and sent to m-sig -- **Type:** Mainnet Readiness -- **Priority:** P0 -- **Status:** In Progress -- **Owner:** Marco -- **Related:** OPS-1, PR [#523](https://github.com/ssvlabs/ssv-network/pull/523) - -**Description:** -Finalize and deliver the mainnet upgrade playbook to the multisig. This involves incorporating the latest protocol parameters (network fee, liquidation collateral, liquidation threshold, oracle set, cooldown duration, quorum BPS) that will be used for the mainnet deployment into the upgrade scripts. Once the scripts are ready, Yurii will validate them locally. After the mainnet contracts are fully populated on Hoodi testnet, the upgrade should be executed following the playbook strictly, using a SAFE wallet on Hoodi to validate the end-to-end flow before mainnet. - -**Actions:** -- [ ] Incorporate final mainnet protocol parameters into upgrade scripts (based on DIP-X proposed values) -- [ ] Yurii to validate scripts locally against Hoodi state -- [ ] Execute full upgrade flow on Hoodi using a SAFE wallet, following the playbook step-by-step -- [ ] Deliver signed-off playbook to the multisig - -**Acceptance Criteria:** -- [ ] All protocol parameters in scripts match the DIP-X approved governance values -- [ ] Hoodi upgrade completes without errors via SAFE wallet -- [ ] Playbook document sent and acknowledged by m-sig signers - ---- - -### [MAINNET-READINESS-2] Full mainnet → staking upgrade flow validated on Hoodi -- **Type:** Mainnet Readiness -- **Priority:** P0 -- **Status:** Blocked (waiting on MAINNET-READINESS-1) -- **Owner:** Marco - -**Description:** -Validate the complete end-to-end upgrade flow from the current mainnet state v1.2.0 to v2.0.0 (SSV Staking) on the Hoodi testnet. This task is blocked until the mainnet contracts are fully populated on Hoodi (i.e., MAINNET-READINESS-1 is complete and the Hoodi environment reflects a realistic mainnet state). The validation must cover the full upgrade sequence: deploying new module implementations, running the reinitializer, verifying post-upgrade state consistency, and confirming all cluster/operator/staking flows work correctly. - -**Actions:** -- [ ] Wait for Hoodi environment to be populated with mainnet-like contract state (dependency: MAINNET-READINESS-1) -- [ ] Deploy all v2.0.0 module implementations to Hoodi -- [ ] Execute `reinitializer(3)` upgrade via SAFE wallet following the playbook -- [ ] Verify post-upgrade state: operator ETH fees, cluster balances, staking module initialization -- [ ] Smoke-test key flows: validator registration, cluster deposit/withdraw, staking/unstaking, oracle EB update - -**Acceptance Criteria:** -- [ ] Full upgrade completes without revert on Hoodi -- [ ] Post-upgrade state matches expected initial values (network fee, liquidation params, oracle set) -- [ ] All core user flows succeed on Hoodi post-upgrade -- [ ] No unexpected state drift detected between pre- and post-upgrade snapshots - ---- - -### [MAINNET-READINESS-3] Deep testing on staking module -- **Type:** Mainnet Readiness -- **Priority:** P0 -- **Status:** In Progress -- **Owner:** Andrew -- **Collaborators:** Venimir, Yurii -- **Related:** Gabriel to share list of new staking test cases - -**Description:** -Expand the staking module test coverage with a deep, targeted test pass focused on the SSV Staking and cSSV token flows. Gabriel will provide a list of specific scenarios to cover. The test suite should cover the full staking lifecycle — stake, requestUnstake, claimUnstake, claimEthRewards — as well as edge cases around the accumulator math, cSSV transfer reward settlement hooks, concurrent multi-user reward accumulation, and the unstake cooldown mechanism. - -**Actions:** -- [ ] Gabriel to share the list of new staking test scenarios -- [ ] Contracts team implement new tests if needed -- [ ] Venimir and Yurii to review and validate test coverage -- [ ] Run full test suite and confirm no regressions - -**Acceptance Criteria:** -- [ ] All scenarios from Gabriel's list are covered by tests -- [ ] Accumulator math (`accEthPerShare`, `userIndex`) verified with multi-user scenarios -- [ ] `onCSSVTransfer` hook reward settlement tested for stake, unstake, and direct cSSV transfers -- [ ] All tests pass with no regressions - ---- - -### [MAINNET-READINESS-4] External audit complete -- **Type:** Mainnet Readiness -- **Priority:** P2 -- **Status:** In Progress (awaiting final report) -- **Owner:** Marco -- **Note:** Ping Massimo — some partners require the audit report for their internal security evaluations. - -**Description:** -Receive and review the final audit report from QuantStamp covering the v2.0.0 SSV Staking release. The audit is a dependency for several ecosystem partners who need it for their own internal security sign-off processes before integrating with the new staking module. Once the report is received, any critical or high findings must be addressed before mainnet deployment. Marco to coordinate with Massimo on report delivery timeline and partner communication. - -**Actions:** -- [ ] Follow up with Massimo on QuantStamp report delivery ETA -- [ ] Share draft/final report with partners who requested it for internal security evaluations -- [ ] Triage all findings and create tracking items for any critical/high severity issues -- [ ] Confirm all critical/high findings are resolved before mainnet go/no-go decision - -**Acceptance Criteria:** -- [ ] Final QuantStamp audit report received -- [ ] All critical and high severity findings resolved or formally accepted with justification -- [ ] Report shared with requesting ecosystem partners -- [ ] Go/no-go sign-off includes audit clearance confirmation - ---- - -### [MAINNET-READINESS-5] cSSV token behavior outside the SSV protocol -- **Type:** Mainnet Readiness -- **Priority:** P1 -- **Status:** In Progress -- **Owner:** Andrew (implementation), Gabriel (execution) - -**Description:** -Validate cSSV token behavior in contexts outside the core SSV protocol — primarily ERC-20 standard compliance and the reward settlement hook when cSSV is transferred between arbitrary addresses. The `onCSSVTransfer` hook in `SSVStaking.sol` must correctly settle pending ETH rewards for both sender and receiver on every transfer. Tests should cover direct transfers (wallet-to-wallet), transfers via ERC-20 `approve`/`transferFrom`, integration with external contracts (e.g., DEX/AMM mock), and edge cases like transferring to/from the zero address and self-transfers. - -**Actions:** -- [ ] Andrew to define test scope for cSSV token external behavior -- [ ] Gabriel to execute the test suite -- [ ] Cover: direct transfer reward settlement, approve/transferFrom, zero-address edge cases, self-transfer -- [ ] Cover: cSSV used in a mock external contract (e.g., staking aggregator) — verify reward hooks fire correctly - -**Acceptance Criteria:** -- [ ] `onCSSVTransfer` settles rewards correctly for sender and receiver on every ERC-20 transfer -- [ ] ERC-20 standard compliance verified (transfer, transferFrom, approve, allowance) -- [ ] No reward leakage or double-claim possible via transfer manipulation -- [ ] All tests pass - ---- - -### [MAINNET-READINESS-6] Merge all pending testing-related PRs -- **Type:** Mainnet Readiness -- **Priority:** P1 -- **Status:** In Progress -- **Owner:** Marco - -**Description:** -Consolidate the repository state by merging all outstanding testing-related pull requests into the `ssv-staking` branch. This is a prerequisite for accurate final coverage reporting and ensures that the mainnet go/no-go decision is based on a clean, up-to-date codebase. Marco to identify all open testing PRs, verify they are ready to merge (CI passing, reviewed), and merge them in dependency order. - -**Actions:** -- [ ] Enumerate all open PRs with testing changes targeting `ssv-staking` -- [ ] Verify CI passes and reviews are complete for each PR -- [ ] Merge in dependency order (no conflicts) -- [ ] Confirm final test run passes on the merged branch - -**Acceptance Criteria:** -- [ ] All pending testing PRs merged into `ssv-staking` -- [ ] No merge conflicts remaining -- [ ] Full test suite passes on the consolidated branch -- [ ] Coverage report reflects all merged test additions - ---- diff --git a/ssv-review/planning/STAKING-TEST-PLAN.md b/ssv-review/planning/STAKING-TEST-PLAN.md deleted file mode 100644 index bd0e31644..000000000 --- a/ssv-review/planning/STAKING-TEST-PLAN.md +++ /dev/null @@ -1,179 +0,0 @@ -# SSV Staking Test Plan — Coverage Report - -Generated: 2026-03-18 - -## 1. Staking — `stake()` (18 test cases) - -| # | Test Case | Status | Covered By | -|---|-----------|--------|------------| -| 1 | Basic stake | Covered | unit/stake.ts:26, integration/staking.ts:87, e2e/lifecycle.ts:58 | -| 2 | Stake exactly minimum | Covered | unit/stake.ts:76, e2e/edge-cases.ts:343 | -| 3 | Stake large amount (full balance) | Covered | unit/stake.ts:26 (stakes STAKE_AMOUNT) | -| 4 | Multiple stakes | Covered | unit/stake.ts:131 | -| 5 | Stake by multiple users | Covered | integration/staking.ts:474, e2e/lifecycle.ts:168 | -| 6 | Rewards start accruing after stake | Covered | e2e/lifecycle.ts:58, e2e/rewards.ts:1101 | -| 7 | Second stake settles pending rewards | Covered | unit/stake.ts:153 | -| 8 | SyncFees called during stake | Covered | unit/syncFees.ts (implicitly), e2e/transfers.ts:305 | -| 9 | RewardsSettled event emitted | Covered | e2e/transfers.ts:503 (during transfer triggers settle) | -| 10 | Staked event emitted | Covered | unit/stake.ts:42 | -| 11 | Stake zero reverts | Covered | unit/stake.ts:88, integration/staking.ts:681, e2e/edge-cases.ts:319 | -| 12 | Stake below minimum reverts | Covered | unit/stake.ts:98, integration/staking.ts:686, e2e/edge-cases.ts:328 | -| 13 | Stake without approval reverts | Covered | unit/stake.ts:120 | -| 14 | Stake more than balance reverts | Covered | unit/stake.ts:121 | -| 15 | Insufficient allowance reverts | Covered | unit/stake.ts:111 | -| 16 | Fees accrued but totalStaked was 0 | Covered | e2e/lifecycle.ts:114, e2e/rewards.ts:1256 | -| 17 | Stake exactly 1 above minimum | Covered | unit/stake.ts:87 | -| 18 | Reentrancy on stake | Covered | unit/reentrancy.ts (for claimEthRewards; stake uses nonReentrant too) | - -## 2. Earning Rewards (26 test cases) - -| # | Test Case | Status | Covered By | -|---|-----------|--------|------------| -| 1 | Rewards start from stake block | Covered | e2e/lifecycle.ts:58, e2e/rewards.ts:1101 | -| 2 | Rewards start from cSSV transfer receive | Covered | e2e/transfers.ts:260 (receiver index set at transfer time) | -| 3 | Rewards stop on requestUnstake (full) | Covered | e2e/lifecycle.ts:395 | -| 4 | Rewards stop on requestUnstake (partial) | Covered | e2e/lifecycle.ts:449 | -| 5 | Rewards stop on cSSV transfer (full) | Covered | e2e/transfers.ts:53 | -| 6 | Rewards stop on cSSV transfer (partial) | Covered | e2e/transfers.ts:53 | -| 7 | Rewards with 1 wei cSSV | Covered | unit/onCSSVTransfer.ts:181 | -| 8 | Single staker gets all rewards | Covered | e2e/rewards.ts:1101, e2e/lifecycle.ts:58 | -| 9 | Two equal stakers split 50/50 | Covered | integration/staking.ts:401 | -| 10 | Two unequal stakers proportional | Covered | e2e/lifecycle.ts:168, e2e/rewards.ts:1155 | -| 11 | Three stakers, one unstakes mid-period | Covered | e2e/lifecycle.ts:246 | -| 12 | Reward math matches formula | Covered | e2e/rewards.ts:1101 (exact formula verification) | -| 13 | Rewards increase after fee raise | Covered | e2e/rewards.ts:78 | -| 14 | Rewards decrease after fee reduction | Covered | e2e/rewards.ts:206 | -| 15 | Rewards stop after fee set to zero | Covered | e2e/rewards.ts:298 | -| 16 | Rewards increase after EB update | Covered | e2e/rewards.ts:891, integration/staking.ts:272 | -| 17 | Multiple fee changes across staking period | Covered | e2e/rewards.ts:410 | -| 18 | Rewards unaffected by cooldown increase | Covered | e2e/rewards.ts:605 | -| 19 | Rewards unaffected by cooldown decrease | Covered | e2e/rewards.ts:748 | -| 20 | Rewards accrue normally after cooldown change and unstake | Covered | e2e/lifecycle.ts:567 | -| 21 | Second stake preserves prior rewards | Covered | unit/stake.ts:153 | -| 22 | Stake after partial unstake | Covered | unit/stake.ts:202 | -| 23 | Late staker doesn't get early rewards | Covered | e2e/lifecycle.ts:249 | -| 24 | Transfer then claim — sender keeps pre-transfer rewards | Covered | e2e/transfers.ts:53 | -| 25 | Stake-transfer-stake cycle | Covered | e2e/transfers.ts:140 | -| 26 | Self-transfer doesn't double rewards | Covered | e2e/transfers.ts:404 | - -## 3. Claim Rewards — `claimEthRewards()` (17 test cases) - -| # | Test Case | Status | Covered By | -|---|-----------|--------|------------| -| 1 | Basic claim | Covered | unit/claimEthRewards.ts:44, e2e/lifecycle.ts:58 | -| 2 | Claim multiple times | Covered | unit/claimEthRewards.ts:270, e2e/edge-cases.ts:162 | -| 3 | Claim after cSSV transfer (sender) | Covered | e2e/transfers.ts:53 | -| 4 | Claim after partial unstake | Covered | e2e/edge-cases.ts:457 | -| 5 | Multiple claims from multiple users | Covered | unit/claimEthRewards.ts:332, e2e/lifecycle.ts:168 | -| 6 | Claim with no rewards reverts | Covered | unit/claimEthRewards.ts:151, integration/staking.ts:758 | -| 7 | Claim when accrued is zero reverts | Covered | unit/claimEthRewards.ts:151 | -| 8 | Claim twice in same block | Covered | unit/claimEthRewards.ts:267, e2e/edge-cases.ts:520, forked/fullIntegrationForked.ts:1795, echidna/SSVStakingEchidna.sol:389 | -| 9 | Claim with sub-precision dust reverts | Covered | unit/claimEthRewards.ts:163 | -| 10 | Payout truncated to ETH_DEDUCTED_DIGITS | Covered | unit/claimEthRewards.ts:83 | -| 11 | Dust forfeited when cSSV balance is zero | Covered | unit/claimEthRewards.ts:102, 366, 391 | -| 12 | Dust preserved when cSSV balance > 0 | Covered | unit/claimEthRewards.ts:127, 414 | -| 13 | Exact precision amount | Covered | unit/claimEthRewards.ts:590 | -| 14 | FeesSynced emitted | Covered | unit/claimEthRewards.ts:195 | -| 15 | RewardsSettled emitted | Covered | e2e/transfers.ts:503 | -| 16 | RewardsClaimed emitted with payout | Covered | unit/claimEthRewards.ts:67 | -| 17 | RewardsClaimed emitted with zero on dust forfeit | Covered | unit/claimEthRewards.ts:384, 407 | - -## 4. Request Unstake — `requestUnstake()` (25 test cases) - -| # | Test Case | Status | Covered By | -|---|-----------|--------|------------| -| 1 | Basic unstake request | Covered | unit/requestUnstake.ts:33 | -| 2 | Partial unstake | Covered | unit/requestUnstake.ts:33, integration/staking.ts:118 | -| 3 | Full unstake | Covered | unit/requestUnstake.ts:114 | -| 4 | Multiple unstake requests | Covered | unit/requestUnstake.ts:152, integration/staking.ts:628 | -| 5 | Settles rewards before burn | Covered | unit/requestUnstake.ts:211, e2e/lifecycle.ts:395 | -| 6 | Rewards still claimable after full unstake | Covered | e2e/lifecycle.ts:395 | -| 7 | Unstake after cSSV transfer receive | Covered | unit/requestUnstake.ts:148 | -| 8 | Unstake zero reverts | Covered | unit/requestUnstake.ts:80, integration/staking.ts:704 | -| 9 | Unstake more than balance reverts | Covered | unit/requestUnstake.ts:103, integration/staking.ts:692 | -| 10 | Unstake with no cSSV reverts | Covered | unit/requestUnstake.ts:110, integration/staking.ts:643 | -| 11 | Exceed max pending requests | Covered | unit/requestUnstake.ts:89, e2e/edge-cases.ts:222 | -| 12 | Unlock time is correct | Covered | unit/requestUnstake.ts:60 | -| 13 | Different requests have different unlock times | Covered | unit/requestUnstake.ts:152 | -| 14 | Cooldown duration change affects new requests only | Covered | unit/requestUnstake.ts:241, integration/staking.ts:651 | -| 15 | Cooldown increase — old request uses old cooldown | Covered | unit/requestUnstake.ts:269, unit/withdrawUnlocked.ts:320 | -| 16 | Cooldown increase — new request uses new cooldown | Covered | unit/requestUnstake.ts:269, unit/withdrawUnlocked.ts:266 | -| 17 | Cooldown decrease — pending not accelerated | Covered | unit/requestUnstake.ts:294, unit/withdrawUnlocked.ts:242 | -| 18 | Cooldown decrease — new request uses shorter | Covered | unit/requestUnstake.ts:294 | -| 19 | cSSV burned immediately | Covered | unit/requestUnstake.ts:33 | -| 20 | SSV tokens NOT returned yet | Covered | (implicit from withdraw tests) | -| 21 | Rewards stop accruing on burned portion | Covered | e2e/lifecycle.ts:449 | -| 22 | syncFees called during requestUnstake | Covered | unit/requestUnstake.ts:211 | -| 23 | UnstakeRequested emitted | Covered | unit/requestUnstake.ts:47, e2e/lifecycle.ts:371 | -| 24 | FeesSynced emitted | Covered | (implicit) | -| 25 | RewardsSettled emitted | Covered | (implicit) | - -## 5. Withdraw Unlocked — `withdrawUnlocked()` (16 test cases) - -| # | Test Case | Status | Covered By | -|---|-----------|--------|------------| -| 1 | Basic withdraw | Covered | unit/withdrawUnlocked.ts:37, integration/staking.ts:150, e2e/lifecycle.ts:335 | -| 2 | Withdraw multiple matured at once | Covered | unit/withdrawUnlocked.ts:137 | -| 3 | Withdraw only matured, immature remain | Covered | unit/withdrawUnlocked.ts:177 | -| 4 | Withdraw at exact unlock time | Covered | unit/withdrawUnlocked.ts:105 | -| 5 | Withdraw long after maturity | Covered | unit/withdrawUnlocked.ts:226, integration/staking.ts:607 | -| 6 | Multiple withdraw calls over time | Covered | unit/withdrawUnlocked.ts:221 | -| 7 | Withdraw after all cSSV burned | Covered | unit/withdrawUnlocked.ts:37 (full unstake then withdraw) | -| 8 | No requests reverts | Covered | unit/withdrawUnlocked.ts:76, integration/staking.ts:730 | -| 9 | All immature reverts | Covered | unit/withdrawUnlocked.ts:85, integration/staking.ts:716 | -| 10 | Withdraw one block before unlock | Covered | unit/withdrawUnlocked.ts:94 | -| 11 | SSV returned to user | Covered | unit/withdrawUnlocked.ts:55, integration/staking.ts:172 | -| 12 | SSV deducted from contract | Covered | unit/withdrawUnlocked.ts:59, integration/staking.ts:173 | -| 13 | cSSV supply unchanged | Covered | unit/withdrawUnlocked.ts:249, integration/staking.ts:628 | -| 14 | Two users withdraw independently | Covered | solvencyInvariant.ts:114 | -| 15 | One user's withdraw doesn't affect another | Covered | unit/withdrawUnlocked.ts:256 | -| 16 | UnstakedWithdrawn emitted | Covered | unit/withdrawUnlocked.ts:51, integration/staking.ts:166 | - -## 6. SyncFees — `syncFees()` (9 test cases) - -| # | Test Case | Status | Covered By | -|---|-----------|--------|------------| -| 1 | Basic sync | Covered | unit/syncFees.ts:24, e2e/edge-cases.ts:359 | -| 2 | Anyone can call | Covered | e2e/edge-cases.ts:423 | -| 3 | Sync after long period | Covered | unit/syncFees.ts:81 (natural accrual) | -| 4 | Multiple syncs with fees between | Covered | unit/syncFees.ts:234 | -| 5 | stake() triggers sync | Covered | unit/syncFees.ts (via events), e2e/transfers.ts:305 | -| 6 | requestUnstake() triggers sync | Covered | unit/requestUnstake.ts:211 | -| 7 | claimEthRewards() triggers sync | Covered | unit/claimEthRewards.ts:195 | -| 8 | cSSV transfer triggers sync | Covered | e2e/transfers.ts:503 | -| 9 | FeesSynced with correct values | Covered | unit/syncFees.ts:46 | - -## 7. Multisig Accounts (15 test cases) - -| # | Test Case | Status | Covered By | -|---|-----------|--------|------------| -| 1 | Multisig stakes SSV | Covered | unit/stake.ts:256, integration/staking.ts:735 | -| 2 | Multisig stakes multiple times | Covered | unit/stake.ts:282, integration/staking.ts:760 | -| 3 | Multisig earns rewards | Covered | unit/stake.ts:307 | -| 4 | Multisig claims rewards | Covered | unit/stake.ts:330 | -| 5 | Multisig claims with dust | Covered | unit/stake.ts:365 | -| 6 | Multisig transfers cSSV to EOA | Covered | unit/stake.ts:400 | -| 7 | EOA transfers cSSV to multisig | Covered | unit/stake.ts:423 | -| 8 | Multisig transfers cSSV to another multisig | Covered | unit/stake.ts:437 | -| 9 | Multisig requests unstake | Covered | unit/stake.ts:458 | -| 10 | Multisig creates multiple unstake requests | Covered | unit/stake.ts:489 | -| 11 | Multisig requests unstake after earning | Covered | unit/stake.ts:524 | -| 12 | Multisig withdraws unlocked SSV | Covered | unit/stake.ts:550 | -| 13 | Multisig withdraws multiple matured requests | Covered | unit/stake.ts:576 | -| 14 | Multisig complete flow | Covered | unit/stake.ts:612 | -| 15 | Mixed EOA and multisig interaction | Covered | unit/stake.ts:651 | - -## Summary - -| Section | Total | Covered | Partially | Not Covered | -|---------|-------|---------|-----------|-------------| -| 1. Staking | 18 | 18 | 0 | 0 | -| 2. Earning Rewards | 26 | 26 | 0 | 0 | -| 3. Claim Rewards | 17 | 17 | 0 | 0 | -| 4. Request Unstake | 25 | 25 | 0 | 0 | -| 5. Withdraw Unlocked | 16 | 16 | 0 | 0 | -| 6. SyncFees | 9 | 9 | 0 | 0 | -| 7. Multisig | 15 | 15 | 0 | 0 | -| **Total** | **126** | **126** | **0** | **0** | - -**Overall: 100% covered** diff --git a/ssv-review/planning/STAKING-TEST-PROGRESS.md b/ssv-review/planning/STAKING-TEST-PROGRESS.md deleted file mode 100644 index 1f87eb327..000000000 --- a/ssv-review/planning/STAKING-TEST-PROGRESS.md +++ /dev/null @@ -1,54 +0,0 @@ -# Staking Test Progress - -Local tracking sheet for `MR-3` staking test slice. - -Source plan: -- `ssv-review/planning/STAKING-TEST-PLAN.md` - -Notes: -- IDs are local-only for this tracking sheet. -- This tracker was seeded from scenarios marked `NOT COVERED` or `Partially` in the source plan and keeps completed rows for local history. -- Based on the current source plan, the remaining open backlog is `0` tasks. All `39` tracked scenarios are done. -- `Plan Ref` uses `
.` from `STAKING-TEST-PLAN.md`. - -| ID | Plan Ref | Section | Task | Plan Status | Current Status/Progress | -|---:|---:|---|---|---|---| -| 1 | 1.13 | Staking | ~~Stake without approval reverts~~ | Covered | Done | -| 2 | 1.17 | Staking | ~~Stake exactly 1 above minimum~~ | Covered | Done | -| 3 | 2.7 | Earning Rewards | ~~Rewards with 1 wei cSSV~~ | Covered | Done | -| 4 | 2.9 | Earning Rewards | ~~Two equal stakers split 50/50~~ | Covered | Done | -| 5 | 2.11 | Earning Rewards | ~~Three stakers, one unstakes mid-period~~ | Covered | Done | -| 6 | 2.13 | Earning Rewards | ~~Rewards increase after fee raise~~ | Covered | Done | -| 7 | 2.14 | Earning Rewards | ~~Rewards decrease after fee reduction~~ | Covered | Done | -| 8 | 2.15 | Earning Rewards | ~~Rewards stop after fee set to zero~~ | Covered | Done | -| 9 | 2.17 | Earning Rewards | ~~Multiple fee changes across staking period~~ | Covered | Done | -| 10 | 2.18 | Earning Rewards | ~~Rewards unaffected by cooldown increase~~ | Covered | Done | -| 11 | 2.19 | Earning Rewards | ~~Rewards unaffected by cooldown decrease~~ | Covered | Done | -| 12 | 2.20 | Earning Rewards | ~~Rewards accrue normally after cooldown change and unstake~~ | Covered | Done | -| 13 | 2.22 | Earning Rewards | ~~Stake after partial unstake~~ | Covered | Done | -| 14 | 2.25 | Earning Rewards | ~~Stake-transfer-stake cycle~~ | Covered | Done | -| 15 | 2.26 | Earning Rewards | ~~Self-transfer doesn't double rewards~~ | Covered | Done | -| 16 | 4.7 | Request Unstake | ~~Unstake after cSSV transfer receive~~ | Covered | Done | -| 17 | 4.10 | Request Unstake | ~~Unstake with no cSSV reverts~~ | Covered | Done | -| 18 | 4.14 | Request Unstake | ~~Cooldown duration change affects new requests only~~ | Covered | Done | -| 19 | 4.15 | Request Unstake | ~~Cooldown increase - old request uses old cooldown~~ | Covered | Done | -| 20 | 4.16 | Request Unstake | ~~Cooldown increase - new request uses new cooldown~~ | Covered | Done | -| 21 | 4.17 | Request Unstake | ~~Cooldown decrease - pending not accelerated~~ | Covered | Done | -| 22 | 4.18 | Request Unstake | ~~Cooldown decrease - new request uses shorter~~ | Covered | Done | -| 23 | 5.5 | Withdraw Unlocked | ~~Withdraw long after maturity~~ | Covered | Done | -| 24 | 5.13 | Withdraw Unlocked | ~~cSSV supply unchanged~~ | Covered | Done | -| 25 | 7.1 | Multisig Accounts | ~~Multisig stakes SSV~~ | Covered | Done | -| 26 | 7.2 | Multisig Accounts | ~~Multisig stakes multiple times~~ | Covered | Done | -| 27 | 7.3 | Multisig Accounts | ~~Multisig earns rewards~~ | Covered | Done | -| 28 | 7.4 | Multisig Accounts | ~~Multisig claims rewards~~ | Covered | Done | -| 29 | 7.5 | Multisig Accounts | ~~Multisig claims with dust~~ | Covered | Done | -| 30 | 7.6 | Multisig Accounts | ~~Multisig transfers cSSV to EOA~~ | Covered | Done | -| 31 | 7.7 | Multisig Accounts | ~~EOA transfers cSSV to multisig~~ | Covered | Done | -| 32 | 7.8 | Multisig Accounts | ~~Multisig transfers cSSV to another multisig~~ | Covered | Done | -| 33 | 7.9 | Multisig Accounts | ~~Multisig requests unstake~~ | Covered | Done | -| 34 | 7.10 | Multisig Accounts | ~~Multisig creates multiple unstake requests~~ | Covered | Done | -| 35 | 7.11 | Multisig Accounts | ~~Multisig requests unstake after earning~~ | Covered | Done | -| 36 | 7.12 | Multisig Accounts | ~~Multisig withdraws unlocked SSV~~ | Covered | Done | -| 37 | 7.13 | Multisig Accounts | ~~Multisig withdraws multiple matured requests~~ | Covered | Done | -| 38 | 7.14 | Multisig Accounts | ~~Multisig complete flow~~ | Covered | Done | -| 39 | 7.15 | Multisig Accounts | ~~Mixed EOA and multisig interaction~~ | Covered | Done | diff --git a/test/common/constants.ts b/test/common/constants.ts index cc0d42b3e..73d826cb8 100644 --- a/test/common/constants.ts +++ b/test/common/constants.ts @@ -44,6 +44,7 @@ export const DEFAULT_UNSTAKE_COOLDOWN = envBigInt( "FORK_DEFAULT_UNSTAKE_COOLDOWN", 7n * 24n * 60n * 60n ); +export const INITIAL_STAKE_AMOUNT = envBigInt("FORK_INITIAL_STAKE_AMOUNT", 0n); export const DEDUCTED_DIGITS = 10_000_000n; export const ETH_DEDUCTED_DIGITS = 100_000n; export const OPERATOR_FEE_PRECISION = ETH_DEDUCTED_DIGITS; diff --git a/test/forked/v2.0.0/fullIntegrationForked.test.ts b/test/forked/v2.0.0/fullIntegrationForked.test.ts index b6f863ddb..a0c4953ae 100644 --- a/test/forked/v2.0.0/fullIntegrationForked.test.ts +++ b/test/forked/v2.0.0/fullIntegrationForked.test.ts @@ -3,7 +3,7 @@ import type { NetworkConnection } from "hardhat/types/network"; import { ssvNetworkFullForkedFixture } from '../../setup/fixtures.ts'; import type { NetworkHelpersType, OperatorTuple, UnstakeRequest } from '../../common/types.ts'; import { buildEBMerkleForDefaultClusters, calculateInitialBurnRate, getCurrentClusterState, makeArrayOfKeysAndShares, getFeeAboveIncreaseLimit, getValidOperatorFeeIncrease, makeOperatorKey, makePublicKey, registerDefaultCluster, registerDefaultClusters, registerOperators, setAccountBalance, updateClusterBalancesForDefaultClusters, whitelistAddresses } from '../../helpers/index.ts'; -import { CLUSTER_VERSION_ETH, DECLARE_OPERATOR_FEE_PERIOD, DEFAULT_ETH_EB_PER_VALIDATOR, DEFAULT_ETH_REGISTER_VALUE, DEFAULT_ORACLES_IDS, DEFAULT_SHARES, DEFAULT_UNSTAKE_COOLDOWN, EMPTY_CLUSTER, EXECUTE_OPERATOR_FEE_PERIOD, MAXIMUM_OPERATORS_FEE, MINIMAL_LIQUIDATION_THRESHOLD, MINIMAL_OPERATOR_ETH_FEE, MINIMUM_BLOCKS_BEFORE_LIQUIDATION, MINIMUM_LIQUIDATION_PERIOD_COLLATERAL, NETWORK_FEE, OPERATOR_MAX_FEE_INCREASE, OPERATOR_FEE_PRECISION, STAKE_AMOUNT, VALIDATORS_PER_OPERATOR_LIMIT, } from '../../common/constants.ts'; +import { CLUSTER_VERSION_ETH, DECLARE_OPERATOR_FEE_PERIOD, DEFAULT_ETH_EB_PER_VALIDATOR, DEFAULT_ETH_REGISTER_VALUE, DEFAULT_ORACLES_IDS, DEFAULT_SHARES, DEFAULT_UNSTAKE_COOLDOWN, EMPTY_CLUSTER, EXECUTE_OPERATOR_FEE_PERIOD, INITIAL_STAKE_AMOUNT, MAXIMUM_OPERATORS_FEE, MINIMAL_LIQUIDATION_THRESHOLD, MINIMAL_OPERATOR_ETH_FEE, MINIMUM_BLOCKS_BEFORE_LIQUIDATION, MINIMUM_LIQUIDATION_PERIOD_COLLATERAL, NETWORK_FEE, OPERATOR_MAX_FEE_INCREASE, OPERATOR_FEE_PRECISION, STAKE_AMOUNT, VALIDATORS_PER_OPERATOR_LIMIT, } from '../../common/constants.ts'; import { Events } from '../../common/events.ts'; import type { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/types'; import { Errors } from '../../common/errors.ts'; @@ -59,7 +59,7 @@ suite("SSVNetwork full integration tests made on forked contract", () => { await expect(await views.getNetworkFeeSSV()).to.equal(NETWORK_FEE); await expect(await views.cooldownDuration()).to.equal(DEFAULT_UNSTAKE_COOLDOWN); await expect(await views.getNetworkEarnings()).to.equal(0n); - await expect(await views.totalStaked()).to.equal(0n); + await expect(await views.totalStaked()).to.equal(INITIAL_STAKE_AMOUNT); }); }); @@ -941,7 +941,7 @@ suite("SSVNetwork full integration tests made on forked contract", () => { it("Is reverted with 'Ownable: caller is not the owner' if caller is not the owner", async function () { const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); - await expect(network.connect(randomUser).updateOperatorFeeIncreaseLimit(OPERATOR_MAX_FEE_INCREASE + 1n)) + await expect(network.connect(randomUser).updateOperatorFeeIncreaseLimit(OPERATOR_MAX_FEE_INCREASE)) .to.be.revertedWith(Errors.OWNABLE_CALLER_NOT_OWNER); }); }); diff --git a/test/test-forked/v2.0.0/fullIntegrationForked.test.ts b/test/test-forked/v2.0.0/fullIntegrationForked.test.ts index 824532b26..e4d7c6a36 100644 --- a/test/test-forked/v2.0.0/fullIntegrationForked.test.ts +++ b/test/test-forked/v2.0.0/fullIntegrationForked.test.ts @@ -20,6 +20,8 @@ import { DEFAULT_SHARES, DEFAULT_UNSTAKE_COOLDOWN, EMPTY_CLUSTER, EXECUTE_OPERATOR_FEE_PERIOD, + BPS_DENOMINATOR, + INITIAL_STAKE_AMOUNT, MAXIMUM_OPERATORS_FEE, MINIMAL_LIQUIDATION_THRESHOLD, MINIMAL_OPERATOR_ETH_FEE, @@ -71,7 +73,6 @@ suite("SSVNetwork full integration tests made on forked contract", () => { const { network, views, cssvToken, ssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); - // todo work on params await expect(await network.getAddress()).to.be.equal(ForkConfig.SSV_NETWORK_ADDRESS); await expect(await views.getAddress()).to.be.equal(ForkConfig.SSV_NETWORK_VIEWS); await expect(await ssvToken.getAddress()).to.be.equal(ForkConfig.SSV_TOKEN); @@ -90,7 +91,7 @@ suite("SSVNetwork full integration tests made on forked contract", () => { await expect(await views.cooldownDuration()).to.equal(DEFAULT_UNSTAKE_COOLDOWN); await expect(await views.getNetworkEarnings()).to.equal(0n); - await expect(await views.totalStaked()).to.equal(0n); + await expect(await views.totalStaked()).to.equal(INITIAL_STAKE_AMOUNT); }); }); @@ -184,7 +185,6 @@ suite("SSVNetwork full integration tests made on forked contract", () => { const operator: OperatorTuple = await views.getOperatorById(expectedId) - // todo check how to make typed, maybe cast to object like cluster await expect(operator[5]).to.be.equal(false) await expect(await views.getOperatorFee(expectedId)).to.be.equal(0); }); @@ -593,7 +593,6 @@ suite("SSVNetwork full integration tests made on forked contract", () => { .withArgs(operatorIds, true); const operator: OperatorTuple = await views.getOperatorById(operatorIds[0]); - // todo type await expect(operator[4]).to.be.equal(true); //isPrivate }); @@ -635,7 +634,6 @@ suite("SSVNetwork full integration tests made on forked contract", () => { .withArgs(operatorIds, false); const operator: OperatorTuple = await views.getOperatorById(operatorIds[0]); - // todo type await expect(operator[4]).to.be.equal(false); //isPrivate }); @@ -686,7 +684,6 @@ suite("SSVNetwork full integration tests made on forked contract", () => { .to.emit(network, Events.OPERATOR_FEE_DECLARED) .withArgs(operatorOwner.address, operatorIds[0], tx.blockNumber, newFee); - // todo type await expect(await views.getOperatorDeclaredFee(operatorIds[0])) .to.be.deep.equal([ true, // isActive @@ -1233,7 +1230,7 @@ suite("SSVNetwork full integration tests made on forked contract", () => { const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); - await expect(network.connect(randomUser).updateOperatorFeeIncreaseLimit(OPERATOR_MAX_FEE_INCREASE + 1n)) + await expect(network.connect(randomUser).updateOperatorFeeIncreaseLimit(OPERATOR_MAX_FEE_INCREASE)) .to.be.revertedWith(Errors.OWNABLE_CALLER_NOT_OWNER); }); @@ -1241,7 +1238,7 @@ suite("SSVNetwork full integration tests made on forked contract", () => { const { network, daoSigner } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); - await expect(network.connect(daoSigner).updateOperatorFeeIncreaseLimit(OPERATOR_MAX_FEE_INCREASE + 1n)) + await expect(network.connect(daoSigner).updateOperatorFeeIncreaseLimit(BPS_DENOMINATOR + 1n)) .to.be.revertedWithCustomError(network, Errors.INVALID_OPERATOR_FEE_INCREASE_LIMIT); }); });