Skip to content

Latest commit

 

History

History
559 lines (497 loc) · 36 KB

040.md

File metadata and controls

559 lines (497 loc) · 36 KB

Spare Wool Mole

Medium

Bumpers can update the earning power of deposits that should be !_isQualifiedForBump after a change in the delegateeEligibilityThresholdScore value.

Summary

Bumpers can update the earning power of deposits that should have become !_isQualifiedForBump because of a change in the delegateeEligibilityThresholdScore value and not enough time elapsed from that moment, so that _isUpdateDelayElapsed == true. This update can be done by bumpers calling the GovernanceStaker::bumpEarningPower sending this way, the _requestedTip from the unclaimed rewards of the deposits to their _tipReceiver.

Relevant GitHub Links

https://github.com/sherlock-audit/2024-11-tally/blob/main/staker/src/GovernanceStaker.sol#L471-L514 https://github.com/sherlock-audit/2024-11-tally/blob/main/staker/src/BinaryEligibilityOracleEarningPowerCalculator.sol#L235-L243 https://github.com/sherlock-audit/2024-11-tally/blob/main/staker/src/BinaryEligibilityOracleEarningPowerCalculator.sol#L146-L161

Vulnerability Detail

Whenever the delegateeEligibilityThresholdScore is changed some delegatees may pass (with the same score) from _isDelegateeEligible to !_isDelegateeEligible because of this change. However, when a delegatee becomes !_isDelegateeEligible in this way, its timeOfIneligibility[_delegatee] is not updated with a block.timestamp of that moment. This prevent a proper calculation of the _isUpdateDelayElapsed = (timeOfIneligibility[_delegatee] + updateEligibilityDelay) <= block.timestamp in the BinaryEligibilityOracleEarningPowerCalculator::getNewEarningPower when is called by bumpers through the GovernanceStaker::bumpEarningPower.

  function bumpEarningPower(
    DepositIdentifier _depositId,
    address _tipReceiver,
    uint256 _requestedTip
  ) external virtual {
    if (_requestedTip > maxBumpTip) revert GovernanceStaker__InvalidTip();

    Deposit storage deposit = deposits[_depositId];

    _checkpointGlobalReward();
    _checkpointReward(deposit);

    uint256 _unclaimedRewards = deposit.scaledUnclaimedRewardCheckpoint / SCALE_FACTOR;

@>  (uint256 _newEarningPower, bool _isQualifiedForBump) =  earningPowerCalculator.getNewEarningPower(
      deposit.balance, deposit.owner, deposit.delegatee, deposit.earningPower
    );
@>  if (!_isQualifiedForBump || _newEarningPower == deposit.earningPower) {
      revert GovernanceStaker__Unqualified(_newEarningPower);
    }

    if (_newEarningPower > deposit.earningPower && _unclaimedRewards < _requestedTip) {
      revert GovernanceStaker__InsufficientUnclaimedRewards();
    }

    // Note: underflow causes a revert if the requested  tip is more than unclaimed rewards
    if (_newEarningPower < deposit.earningPower && (_unclaimedRewards - _requestedTip) < maxBumpTip)
    {
      revert GovernanceStaker__InsufficientUnclaimedRewards();
    }

    // Update global earning power & deposit earning power based on this bump
    totalEarningPower =
      _calculateTotalEarningPower(deposit.earningPower, _newEarningPower, totalEarningPower);
    depositorTotalEarningPower[deposit.owner] = _calculateTotalEarningPower(
      deposit.earningPower, _newEarningPower, depositorTotalEarningPower[deposit.owner]
    );
    deposit.earningPower = _newEarningPower.toUint96();

    // Send tip to the receiver
    SafeERC20.safeTransfer(REWARD_TOKEN, _tipReceiver, _requestedTip);
    deposit.scaledUnclaimedRewardCheckpoint =
      deposit.scaledUnclaimedRewardCheckpoint - (_requestedTip * SCALE_FACTOR);
  }
function getNewEarningPower(
    uint256 _amountStaked,
    address, /* _staker */
    address _delegatee,
    uint256 /* _oldEarningPower */
  ) external view returns (uint256, bool) {
    if (_isOracleStale() || isOraclePaused) return (_amountStaked, true);

    if (!_isDelegateeEligible(_delegatee)) {
@>  bool _isUpdateDelayElapsed =
        (timeOfIneligibility[_delegatee] + updateEligibilityDelay) <= block.timestamp;
      return (0, _isUpdateDelayElapsed);
    }

    return (_amountStaked, true);
  }

In this contest the timeOfIneligibility[delegatee] it's still set to 0 (considering a contest with no previous ineligibilities for that delegatee). Although the updateEligibilityDelay is not elapsed, bumpers can still use GovernanceStaker::bumpEarningPower on deposits having that delegatee which however, should be !_isQualifiedForBump.

Moreover, considering a contest like this where a delegatee has become !_isDelegateeEligibleligible after changes made in the delegateeEligibilityThresholdScore, any further update in the score of delegatee which would not make it eligible again (either made with the BinaryEligibilityOracleEarningPowerCalculator::updateDelegateeScore or BinaryEligibilityOracleEarningPowerCalculator::overrideDelegateeScore) will not correctly update timeOfIneligibility[_delegatee] because there would be a situation where the delegatee would be !_previouslyEligible && !_newlyEligible that is not considered by the BinaryEligibilityOracleEarningPowerCalculator_updateDelegateeScore.

