Magic Eggshell Stallion
Medium
https://github.com/withtally/staker/blob/90802966475239c3eb8aafc3dbf18edd5a0b6b1b/src/BinaryEligibilityOracleEarningPowerCalculator.sol#L130-L137
This vulnerability allows the oraclePauseGuardian
role to forcefully pause the oracle logic, thereby triggering fallback behavior that grants every staker 100% earning power—regardless of their delegate’s real governance score. While the Tally ARB Staker is designed to distribute rewards only to stakers whose delegatees meet a certain eligibility threshold, pausing the oracle or allowing it to become stale bypasses this mechanism entirely.
By keeping the system perpetually paused (or failing to unpause), malicious or negligent parties undermine the primary incentive structure of the protocol. This effectively cancels out any differential rewards intended to encourage active governance participation. Although this scenario requires specific role access (i.e., the oraclePauseGuardian
must be compromised, malicious, or coerced), the resulting system failure is severe enough to warrant a medium classification: the exploit is straightforward to carry out but somewhat limited by role-based privileges.
The core contracts (notably BinaryEligibilityOracleEarningPowerCalculator
) rely on the following logic to calculate staker eligibility:
function getEarningPower(
uint256 _amountStaked,
address,
address _delegatee
) external view returns (uint256) {
if (_isOracleStale() || isOraclePaused) return _amountStaked;
return _isDelegateeEligible(_delegatee) ? _amountStaked : 0;
}
Here, _isOracleStale()
is:
function _isOracleStale() internal view returns (bool) {
return block.timestamp - lastOracleUpdateTime > STALE_ORACLE_WINDOW;
}
And pausing is done by the oraclePauseGuardian
via:
function setOracleState(bool _pauseOracle) public {
if (msg.sender != oraclePauseGuardian) {
revert BinaryEligibilityOracleEarningPowerCalculator__Unauthorized(
"not oracle pause guardian", msg.sender
);
}
emit OraclePausedStatusUpdated(isOraclePaused, _pauseOracle);
isOraclePaused = _pauseOracle;
}
Problematic Code Path
- Pausing:
isOraclePaused = true;
- No Score Updates: With the oracle paused (or stale past
STALE_ORACLE_WINDOW
),_isOracleStale()
returnstrue
. - Everyone Eligible:
getEarningPower()
subsequently returns_amountStaked
in all scenarios.
Test Code Snippet (from the exploit demonstration)
// 2. Malicious "pauseGuardian" forcibly pauses the oracle.
vm.prank(pauseGuardian);
calc.setOracleState(true);
// 3. Move forward in time > STALE_ORACLE_WINDOW => oracle becomes "stale".
vm.warp(block.timestamp + STALE_ORACLE_WINDOW + 1);
// 4. Everyone gets full earning power, ignoring real scores.
uint256 badEarningAfterPause = stakingCaller.getEarningPower(
100 ether, stakerWithBadDelegate, address(0xBadDelegatee)
);
// badEarningAfterPause == 100 ether
Invariants dictate that “only stakers whose delegatees meet a certain eligibility score can earn rewards.” By making the system stale or paused indefinitely, this invariant is violated: all stakers are treated as meeting the threshold. This effectively defeats the protocol’s core governance-based reward distribution design.
Manual Review and foundry
-
Restrict or Decentralize Pause Authority
- Require a multisig, DAO vote, or time-delayed mechanism for pausing. This ensures no single address can permanently subvert the protocol.
-
Restrict or Decentralize Pause Authority
- Require a multisig, DAO vote, or time-delayed mechanism for pausing. This ensures no single address can permanently subvert the protocol.