Skip to content

fix(contract): avoid validator config v2 resource exhaustion#2900

Draft
SuperFluffy wants to merge 3 commits intomainfrom
janis/val-cfg-v2-track-active
Draft

fix(contract): avoid validator config v2 resource exhaustion#2900
SuperFluffy wants to merge 3 commits intomainfrom
janis/val-cfg-v2-track-active

Conversation

@SuperFluffy
Copy link
Contributor

This patch fixes a potential resource exhaustion in ValidatorConfigV2.getAllValidators.

Active V2 validators are now tracked in a separate array that holds indices into the global append only validatorsArray. Consistency is ensured by deleting entries on deactiveValidator and rotateValidator.

@tempoxyz-bot

This comment was marked as outdated.

tempoxyz-bot

This comment was marked as outdated.

@tempoxyz-bot
Copy link

tempoxyz-bot commented Feb 27, 2026

🐺 Ralph Security Review

4b7dc0f

Worker Engine Progress Status
pr-2900-w1 gemini-3.1-pro-preview 🚨 thread-1 🚨 thread-2 🚨 thread-3 Done
pr-2900-w2 amp/deep thread-1 🚨 thread-2thread-3 Done

Findings

# Finding Severity Verification Threads
1 Off-diff invariant harness break from getAllValidators removal Medium ✅ Verified audit · verify
2 PR fails to implement gas exhaustion fix in actual Node Precompile, leaving getActiveValidators vulnerable to OOG and DoS High ✅ Verified audit · verify
3 ValidatorConfigV2 Reintroduces ZELLIC-24: Validators Can Block Deactivation via Front-running Medium ✅ Verified audit · verify
4 Migration to ValidatorConfigV2 permanently halts if an inactive V1 validator shares an IP with an active validator High ✅ Verified audit · verify

📋 Consolidation complete · Thread


📜 28 events

🔍 pr-2900-w1 iter 1/3 [audit-ripple.md]
🔍 pr-2900-w2 iter 1/3 [audit-focused.md]
pr-2900-w2 iter 1 — clear | Thread
🔍 pr-2900-w2 iter 2/3 [audit-ripple.md]
🚨 pr-2900-w2 iter 2 — finding | Thread
🚨 Finding: Off-diff invariant harness break from getAllValidators removal (Medium) | Thread
🔍 pr-2900-w2 iter 3/3 [audit-historical.md]
🔬 Verifying: Off-diff invariant harness break from getAllValidators removal | Thread
📋 Verify: Off-diff invariant harness break from getAllValidators removal → ✅ Verified | Thread
pr-2900-w2 iter 3 — clear | Thread
🏁 pr-2900-w2 done
🚨 pr-2900-w1 iter 1 — finding | Thread
🚨 Finding: PR fails to implement gas exhaustion fix in actual Node Precompile, leaving getActiveValidators vulnerable to OOG and DoS (High) | Thread
🔍 pr-2900-w1 iter 2/3 [audit-historical.md]
🔬 Verifying: PR fails to implement gas exhaustion fix in actual Node Precompile, leaving getActiveValidators vulnerable to OOG and DoS | Thread
📋 Verify: PR fails to implement gas exhaustion fix in actual Node Precompile, leaving getActiveValidators vulnerable to OOG and DoS → ✅ Verified | Thread
🚨 pr-2900-w1 iter 2 — finding | Thread
🚨 Finding: ValidatorConfigV2 Reintroduces ZELLIC-24: Validators Can Block Deactivation via Front-running (Medium) | Thread
🔍 pr-2900-w1 iter 3/3 [audit-focused.md]
🔬 Verifying: ValidatorConfigV2 Reintroduces ZELLIC-24: Validators Can Block Deactivation via Front-running | Thread
📋 Verify: ValidatorConfigV2 Reintroduces ZELLIC-24: Validators Can Block Deactivation via Front-running → ✅ Verified | Thread
🚨 pr-2900-w1 iter 3 — finding | Thread
🚨 Finding: Migration to ValidatorConfigV2 permanently halts if an inactive V1 validator shares an IP with an active validator (High) | Thread
🏁 pr-2900-w1 done
🔬 Verifying: Migration to ValidatorConfigV2 permanently halts if an inactive V1 validator shares an IP with an active validator | Thread
📋 Verify: Migration to ValidatorConfigV2 permanently halts if an inactive V1 validator shares an IP with an active validator → ✅ Verified | Thread
📦 Consolidation: running | Thread
📦 Consolidation: complete | Thread