function _updateDelegateeScore(address _delegatee, uint256 _newScore) internal {
    uint256 _oldScore = delegateeScores[_delegatee];
    bool _previouslyEligible = _oldScore >= delegateeEligibilityThresholdScore;
    bool _newlyEligible = _newScore >= delegateeEligibilityThresholdScore;
    emit DelegateeScoreUpdated(_delegatee, _oldScore, _newScore);
    // Record the time if the new score crosses the eligibility threshold.
@>  if (_previouslyEligible && !_newlyEligible) timeOfIneligibility[_delegatee] = block.timestamp;
    delegateeScores[_delegatee] = _newScore;
  }

Impact

The impact of this vulnerability is twofold:

  1. Calling the GovernanceStaker::bumpEarningPower for deposits that should be !_isQualifiedForBump, bumpers can reduce their unclaimed rewards.

  2. The updates in the score of a delegatee (that has become !_isDelegateeEligibleligible because of a change made on the delegateeEligibilityThresholdScore) that would not make it _isDelegateeEligibleligible again, would leave timeOfIneligibility[delegatee] set to 0 breaking in this way a core mechanism of the contract.

POC

// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.23;

import {Vm, Test, stdStorage, StdStorage, console2, stdError} from "forge-std/Test.sol";
import {GovernanceStaker, IERC20, IEarningPowerCalculator} from "src/GovernanceStaker.sol";
import {IERC20Staking} from "src/interfaces/IERC20Staking.sol";
import {DelegationSurrogate} from "src/DelegationSurrogate.sol";
import {GovernanceStakerHarness} from "test/harnesses/GovernanceStakerHarness.sol";
import {GovernanceStakerOnBehalf} from "src/extensions/GovernanceStakerOnBehalf.sol";
import {ERC20VotesMock, ERC20Permit} from "test/mocks/MockERC20Votes.sol";
import {IERC20Errors} from "openzeppelin/interfaces/draft-IERC6093.sol";
import {ERC20Fake} from "test/fakes/ERC20Fake.sol";
import {MockFullEarningPowerCalculator} from "test/mocks/MockFullEarningPowerCalculator.sol";
import {PercentAssertions} from "test/helpers/PercentAssertions.sol";

import {Test, console, console2} from "forge-std/Test.sol";
import {
  BinaryEligibilityOracleEarningPowerCalculator as EarningPowerCalculator,
  Ownable
} from "src/BinaryEligibilityOracleEarningPowerCalculator.sol";

contract EarningPowervsChangeThresholdTest is Test, PercentAssertions {
  address public owner;
  address public scoreOracle;
  uint256 public staleOracleWindow;
  address public oraclePauseGuardian;
  uint256 public delegateeScoreEligibilityThreshold;
  uint256 public updateEligibilityDelay;
  address public delegatee1;
  address public delegatee2;
  address public depositor;
  address public tipReceiver;
  address public bumper;
  EarningPowerCalculator public calculator;

  ERC20Fake rewardToken;
  ERC20VotesMock govToken;
  address admin;
  address rewardNotifier;
  GovernanceStakerHarness govStaker;
  uint256 SCALE_FACTOR;
  
  // console2.log(uint(_domainSeparatorV4()))
  bytes32 EIP712_DOMAIN_SEPARATOR = bytes32(
    uint256(
      100_848_718_687_569_044_464_352_297_364_979_714_567_529_445_102_133_191_562_407_938_263_844_493_123_852
    )
  );
  uint256 maxBumpTip = 1e18;

  bytes32 constant PERMIT_TYPEHASH =
    keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");

  event RewardNotifierSet(address indexed account, bool isEnabled);
  event AdminSet(address indexed oldAdmin, address indexed newAdmin);

  mapping(DelegationSurrogate surrogate => bool isKnown) isKnownSurrogate;
  mapping(address depositor => bool isKnown) isKnownDepositor;
  
  function setUp() public {
    owner = makeAddr("owner");
    scoreOracle = makeAddr("scoreOracle");
    staleOracleWindow = 7 days;
    oraclePauseGuardian = makeAddr("oraclePauseGuardian");
    delegateeScoreEligibilityThreshold = 50;
    updateEligibilityDelay = 3 days;

    calculator = new EarningPowerCalculator(
      owner,
      scoreOracle,
      staleOracleWindow,
      oraclePauseGuardian,
      delegateeScoreEligibilityThreshold,
      updateEligibilityDelay
    );

    // Set the block timestamp to an arbitrary value to avoid introducing assumptions into tests
    // based on a starting timestamp of 0, which is the default.
    uint256 start = block.timestamp;
    vm.warp(start + 2 days);

    rewardToken = new ERC20Fake();
    vm.label(address(rewardToken), "Reward Token");

    govToken = new ERC20VotesMock();
    vm.label(address(govToken), "Governance Token");

    rewardNotifier = address(0xaffab1ebeef);
    vm.label(rewardNotifier, "Reward Notifier");

    vm.label(address(calculator), "Full Earning Power Calculator");

    admin = makeAddr("admin");

    govStaker = new GovernanceStakerHarness(
      rewardToken, govToken, calculator, maxBumpTip, admin, "GovernanceStaker"
    );
    vm.label(address(govStaker), "GovStaker");

    vm.prank(admin);
    govStaker.setRewardNotifier(rewardNotifier, true);

    // Convenience for use in tests
    SCALE_FACTOR = govStaker.SCALE_FACTOR();

    delegatee1 = makeAddr("Delegatee1");
    delegatee2 = makeAddr("Delegatee2");
    depositor = makeAddr("Depositor");
    tipReceiver = makeAddr("tipReceiver");
    bumper = makeAddr("bumper");
  }

  function test_EligibilityvsThresholdChange() public {
  GovernanceStaker.DepositIdentifier depositID1;
  GovernanceStaker.DepositIdentifier depositID2;
  deal(address(govToken), depositor, 100 ether);
  deal(address(rewardToken), address(govStaker), 100 ether);
  
  vm.startPrank(address(scoreOracle));
  calculator.updateDelegateeScore(delegatee1, 60);

  vm.startPrank(depositor);
  govToken.approve(address(govStaker), 20 ether);
  depositID2 = govStaker.stake(10 ether, delegatee2);
  depositID1 = govStaker.stake(10 ether, delegatee1);
  
  vm.startPrank(rewardNotifier);
  govStaker.notifyRewardAmount(10 ether);
  
  vm.warp(block.timestamp + 2 days);
  vm.startPrank(depositor);
  govStaker.withdraw(depositID2, 5 ether);
  
  vm.warp(block.timestamp + 2 days);
  vm.startPrank(owner);
  calculator.setDelegateeScoreEligibilityThreshold(80);
  
  vm.startPrank(depositor);
  uint256 unclaimedrew = govStaker.unclaimedReward(depositID1);
  console.log("unclaimedReward:", unclaimedrew);
  console.log("timeOfIneligibility[delegatee1]:", calculator.timeOfIneligibility(delegatee1));
  
  vm.startPrank(bumper);
  govStaker.bumpEarningPower(depositID1, tipReceiver, 1e17);
  
  vm.startPrank(scoreOracle);
  calculator.updateDelegateeScore(delegatee1, 70);
  console.log("timeOfIneligibility[delegatee1] after the score change:", calculator.timeOfIneligibility(delegatee1));

  assertEq(rewardToken.balanceOf(tipReceiver), 1e17);
  assertEq(calculator.timeOfIneligibility(delegatee1), 0);

  }
  }
