Severe Division-by-Zero Bug in EigenLayer Sidecar Rewards: Examination and Fix
tldr: A division-by-zero bug in EigenLayer's sidecar reward reckoning 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.
Primer
EigenLayer brings in restaking to Ethereum, allowing staked assets to secure other applications known as Actively Validated Services (AVSs). This intricate ecosystem relies on both onchain smart contracts and off-chain pieces (sidecar) for tasks like data aggregation and reward computations. This write-up details a high-severity division-by-zero bug found in the EigenLayer sidecar's reward processing logic, explaining its root cause, plausible 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, runs computations (like reward reckonings), 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 work out 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 covers parameters like amount and the duration (in seconds) over which the reward was earned. The contract carried out 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 particular 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 computation 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 immediately from the event payload without checking if it was non-zero.
// 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 ...
}Further, the PostgreSQL database schema employed 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 so be successfully parsed and stored.
2. Division by Zero in SQL Reckoning:
Subsequent reward processing steps involved SQL queries that divided by the stored duration to work out 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))- : (Alike 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 high-severity division-by-zero error during SQL execution.
Discovery Process
The discovery of this vulnerability followed a standard security review process, starting from possible sink points and tracing back to the source:
- Identifying the Sink: While reviewing the SQL queries within the sidecar's reward computation logic (e.g.,
1_goldActiveRewards.go,7_goldActiveODRewards.go,11_goldActiveODOperatorSetRewards.go), the division operation involving thedurationDivision operations are common areas to check for such vulnerabilities. variable was flagged as a plausible division-by-zero risk. - Tracing Back within Sidecar: The next step was to trace the
durationvariable back to its point of entry into the sidecar system. Examination of the event parsing logic (inoperatorDirectedOperatorSetRewardSubmissions.go) revealed that thedurationvalue from the incoming event data was employed immediately without any validation to keep it was non-zero. - Checking Data Storage: The database schema was then checked. The
durationcolumn was defined asbigint not null. While this prevents null values, it explicitly allows a value of0. A zero-duration reward could hence be successfully parsed and stored. - Tracing to the Source (onchain): Finally, the origin of the data was traced back to the onchain
RewardsCoordinator.solcontract (though out of the initial sidecar-specific scope, confirming the source is crucial). The validation logicrequire(duration % CALCULATION_INTERVAL_SECONDS == 0, ...)was spotted. This confirmed that adurationof 0 would pass the onchain check, as 0 modulo any non-zero number is 0. - Pattern Study: The
require(value % interval == 0)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. check is a common pattern applied for a range of validations, such as ensuring data alignment, checking proof lengths in cryptographic schemes, or enforcing periodic intervals. Still, this incident highlighted the severe consideration that when via this modulo pattern, developers must also explicitly look at whether zero itself is a valid input forvaluein the given context. Downstream logic might fail ifvalueis zero (e.g., if applied as a divisor). So, a plain modulo check is often insufficient, and an extra check likevalue > 0might be required (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 usually crashes the executing process. In this context, if the sidecar encountered any reward batch containing a submission with duration = 0, the paired SQL query would fail. This failure halted the whole reward reckoning 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 possible impact was severe.
Resolution: Fixed Before Exploitation
Fortunately, this vulnerability was flagged by security auditors during reviews and was addressed before any known exploitation took place. The EigenLayer team implemented fixes at multiple layers:
- Smart Contract: The
RewardsCoordinator.solcontract was patched to add an explicit onchain validation requiringduration > 0. (See eigenlayer-contracts PR ).
The EigenLayer rewards system on mainnet is now protected against this issue. These fixes make sure that zero-duration rewards are rejected both onchain and defensively within the sidecar, mitigating this particular DoS vector.