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 (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, ...
- 7_goldActiveODRewards.go#L160: (Occurred when
num_registered_snapshots
was 0)
-- Inside _7_goldActiveODRewardsQuery (within CASE statement)
WHEN ar.num_registered_snapshots = 0 THEN floor(ar.amount_decimal / (duration / 86400))
- 11_goldActiveODOperatorSetRewards.go#L165: (Similar logic as above)
-- 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:
- 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 theduration
variable was flagged as a potential division-by-zero risk. Division operations are common areas to check for such vulnerabilities. - 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 (inoperatorDirectedOperatorSetRewardSubmissions.go
) revealed that theduration
value from the incoming event data was used directly without any validation to ensure it was non-zero. - Checking Data Storage: The database schema was then checked. The
duration
column was defined asbigint not null
. While this prevents null values, it explicitly allows a value of0
. A zero-duration reward could therefore be successfully parsed and stored. - 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 logicrequire(duration % CALCULATION_INTERVAL_SECONDS == 0, ...)
was identified. This confirmed that aduration
of 0 would pass the onchain check, as 0 modulo any non-zero number is 0. - 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 forvalue
in the specific context. Downstream logic might fail ifvalue
is zero (e.g., if used as a divisor). Therefore, a simple modulo check is often insufficient, and an additional check likevalue > 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:
- 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). - Smart Contract: The
RewardsCoordinator.sol
contract was patched to add an explicit onchain validation requiringduration > 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.