Copy link

@tempoxyz-bot tempoxyz-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🐺 Ralph Review

Summary

The Solidity active-index fix is algorithmically correct, but the PR has several gaps: the Rust node precompile still has the O(N) scan (highest priority), a previous audit finding (ZELLIC-24) is reintroduced, migration can permanently halt on IP collisions, and off-diff invariant tests are broken.


🚨 [SECURITY] getActiveValidators Gas Exhaustion Fix Missing from Rust Node Precompile

File: crates/precompiles/src/validator_config_v2/mod.rs:209-217

The O(N) linear scan fix was only applied to the Solidity reference implementation. The Rust precompile's get_active_validators() still iterates over all historical validators via self.read_validator_at(i), issuing ~2100 gas per SLOAD. Once the array exceeds ~14K entries, this exceeds the block gas limit and becomes permanently uncallable. Additionally, getAllValidators() is still exposed in the Rust ABI and dispatcher, creating spec drift.

Recommended Fix: Port the activeValidatorsByIndex pattern to the Rust precompile. Remove getAllValidators() from the Rust ABI and dispatch layer.


🚨 [SECURITY] Migration Permanently Halts on IP Collision

File: crates/precompiles/src/validator_config_v2/mod.rs:591-606, tips/ref-impls/src/ValidatorConfigV2.sol:337

migrateValidator unconditionally enforces _requireUniqueIngressIp on all validators including inactive ones. If a deactivated V1 validator shares an IP with an already-migrated active validator, migration reverts and cannot be resumed due to strict sequential indexing.

Recommended Fix: Only enforce IP uniqueness for active validators during migration.


⚠️ [ISSUE] Solidity rotateValidator Incorrectly Reverts When Keeping Same IP

File: tips/ref-impls/src/ValidatorConfigV2.sol:165-174

rotateValidator calls _validateRotateParams_requireUniqueIngressIp(ingress) before freeing the old IP. If a validator keeps their existing IP, the call incorrectly reverts. The Rust precompile handles this correctly.

Recommended Fix: Free the old IP before validation, or exclude the validator's own IP from the uniqueness check.

external
onlyOwnerOrValidator(validatorAddress)
{
// idx is 1-indexed.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 [SECURITY] ZELLIC-24 Regression: Validators Can Block Deactivation via Front-Running

V1 fixed ZELLIC-24 by using index-based lookups for deactivation. V2 regresses this: deactivateValidator(address) and rotateValidator(address, ...) use addressToIndex[validatorAddress]. A malicious validator can front-run the owner's deactivation with transferValidatorOwnership(currentAddress, newAddress), deleting addressToIndex[currentAddress] and causing the owner's tx to revert with ValidatorNotFound(). This is repeatable indefinitely.

Recommended Fix: Use publicKey (immutable, unique) or array index as the lookup key, matching the V1 ZELLIC-24 fix. Update TIP-1017 spec accordingly.

function getAllValidators() external view returns (Validator[] memory validators);

/// @notice Get only active validators (where deactivatedAtHeight == 0)
/// @notice Get only active validators (where deactivatedAtHeight == 0).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [ISSUE] Off-Diff Invariant Harness Broken by getAllValidators() Removal

Removing getAllValidators() breaks invariant tests in test/invariants/ValidatorConfigV2.t.sol (lines 983, 1009, 1018, 1036+) that are outside this diff. forge test now fails at compile time, silently disabling regression coverage for append-only and history invariants (TEMPO-VALV2-14 through 19).

Recommended Fix: Migrate invariant tests to use validatorCount() + validatorByIndex() before merging. Do not ship with broken invariant coverage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants