Spare Wool Mole
Medium
Bumpers
can update the earning power
of deposits
that should be !_isQualifiedForBump
after a change in the delegateeEligibilityThresholdScore
value.
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
.
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
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;
}
The impact of this vulnerability is twofold:
-
Calling the
GovernanceStaker::bumpEarningPower
fordeposits
that should be!_isQualifiedForBump
,bumpers
can reduce theirunclaimed rewards
. -
The updates in the
score
of adelegatee
(that has become!_isDelegateeEligibleligible
because of a change made on thedelegateeEligibilityThresholdScore
) that would not make it_isDelegateeEligibleligible
again, would leavetimeOfIneligibility[delegatee]
set to0
breaking in this way a core mechanism of the contract.
// 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)
Manual Review
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;
}