Live EigenLayer Bug Discovered During Sidecar Security Review

Category Security, Blockchain

Critical Division-by-Zero Bug in EigenLayer Sidecar Rewards: Analysis and Fix

tldr: A division-by-zero bug in EigenLayer's sidecar reward calculation could have caused a denial-of-service for all AVSs and operators. The root cause was insufficient validation of the duration parameter, allowing zero values to propagate from smart contracts to SQL queries. The issue was fixed before exploitation by adding explicit checks both onchain and in sidecar.

Introduction

EigenLayer introduces restaking to Ethereum, allowing staked assets to secure other applications known as Actively Validated Services (AVSs). This complex ecosystem relies on both onchain smart contracts and off-chain components (sidecar) for tasks like data aggregation and reward calculations. This post details a critical division-by-zero bug found in the EigenLayer sidecar's reward processing logic, explaining its root cause, potential impact, and the subsequent fixes implemented.

EigenLayer Sidecar and Reward Processing

The EigenLayer sidecar acts as an off-chain worker supporting the core EigenLayer smart contracts. It listens for onchain events, processes data, performs computations (like reward calculations), and stores results, often in a database.

A key function is calculating rewards for operators and stakers securing AVSs. AVSs submit reward details to the onchain RewardsCoordinator.sol contract. This contract emits events that the sidecar monitors. The sidecar then uses the event data, including reward amount and duration, to calculate and attribute rewards, storing the results for later distribution.

The RewardsCoordinator and the Path to Vulnerability

The RewardsCoordinator.sol contract is the gateway for AVS reward submissions. It includes parameters like amount and the duration (in seconds) over which the reward was earned. The contract performed a basic validation on duration:

RewardsCoordinator.sol#L439

// RewardsCoordinator.sol (before patch)
require(duration % CALCULATION_INTERVAL_SECONDS == 0, InvalidDurationRemainder())

While ensuring the duration was a multiple of a specific interval, this check did not prevent a duration of zero (as 0 % N == 0). A zero duration could thus be emitted in an event.

Root Cause: Unvalidated Duration Leads to Division by Zero

The vulnerability manifested in the sidecar's reward calculation logic, triggered by the acceptance of zero-duration rewards.

1. Lack of Validation During Ingestion: The sidecar's event parsing logic (e.g., in operatorDirectedOperatorSetRewardSubmissions.go) read the duration value directly from the event payload without checking if it was non-zero.

operatorDirectedOperatorSetRewardSubmissions.go#L160-L178

// Path: /sidecar/pkg/eigenState/operatorDirectedOperatorSetRewardSubmissions/operatorDirectedOperatorSetRewardSubmissions.go
// Inside handleOperatorDirectedOperatorSetRewardSubmissionCreatedEvent function:

// ... other fields parsed ...
rewardSubmission := &OperatorDirectedOperatorSetRewardSubmission{
    // ... other fields ...
    Duration:        outputRewardData.Duration, // Duration read directly without validation
    // ... other fields ...
}

Additionally, the PostgreSQL database schema used by the sidecar defined the duration column as bigint not null. This prevents NULL but allows a value of 0. A zero-duration reward could therefore be successfully parsed and stored.

2. Division by Zero in SQL Calculation: Subsequent reward processing steps involved SQL queries that divided by the stored duration to calculate reward rates (e.g., tokens per day). This pattern appeared in multiple Go files responsible for different reward types:

    -- Inside _1_goldActiveRewardsQuery
    SELECT ..., amount / (duration / 86400) as tokens_per_day, ...
    -- Inside _7_goldActiveODRewardsQuery (within CASE statement)
    WHEN ar.num_registered_snapshots = 0 THEN floor(ar.amount_decimal / (duration / 86400))
    -- Inside _11_goldActiveODOperatorSetRewardsQuery (within CASE statement)
    WHEN ar.num_registered_snapshots = 0 THEN floor(ar.amount_decimal / (duration / 86400))

If any database record processed by these queries had duration = 0, the expression (duration / 86400) would evaluate to zero, causing a critical division-by-zero error during SQL execution.

Discovery Process

The discovery of this vulnerability followed a standard security review process, starting from potential sink points and tracing back to the source:

  1. Identifying the Sink: While reviewing the SQL queries within the sidecar's reward calculation logic (e.g., 1_goldActiveRewards.go, 7_goldActiveODRewards.go, 11_goldActiveODOperatorSetRewards.go), the division operation involving the duration variable was flagged as a potential division-by-zero risk. Division operations are common areas to check for such vulnerabilities.
  2. Tracing Back within Sidecar: The next step was to trace the duration variable back to its point of entry into the sidecar system. Examination of the event parsing logic (in operatorDirectedOperatorSetRewardSubmissions.go) revealed that the duration value from the incoming event data was used directly without any validation to ensure it was non-zero.
  3. Checking Data Storage: The database schema was then checked. The duration column was defined as bigint not null. While this prevents null values, it explicitly allows a value of 0. A zero-duration reward could therefore be successfully parsed and stored.
  4. Tracing to the Source (onchain): Finally, the origin of the data was traced back to the onchain RewardsCoordinator.sol contract (though out of the initial sidecar-specific scope, confirming the source is crucial). The validation logic require(duration % CALCULATION_INTERVAL_SECONDS == 0, ...) was identified. This confirmed that a duration of 0 would pass the onchain check, as 0 modulo any non-zero number is 0.
  5. Pattern Analysis: The require(value % interval == 0) check is a common pattern used for various validations, such as ensuring data alignment, checking proof lengths in cryptographic schemes, or enforcing periodic intervals. During development and code review, this check can sometimes be misread as simply ensuring "value is a multiple of interval," implicitly overlooking the edge case where value is zero. However, this incident highlighted the critical consideration that when using this modulo pattern, developers must also explicitly consider whether zero itself is a valid input for value in the specific context. Downstream logic might fail if value is zero (e.g., if used as a divisor). Therefore, a simple modulo check is often insufficient, and an additional check like value > 0 might be necessary (require(value > 0 && value % interval == 0)) to prevent vulnerabilities arising from an unexpected zero input.

Impact: Denial-of-Service (DoS)

A division-by-zero error typically crashes the executing process. In this context, if the sidecar encountered any reward batch containing a submission with duration = 0, the corresponding SQL query would fail. This failure halted the entire reward calculation pipeline for all AVSs and operators dependent on that sidecar instance.

This vulnerability presented a significant Denial-of-Service (DoS) risk. A malicious or accidental submission of a zero-duration reward via RewardsCoordinator.sol could block the sidecar from processing any further rewards, disrupting the ecosystem's reward distribution until manual intervention resolved the problematic data. Given that modules like 1_goldActiveRewards.go and 7_goldActiveODRewards.go were live and processing rewards on mainnet, the potential impact was critical.

Resolution: Fixed Before Exploitation

Fortunately, this vulnerability was identified by security auditors during reviews and was addressed before any known exploitation occurred. The EigenLayer team implemented fixes at multiple layers:

  1. Sidecar Logic: Reward state models in the sidecar were updated to explicitly filter out submissions with duration = 0 before writing them to the database. This prevents invalid data from reaching the vulnerable SQL calculations. (See sidecar PRs #266, #275).
  2. Smart Contract: The RewardsCoordinator.sol contract was patched to add an explicit onchain validation requiring duration > 0. (See eigenlayer-contracts PR #1216).

These fixes ensure that zero-duration rewards are rejected both onchain and defensively within the sidecar, mitigating this specific DoS vector. The EigenLayer rewards system on mainnet is now protected against this issue.