Magic Eggshell Stallion
High
https://github.com/withtally/staker/blob/90802966475239c3eb8aafc3dbf18edd5a0b6b1b/src/BinaryEligibilityOracleEarningPowerCalculator.sol#L130-L137
The Oracle Staleness and Pause Exploit vulnerability exists within the BinaryEligibilityOracleEarningPowerCalculator
contract of the Tally ARB Staker protocol. This flaw allows an attacker to manipulate the system's earning power calculations by either pausing the oracle or causing it to become stale. When triggered, the contract defaults all delegatees to receive 100% earning power, irrespective of their actual eligibility scores. This undermines the protocol's fundamental mechanism of rewarding only eligible participants, enabling unauthorized entities to earn rewards disproportionately or indefinitely. If left unaddressed, this vulnerability can lead to significant financial losses, governance manipulation, and erosion of user trust.
The BinaryEligibilityOracleEarningPowerCalculator
contract determines a staker's earning power based on their delegatee's eligibility score. The core logic is as follows:
if (_isOracleStale() || isOraclePaused) return _amountStaked;
return _isDelegateeEligible(_delegatee) ? _amountStaked : 0;
Mechanism Breakdown:
-
Oracle Staleness Check (
_isOracleStale()
):- Evaluates whether the oracle has failed to update delegatee scores within a predefined window (
STALE_ORACLE_WINDOW
). - If stale, the system defaults to granting full earning power (
_amountStaked
) to all delegatees, bypassing actual eligibility checks.
- Evaluates whether the oracle has failed to update delegatee scores within a predefined window (
-
Oracle Pause Flag (
isOraclePaused
):- A boolean flag controlled by the
oraclePauseGuardian
. - When set to
true
, the system similarly defaults to full earning power for all delegatees.
- A boolean flag controlled by the
Vulnerability Path:
-
Exploitation via Oracle Pause:
- An attacker gains control over the
oraclePauseGuardian
role. - They set
isOraclePaused
totrue
, forcing the contract to grant full earning power universally.
- An attacker gains control over the
-
Exploitation via Oracle Staleness:
- The attacker disrupts the oracle's functionality, preventing it from updating delegatee scores.
- Once the
STALE_ORACLE_WINDOW
elapses without updates, the system automatically grants full earning power to all delegatees.
Test Code:
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.23;
import {Test} from "forge-std/Test.sol";
import {BinaryEligibilityOracleEarningPowerCalculator} from "../src/BinaryEligibilityOracleEarningPowerCalculator.sol";
import {ERC20VotesMock} from "./mocks/MockERC20Votes.sol";
contract OracleVulnerabilityTest is Test {
BinaryEligibilityOracleEarningPowerCalculator calculator;
address owner;
address scoreOracle;
address pauseGuardian;
address attacker;
uint256 staleWindow;
uint256 threshold;
ERC20VotesMock mockToken;
function setUp() public {
owner = makeAddr("owner");
scoreOracle = makeAddr("oracle");
pauseGuardian = makeAddr("guardian");
attacker = makeAddr("attacker");
staleWindow = 1 days;
threshold = 50; // Minimum score needed for eligibility
vm.startPrank(owner);
calculator = new BinaryEligibilityOracleEarningPowerCalculator(
owner,
scoreOracle,
staleWindow,
pauseGuardian,
threshold,
1 hours
);
mockToken = new ERC20VotesMock();
vm.stopPrank();
}
function testExploit_StalenessGrantsFullPower() public {
// Setup: Set initial score below threshold
vm.startPrank(scoreOracle);
calculator.updateDelegateeScore(attacker, 10); // Score well below threshold
vm.stopPrank();
// Initial check - should have zero earning power
assertEq(
calculator.getEarningPower(100, attacker, attacker),
0,
"Should have zero earning power when score below threshold"
);
// Simulate oracle becoming stale
vm.warp(block.timestamp + staleWindow + 1);
// Check after staleness - should now have full earning power
uint256 stakeAmount = 100;
assertEq(
calculator.getEarningPower(stakeAmount, attacker, attacker),
stakeAmount,
"Should have full earning power when oracle is stale"
);
}
function testExploit_PausedOracleGrantsFullPower() public {
// Setup: Set initial score below threshold
vm.startPrank(scoreOracle);
calculator.updateDelegateeScore(attacker, 10);
vm.stopPrank();
// Initial check
assertEq(
calculator.getEarningPower(100, attacker, attacker),
0,
"Should have zero earning power when score below threshold"
);
// Pause oracle
vm.prank(pauseGuardian);
calculator.setOracleState(true);
// Check after pause - should have full earning power
uint256 stakeAmount = 100;
assertEq(
calculator.getEarningPower(stakeAmount, attacker, attacker),
stakeAmount,
"Should have full earning power when oracle is paused"
);
}
}
Test Results:
Ran 2 tests for test/OracleVulnerability.t.sol:OracleVulnerabilityTest [PASS] testExploit_PausedOracleGrantsFullPower() (gas: 60889) [PASS] testExploit_StalenessGrantsFullPower() (gas: 55424) Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 38.21ms (8.45ms CPU time)
Ran 1 test suite in 87.07ms (38.21ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)
Interpretation:
-
testExploit_StalenessGrantsFullPower()
Passed:- Verified that after the oracle becomes stale (i.e., no updates within
STALE_ORACLE_WINDOW
), a delegatee with a score below the threshold (10 < 50
) is erroneously granted full earning power (100
instead of0
).
- Verified that after the oracle becomes stale (i.e., no updates within
-
testExploit_PausedOracleGrantsFullPower()
Passed:- Confirmed that when the oracle is explicitly paused by the
oraclePauseGuardian
, a delegatee with an insufficient score similarly receives full earning power.
- Confirmed that when the oracle is explicitly paused by the
These results conclusively demonstrate that both exploitation vectors—oracle staleness and oracle pausing—effectively bypass the intended eligibility checks, validating the presence of the vulnerability.
-
Bypassing Eligibility Checks:
- The contract's fallback mechanism prioritizes system availability over security, defaulting to full earning power when the oracle's state is compromised or inactive.
-
Role Exploitation:
- Control over the
oraclePauseGuardian
role or the ability to induce oracle staleness provides an attacker with the means to exploit the system's fallback logic.
- Control over the
-
System Invariant Violation:
- The protocol's invariant that only eligible delegatees should receive rewards is violated when earning power defaults to full, irrespective of actual delegatee scores.
-
Impact on Core Functionality:
- The vulnerability directly affects the reward distribution mechanism, a cornerstone of the staking protocol, leading to potential economic abuse and governance manipulation.
The Oracle Staleness and Pause Exploit has profound implications for the Tally ARB Staker protocol:
-
Financial Exploitation:
- Unlimited Reward Accumulation: Attackers can continuously earn rewards by maintaining the oracle in a stale or paused state, disregarding actual delegatee eligibility.
- Resource Drain: Legitimate stakers with low or zero eligibility scores can unjustly siphon rewards meant for active, eligible participants.
-
Governance Manipulation:
- Distorted Voting Power: Full earning power may inadvertently or deliberately grant disproportionate governance influence to ineligible or malicious delegates.
- Erosion of Trust: Users may lose confidence in the protocol's fairness and security, leading to reduced participation and potential exit.
-
System Integrity Compromise:
- Economic Imbalance: The protocol's reward distribution becomes skewed, undermining the economic incentives designed to promote active governance participation.
- Potential for Repeated Exploits: Persistent toggling of the oracle state can create an ongoing vulnerability window, exacerbating the protocol's exposure.
Manual Review and Foundry
To mitigate the Oracle Staleness and Pause Exploit, the following actionable steps are recommended:
-
Default to Zero Earning Power:
- Modify the
getEarningPower
function to return zero earning power when the oracle is stale or paused, rather than granting full earning power.
if (_isOracleStale() || isOraclePaused) return 0; return _isDelegateeEligible(_delegatee) ? _amountStaked : 0;
- Modify the
-
Conditional Reward Accrual:
- Implement a mechanism to halt reward distribution entirely if the oracle is stale or paused, preventing any earning regardless of delegatee scores.