Trace
Ran 1 test for test/4th05.t.sol:EarningPowervsChangeThresholdTest
[PASS] test_EligibilityvsThresholdChange() (gas: 1109274)
Logs:
  unclaimedReward: 1333333333333333333
  timeOfIneligibility[delegatee1]: 0
  timeOfIneligibility[delegatee1] after the score change: 0

Traces:
  [7107172] EarningPowervsChangeThresholdTest::setUp()
    ├─ [0] VM::addr(<pk>) [staticcall]
    │   └─ ← [Return] owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266]
    ├─ [0] VM::label(owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], "owner")
    │   └─ ← [Return] 
    ├─ [0] VM::addr(<pk>) [staticcall]
    │   └─ ← [Return] scoreOracle: [0xc1838b9ECEaBA710f564473f92419F38c906ad85]
    ├─ [0] VM::label(scoreOracle: [0xc1838b9ECEaBA710f564473f92419F38c906ad85], "scoreOracle")
    │   └─ ← [Return] 
    ├─ [0] VM::addr(<pk>) [staticcall]
    │   └─ ← [Return] oraclePauseGuardian: [0xD7a74ec423cF3e091f9efFb701a82FC63ca7Ba4e]
    ├─ [0] VM::label(oraclePauseGuardian: [0xD7a74ec423cF3e091f9efFb701a82FC63ca7Ba4e], "oraclePauseGuardian")
    │   └─ ← [Return] 
    ├─ [898650] → new Full Earning Power Calculator@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
    │   ├─ emit OwnershipTransferred(previousOwner: 0x0000000000000000000000000000000000000000, newOwner: owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266])
    │   ├─ emit ScoreOracleSet(oldScoreOracle: 0x0000000000000000000000000000000000000000, newScoreOracle: scoreOracle: [0xc1838b9ECEaBA710f564473f92419F38c906ad85])
    │   ├─ emit OraclePauseGuardianSet(oldOraclePauseGuardian: 0x0000000000000000000000000000000000000000, newOraclePauseGuardian: oraclePauseGuardian: [0xD7a74ec423cF3e091f9efFb701a82FC63ca7Ba4e])
    │   ├─ emit DelegateeEligibilityThresholdScoreSet(oldThreshold: 0, newThreshold: 50)
    │   ├─ emit UpdateEligibilityDelaySet(oldDelay: 0, newDelay: 259200 [2.592e5])
    │   └─ ← [Return] 3784 bytes of code
    ├─ [0] VM::warp(172801 [1.728e5])
    │   └─ ← [Return] 
    ├─ [548670] → new Reward Token@0x2e234DAe75C793f67A35089C9d99245E1C58470b
    │   └─ ← [Return] 2515 bytes of code
    ├─ [0] VM::label(Reward Token: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], "Reward Token")
    │   └─ ← [Return] 
    ├─ [1081652] → new Governance Token@0xF62849F9A0B5Bf2913b396098F7c7019b51A820a
    │   └─ ← [Return] 5171 bytes of code
    ├─ [0] VM::label(Governance Token: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], "Governance Token")
    │   └─ ← [Return] 
    ├─ [0] VM::label(Reward Notifier: [0x00000000000000000000000000000aFFAB1eBEEf], "Reward Notifier")
    │   └─ ← [Return] 
    ├─ [0] VM::label(Full Earning Power Calculator: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], "Full Earning Power Calculator")
    │   └─ ← [Return] 
    ├─ [0] VM::addr(<pk>) [staticcall]
    │   └─ ← [Return] admin: [0xaA10a84CE7d9AE517a52c6d5cA153b369Af99ecF]
    ├─ [0] VM::label(admin: [0xaA10a84CE7d9AE517a52c6d5cA153b369Af99ecF], "admin")
    │   └─ ← [Return] 
    ├─ [4014230] → new GovStaker@0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9
    │   ├─ emit AdminSet(oldAdmin: 0x0000000000000000000000000000000000000000, newAdmin: admin: [0xaA10a84CE7d9AE517a52c6d5cA153b369Af99ecF])
    │   ├─ emit MaxBumpTipSet(oldMaxBumpTip: 0, newMaxBumpTip: 1000000000000000000 [1e18])
    │   ├─ emit EarningPowerCalculatorSet(oldEarningPowerCalculator: 0x0000000000000000000000000000000000000000, newEarningPowerCalculator: Full Earning Power Calculator: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f])
    │   ├─ emit ClaimFeeParametersSet(oldFeeAmount: 0, newFeeAmount: 0, oldFeeCollector: 0x0000000000000000000000000000000000000000, newFeeCollector: 0x0000000000000000000000000000000000000000)
    │   └─ ← [Return] 19657 bytes of code
    ├─ [0] VM::label(GovStaker: [0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9], "GovStaker")
    │   └─ ← [Return] 
    ├─ [0] VM::prank(admin: [0xaA10a84CE7d9AE517a52c6d5cA153b369Af99ecF])
    │   └─ ← [Return] 
    ├─ [24278] GovStaker::setRewardNotifier(Reward Notifier: [0x00000000000000000000000000000aFFAB1eBEEf], true)
    │   ├─ emit RewardNotifierSet(account: Reward Notifier: [0x00000000000000000000000000000aFFAB1eBEEf], isEnabled: true)
    │   └─ ← [Stop] 
    ├─ [273] GovStaker::SCALE_FACTOR() [staticcall]
    │   └─ ← [Return] 1000000000000000000000000000000000000 [1e36]
    ├─ [0] VM::addr(<pk>) [staticcall]
    │   └─ ← [Return] Delegatee1: [0x40040FAB876c6733d5781094F4f22aD993f88313]
    ├─ [0] VM::label(Delegatee1: [0x40040FAB876c6733d5781094F4f22aD993f88313], "Delegatee1")
    │   └─ ← [Return] 
    ├─ [0] VM::addr(<pk>) [staticcall]
    │   └─ ← [Return] Delegatee2: [0xB370260ccED0E101398A032f6B365a5E682D2e1a]
    ├─ [0] VM::label(Delegatee2: [0xB370260ccED0E101398A032f6B365a5E682D2e1a], "Delegatee2")
    │   └─ ← [Return] 
    ├─ [0] VM::addr(<pk>) [staticcall]
    │   └─ ← [Return] Depositor: [0x888d7213BfbfE01A4C88346Eec0381e8903fBa0A]
    ├─ [0] VM::label(Depositor: [0x888d7213BfbfE01A4C88346Eec0381e8903fBa0A], "Depositor")
    │   └─ ← [Return] 
    ├─ [0] VM::addr(<pk>) [staticcall]
    │   └─ ← [Return] tipReceiver: [0x69a6C849c70629d658fa38407f2Cd53f1E92BAd3]
    ├─ [0] VM::label(tipReceiver: [0x69a6C849c70629d658fa38407f2Cd53f1E92BAd3], "tipReceiver")
    │   └─ ← [Return] 
    ├─ [0] VM::addr(<pk>) [staticcall]
    │   └─ ← [Return] bumper: [0xC65829824821e0773dBEA7A496C7Bf010afd1F9e]
    ├─ [0] VM::label(bumper: [0xC65829824821e0773dBEA7A496C7Bf010afd1F9e], "bumper")
    │   └─ ← [Return] 
    └─ ← [Stop] 

  [1109274] EarningPowervsChangeThresholdTest::test_EligibilityvsThresholdChange()
    ├─ [2599] Governance Token::balanceOf(Depositor: [0x888d7213BfbfE01A4C88346Eec0381e8903fBa0A]) [staticcall]
    │   └─ ← [Return] 0
    ├─ [0] VM::record()
    │   └─ ← [Return] 
    ├─ [599] Governance Token::balanceOf(Depositor: [0x888d7213BfbfE01A4C88346Eec0381e8903fBa0A]) [staticcall]
    │   └─ ← [Return] 0
    ├─ [0] VM::accesses(Governance Token: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a])
    │   └─ ← [Return] [0x7d11d99ef3ac0118b9aa1e88ab1942a2da3301d96a68a9e3bb56d2c399f08787], []
    ├─ [0] VM::load(Governance Token: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], 0x7d11d99ef3ac0118b9aa1e88ab1942a2da3301d96a68a9e3bb56d2c399f08787) [staticcall]
    │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000000
    ├─ emit WARNING_UninitedSlot(who: Governance Token: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], slot: 56570644437300003375976572106532207474341764501783339481403678330086963054471 [5.657e76])
    ├─ [0] VM::load(Governance Token: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], 0x7d11d99ef3ac0118b9aa1e88ab1942a2da3301d96a68a9e3bb56d2c399f08787) [staticcall]
    │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000000
    ├─ [599] Governance Token::balanceOf(Depositor: [0x888d7213BfbfE01A4C88346Eec0381e8903fBa0A]) [staticcall]
    │   └─ ← [Return] 0
    ├─ [0] VM::store(Governance Token: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], 0x7d11d99ef3ac0118b9aa1e88ab1942a2da3301d96a68a9e3bb56d2c399f08787, 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff)
    │   └─ ← [Return] 
    ├─ [599] Governance Token::balanceOf(Depositor: [0x888d7213BfbfE01A4C88346Eec0381e8903fBa0A]) [staticcall]
    │   └─ ← [Return] 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77]
    ├─ [0] VM::store(Governance Token: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], 0x7d11d99ef3ac0118b9aa1e88ab1942a2da3301d96a68a9e3bb56d2c399f08787, 0x0000000000000000000000000000000000000000000000000000000000000000)
    │   └─ ← [Return] 
    ├─ emit SlotFound(who: Governance Token: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], fsig: 0x70a0823100000000000000000000000000000000000000000000000000000000, keysHash: 0x7d11d99ef3ac0118b9aa1e88ab1942a2da3301d96a68a9e3bb56d2c399f08787, slot: 56570644437300003375976572106532207474341764501783339481403678330086963054471 [5.657e76])
    ├─ [0] VM::load(Governance Token: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], 0x7d11d99ef3ac0118b9aa1e88ab1942a2da3301d96a68a9e3bb56d2c399f08787) [staticcall]
    │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000000
    ├─ [0] VM::store(Governance Token: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], 0x7d11d99ef3ac0118b9aa1e88ab1942a2da3301d96a68a9e3bb56d2c399f08787, 0x0000000000000000000000000000000000000000000000056bc75e2d63100000)
    │   └─ ← [Return] 
    ├─ [599] Governance Token::balanceOf(Depositor: [0x888d7213BfbfE01A4C88346Eec0381e8903fBa0A]) [staticcall]
    │   └─ ← [Return] 100000000000000000000 [1e20]
    ├─ [2561] Reward Token::balanceOf(GovStaker: [0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9]) [staticcall]
    │   └─ ← [Return] 0
    ├─ [0] VM::record()
    │   └─ ← [Return] 
    ├─ [561] Reward Token::balanceOf(GovStaker: [0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9]) [staticcall]
    │   └─ ← [Return] 0
    ├─ [0] VM::accesses(Reward Token: [0x2e234DAe75C793f67A35089C9d99245E1C58470b])
    │   └─ ← [Return] [0x6aabb5e3159f63e459180f984db14c38dd83524ae5f05f37086f1b3a53e50715], []
    ├─ [0] VM::load(Reward Token: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], 0x6aabb5e3159f63e459180f984db14c38dd83524ae5f05f37086f1b3a53e50715) [staticcall]
    │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000000
    ├─ emit WARNING_UninitedSlot(who: Reward Token: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], slot: 48248548136177130874595238330305211175626856694428216567256805355870241490709 [4.824e76])
    ├─ [0] VM::load(Reward Token: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], 0x6aabb5e3159f63e459180f984db14c38dd83524ae5f05f37086f1b3a53e50715) [staticcall]
    │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000000
    ├─ [561] Reward Token::balanceOf(GovStaker: [0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9]) [staticcall]
    │   └─ ← [Return] 0
    ├─ [0] VM::store(Reward Token: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], 0x6aabb5e3159f63e459180f984db14c38dd83524ae5f05f37086f1b3a53e50715, 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff)
    │   └─ ← [Return] 
    ├─ [561] Reward Token::balanceOf(GovStaker: [0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9]) [staticcall]
    │   └─ ← [Return] 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77]
    ├─ [0] VM::store(Reward Token: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], 0x6aabb5e3159f63e459180f984db14c38dd83524ae5f05f37086f1b3a53e50715, 0x0000000000000000000000000000000000000000000000000000000000000000)
    │   └─ ← [Return] 
    ├─ emit SlotFound(who: Reward Token: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], fsig: 0x70a0823100000000000000000000000000000000000000000000000000000000, keysHash: 0x6aabb5e3159f63e459180f984db14c38dd83524ae5f05f37086f1b3a53e50715, slot: 48248548136177130874595238330305211175626856694428216567256805355870241490709 [4.824e76])
    ├─ [0] VM::load(Reward Token: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], 0x6aabb5e3159f63e459180f984db14c38dd83524ae5f05f37086f1b3a53e50715) [staticcall]
    │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000000
    ├─ [0] VM::store(Reward Token: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], 0x6aabb5e3159f63e459180f984db14c38dd83524ae5f05f37086f1b3a53e50715, 0x0000000000000000000000000000000000000000000000056bc75e2d63100000)
    │   └─ ← [Return] 
    ├─ [561] Reward Token::balanceOf(GovStaker: [0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9]) [staticcall]
    │   └─ ← [Return] 100000000000000000000 [1e20]
    ├─ [0] VM::startPrank(scoreOracle: [0xc1838b9ECEaBA710f564473f92419F38c906ad85])
    │   └─ ← [Return] 
    ├─ [38042] Full Earning Power Calculator::updateDelegateeScore(Delegatee1: [0x40040FAB876c6733d5781094F4f22aD993f88313], 60)
    │   ├─ emit DelegateeScoreUpdated(delegatee: Delegatee1: [0x40040FAB876c6733d5781094F4f22aD993f88313], oldScore: 0, newScore: 60)
    │   └─ ← [Stop] 
    ├─ [0] VM::startPrank(Depositor: [0x888d7213BfbfE01A4C88346Eec0381e8903fBa0A])
    │   └─ ← [Return] 
    ├─ [24735] Governance Token::approve(GovStaker: [0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9], 20000000000000000000 [2e19])
    │   ├─ emit Approval(owner: Depositor: [0x888d7213BfbfE01A4C88346Eec0381e8903fBa0A], spender: GovStaker: [0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9], value: 20000000000000000000 [2e19])
    │   └─ ← [Return] true
    ├─ [306468] GovStaker::stake(10000000000000000000 [1e19], Delegatee2: [0xB370260ccED0E101398A032f6B365a5E682D2e1a])
    │   ├─ [61043] → new DelegationSurrogateVotes@0x5B0091f49210e7B2A57B03dfE1AB9D08289d9294
    │   │   ├─ [24735] Governance Token::approve(GovStaker: [0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9], 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77])
    │   │   │   ├─ emit Approval(owner: DelegationSurrogateVotes: [0x5B0091f49210e7B2A57B03dfE1AB9D08289d9294], spender: GovStaker: [0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9], value: 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77])
    │   │   │   └─ ← [Return] true
    │   │   ├─ [22515] Governance Token::delegate(Delegatee2: [0xB370260ccED0E101398A032f6B365a5E682D2e1a])
    │   │   │   └─ ← [Stop] 
    │   │   └─ ← [Return] 63 bytes of code
    │   ├─ emit SurrogateDeployed(delegatee: Delegatee2: [0xB370260ccED0E101398A032f6B365a5E682D2e1a], surrogate: DelegationSurrogateVotes: [0x5B0091f49210e7B2A57B03dfE1AB9D08289d9294])
    │   ├─ [3260] Full Earning Power Calculator::getEarningPower(10000000000000000000 [1e19], Depositor: [0x888d7213BfbfE01A4C88346Eec0381e8903fBa0A], Delegatee2: [0xB370260ccED0E101398A032f6B365a5E682D2e1a]) [staticcall]
    │   │   └─ ← [Return] 0
    │   ├─ [26017] Governance Token::transferFrom(Depositor: [0x888d7213BfbfE01A4C88346Eec0381e8903fBa0A], DelegationSurrogateVotes: [0x5B0091f49210e7B2A57B03dfE1AB9D08289d9294], 10000000000000000000 [1e19])
    │   │   ├─ emit Transfer(from: Depositor: [0x888d7213BfbfE01A4C88346Eec0381e8903fBa0A], to: DelegationSurrogateVotes: [0x5B0091f49210e7B2A57B03dfE1AB9D08289d9294], value: 10000000000000000000 [1e19])
    │   │   └─ ← [Return] true
    │   ├─ emit StakeDeposited(owner: Depositor: [0x888d7213BfbfE01A4C88346Eec0381e8903fBa0A], depositId: 0, amount: 10000000000000000000 [1e19], depositBalance: 10000000000000000000 [1e19])
    │   ├─ emit ClaimerAltered(depositId: 0, oldClaimer: 0x0000000000000000000000000000000000000000, newClaimer: Depositor: [0x888d7213BfbfE01A4C88346Eec0381e8903fBa0A])
    │   ├─ emit DelegateeAltered(depositId: 0, oldDelegatee: 0x0000000000000000000000000000000000000000, newDelegatee: Delegatee2: [0xB370260ccED0E101398A032f6B365a5E682D2e1a])
    │   └─ ← [Return] 0
    ├─ [266458] GovStaker::stake(10000000000000000000 [1e19], Delegatee1: [0x40040FAB876c6733d5781094F4f22aD993f88313])
    │   ├─ [61043] → new DelegationSurrogateVotes@0xDD4c722d1614128933d6DC7EFA50A6913e804E12
    │   │   ├─ [24735] Governance Token::approve(GovStaker: [0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9], 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77])
    │   │   │   ├─ emit Approval(owner: DelegationSurrogateVotes: [0xDD4c722d1614128933d6DC7EFA50A6913e804E12], spender: GovStaker: [0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9], value: 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77])
    │   │   │   └─ ← [Return] true
    │   │   ├─ [22515] Governance Token::delegate(Delegatee1: [0x40040FAB876c6733d5781094F4f22aD993f88313])
    │   │   │   └─ ← [Stop] 
    │   │   └─ ← [Return] 63 bytes of code
    │   ├─ emit SurrogateDeployed(delegatee: Delegatee1: [0x40040FAB876c6733d5781094F4f22aD993f88313], surrogate: DelegationSurrogateVotes: [0xDD4c722d1614128933d6DC7EFA50A6913e804E12])
    │   ├─ [1250] Full Earning Power Calculator::getEarningPower(10000000000000000000 [1e19], Depositor: [0x888d7213BfbfE01A4C88346Eec0381e8903fBa0A], Delegatee1: [0x40040FAB876c6733d5781094F4f22aD993f88313]) [staticcall]
    │   │   └─ ← [Return] 10000000000000000000 [1e19]
    │   ├─ [26017] Governance Token::transferFrom(Depositor: [0x888d7213BfbfE01A4C88346Eec0381e8903fBa0A], DelegationSurrogateVotes: [0xDD4c722d1614128933d6DC7EFA50A6913e804E12], 10000000000000000000 [1e19])
    │   │   ├─ emit Transfer(from: Depositor: [0x888d7213BfbfE01A4C88346Eec0381e8903fBa0A], to: DelegationSurrogateVotes: [0xDD4c722d1614128933d6DC7EFA50A6913e804E12], value: 10000000000000000000 [1e19])
    │   │   └─ ← [Return] true
    │   ├─ emit StakeDeposited(owner: Depositor: [0x888d7213BfbfE01A4C88346Eec0381e8903fBa0A], depositId: 1, amount: 10000000000000000000 [1e19], depositBalance: 10000000000000000000 [1e19])
    │   ├─ emit ClaimerAltered(depositId: 1, oldClaimer: 0x0000000000000000000000000000000000000000, newClaimer: Depositor: [0x888d7213BfbfE01A4C88346Eec0381e8903fBa0A])
    │   ├─ emit DelegateeAltered(depositId: 1, oldDelegatee: 0x0000000000000000000000000000000000000000, newDelegatee: Delegatee1: [0x40040FAB876c6733d5781094F4f22aD993f88313])
    │   └─ ← [Return] 1
    ├─ [0] VM::startPrank(Reward Notifier: [0x00000000000000000000000000000aFFAB1eBEEf])
    │   └─ ← [Return] 
    ├─ [68862] GovStaker::notifyRewardAmount(10000000000000000000 [1e19])
    │   ├─ [561] Reward Token::balanceOf(GovStaker: [0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9]) [staticcall]
    │   │   └─ ← [Return] 100000000000000000000 [1e20]
    │   ├─ emit RewardNotified(amount: 10000000000000000000 [1e19], notifier: Reward Notifier: [0x00000000000000000000000000000aFFAB1eBEEf])
    │   └─ ← [Stop] 
    ├─ [0] VM::warp(345601 [3.456e5])
    │   └─ ← [Return] 
    ├─ [0] VM::startPrank(Depositor: [0x888d7213BfbfE01A4C88346Eec0381e8903fBa0A])
    │   └─ ← [Return] 
    ├─ [56515] GovStaker::withdraw(0, 5000000000000000000 [5e18])
    │   ├─ [1260] Full Earning Power Calculator::getEarningPower(5000000000000000000 [5e18], Depositor: [0x888d7213BfbfE01A4C88346Eec0381e8903fBa0A], Delegatee2: [0xB370260ccED0E101398A032f6B365a5E682D2e1a]) [staticcall]
    │   │   └─ ← [Return] 0
    │   ├─ [3714] Governance Token::transferFrom(DelegationSurrogateVotes: [0x5B0091f49210e7B2A57B03dfE1AB9D08289d9294], Depositor: [0x888d7213BfbfE01A4C88346Eec0381e8903fBa0A], 5000000000000000000 [5e18])
    │   │   ├─ emit Transfer(from: DelegationSurrogateVotes: [0x5B0091f49210e7B2A57B03dfE1AB9D08289d9294], to: Depositor: [0x888d7213BfbfE01A4C88346Eec0381e8903fBa0A], value: 5000000000000000000 [5e18])
    │   │   └─ ← [Return] true
    │   ├─ emit StakeWithdrawn(owner: Depositor: [0x888d7213BfbfE01A4C88346Eec0381e8903fBa0A], depositId: 0, amount: 5000000000000000000 [5e18], depositBalance: 5000000000000000000 [5e18])
    │   └─ ← [Stop] 
    ├─ [0] VM::warp(518401 [5.184e5])
    │   └─ ← [Return] 
    ├─ [0] VM::startPrank(owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266])
    │   └─ ← [Return] 
    ├─ [6809] Full Earning Power Calculator::setDelegateeScoreEligibilityThreshold(80)
    │   ├─ emit DelegateeEligibilityThresholdScoreSet(oldThreshold: 50, newThreshold: 80)
    │   └─ ← [Stop] 
    ├─ [0] VM::startPrank(Depositor: [0x888d7213BfbfE01A4C88346Eec0381e8903fBa0A])
    │   └─ ← [Return] 
    ├─ [2186] GovStaker::unclaimedReward(1) [staticcall]
    │   └─ ← [Return] 1333333333333333333 [1.333e18]
    ├─ [0] console::log("unclaimedReward:", 1333333333333333333 [1.333e18]) [staticcall]
    │   └─ ← [Stop] 
    ├─ [2574] Full Earning Power Calculator::timeOfIneligibility(Delegatee1: [0x40040FAB876c6733d5781094F4f22aD993f88313]) [staticcall]
    │   └─ ← [Return] 0
    ├─ [0] console::log("timeOfIneligibility[delegatee1]:", 0) [staticcall]
    │   └─ ← [Stop] 
    ├─ [0] VM::startPrank(bumper: [0xC65829824821e0773dBEA7A496C7Bf010afd1F9e])
    │   └─ ← [Return] 
    ├─ [79828] GovStaker::bumpEarningPower(1, tipReceiver: [0x69a6C849c70629d658fa38407f2Cd53f1E92BAd3], 100000000000000000 [1e17])
    │   ├─ [3693] Full Earning Power Calculator::getNewEarningPower(10000000000000000000 [1e19], Depositor: [0x888d7213BfbfE01A4C88346Eec0381e8903fBa0A], Delegatee1: [0x40040FAB876c6733d5781094F4f22aD993f88313], 10000000000000000000 [1e19]) [staticcall]
    │   │   └─ ← [Return] 0, true
    │   ├─ [25046] Reward Token::transfer(tipReceiver: [0x69a6C849c70629d658fa38407f2Cd53f1E92BAd3], 100000000000000000 [1e17])
    │   │   ├─ emit Transfer(from: GovStaker: [0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9], to: tipReceiver: [0x69a6C849c70629d658fa38407f2Cd53f1E92BAd3], value: 100000000000000000 [1e17])
    │   │   └─ ← [Return] true
    │   └─ ← [Stop] 
    ├─ [0] VM::startPrank(scoreOracle: [0xc1838b9ECEaBA710f564473f92419F38c906ad85])
    │   └─ ← [Return] 
    ├─ [3242] Full Earning Power Calculator::updateDelegateeScore(Delegatee1: [0x40040FAB876c6733d5781094F4f22aD993f88313], 70)
    │   ├─ emit DelegateeScoreUpdated(delegatee: Delegatee1: [0x40040FAB876c6733d5781094F4f22aD993f88313], oldScore: 60, newScore: 70)
    │   └─ ← [Stop] 
    ├─ [574] Full Earning Power Calculator::timeOfIneligibility(Delegatee1: [0x40040FAB876c6733d5781094F4f22aD993f88313]) [staticcall]
    │   └─ ← [Return] 0
    ├─ [0] console::log("timeOfIneligibility[delegatee1] after the score change:", 0) [staticcall]
    │   └─ ← [Stop] 
    ├─ [561] Reward Token::balanceOf(tipReceiver: [0x69a6C849c70629d658fa38407f2Cd53f1E92BAd3]) [staticcall]
    │   └─ ← [Return] 100000000000000000 [1e17]
    ├─ [0] VM::assertEq(100000000000000000 [1e17], 100000000000000000 [1e17]) [staticcall]
    │   └─ ← [Return] 
    ├─ [574] Full Earning Power Calculator::timeOfIneligibility(Delegatee1: [0x40040FAB876c6733d5781094F4f22aD993f88313]) [staticcall]
    │   └─ ← [Return] 0
    ├─ [0] VM::assertEq(0, 0) [staticcall]
    │   └─ ← [Return] 
    └─ ← [Stop] 

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.62ms (1.63ms CPU time) 

Tool used

Manual Review

Recommendation

This could be the solution addressing the 2 problems:

function getNewEarningPower(
    uint256 _amountStaked,
    address, /* _staker */
    address _delegatee,
    uint256 /* _oldEarningPower */
  ) external view returns (uint256, bool) {
    if (_isOracleStale() || isOraclePaused) return (_amountStaked, true);

    if (!_isDelegateeEligible(_delegatee)) {
+       if(timeOfIneligibility[_delegatee] == 0){
+       return (0, false);
+       }
        bool _isUpdateDelayElapsed =
        (timeOfIneligibility[_delegatee] + updateEligibilityDelay) <= block.timestamp;
        return (0, _isUpdateDelayElapsed);
    }

    return (_amountStaked, true);
  }
  function _updateDelegateeScore(address _delegatee, uint256 _newScore) internal {
    uint256 _oldScore = delegateeScores[_delegatee];
    bool _previouslyEligible = _oldScore >= delegateeEligibilityThresholdScore;
    bool _newlyEligible = _newScore >= delegateeEligibilityThresholdScore;
    emit DelegateeScoreUpdated(_delegatee, _oldScore, _newScore);
    // Record the time if the new score crosses the eligibility threshold.
-    if (_previouslyEligible && !_newlyEligible) timeOfIneligibility[_delegatee] = block.timestamp;
+    if (_previouslyEligible && !_newlyEligible || !_previouslyEligible && !_newlyEligible && timeOfIneligibility[_delegatee] == 0) timeOfIneligibility[_delegatee] = block.timestamp;
    delegateeScores[_delegatee] = _newScore;
  }