From 09d146a76937be5dcc875250ab524be06fdf4961 Mon Sep 17 00:00:00 2001 From: garyghayrat Date: Thu, 26 Sep 2024 09:19:17 -0400 Subject: [PATCH 1/5] Add `EarningPowerCalculator` and tests Make `delegateScoreEligibilityThreshold` updatable Add missing test and update comment Merge lockDelegateScore and unlockDelegateScore and update tests Rename delegate to delegatee Rename `_isEligibleForUpdate` to `_isQualifiedForUpdate` Rename `NotScoreOracle` to `Unauthorized` Rename `EarningPowerCalculator` to `BinaryEligibilityOracleEarningPowerCalculator` and inherit `IEarningPowerCalculator` Update comments around ScoreOracle, Karma, and reorder internal function Rename tests Add `lastDelegateeEligibilityChangeTime` and tests Use local vars to simplify checking change in eligibility --- ...ligibilityOracleEarningPowerCalculator.sol | 232 +++++++ src/interfaces/IEarningPowerCalculator.sol | 15 + ...gibilityOracleEarningPowerCalculator.t.sol | 591 ++++++++++++++++++ 3 files changed, 838 insertions(+) create mode 100644 src/BinaryEligibilityOracleEarningPowerCalculator.sol create mode 100644 test/BinaryEligibilityOracleEarningPowerCalculator.t.sol diff --git a/src/BinaryEligibilityOracleEarningPowerCalculator.sol b/src/BinaryEligibilityOracleEarningPowerCalculator.sol new file mode 100644 index 0000000..64fe895 --- /dev/null +++ b/src/BinaryEligibilityOracleEarningPowerCalculator.sol @@ -0,0 +1,232 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.23; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {IEarningPowerCalculator} from "src/interfaces/IEarningPowerCalculator.sol"; + +/// @title BinaryEligibilityOracleEarningPowerCalculator +/// @author [ScopeLift](https://scopelift.co) +/// @notice This contract calculates the earning power of a staker based on their delegatee's score. +contract BinaryEligibilityOracleEarningPowerCalculator is Ownable, IEarningPowerCalculator { + /// @notice Emitted when a delegatee's score is updated. + /// @param delegatee The address of the delegatee whose score was updated. + /// @param oldScore The previous score of the delegatee. + /// @param newScore The new score assigned to the delegatee. + event DelegateeScoreUpdated(address indexed delegatee, uint256 oldScore, uint256 newScore); + + /// @notice Emitted when a delegatee's score lock status is updated. + /// @param delegatee The address of the delegatee whose score lock status was updated. + /// @param oldState The previous lock state of the delegatee's score. + /// @param newState The new lock state of the delegatee's score. + event DelegateeScoreLockSet(address indexed delegatee, bool oldState, bool newState); + + /// @notice Emitted when the ScoreOracle address is updated. + /// @param oldScoreOracle The address of the previous ScoreOracle. + /// @param newScoreOracle The address of the new ScoreOracle. + event ScoreOracleSet(address indexed oldScoreOracle, address indexed newScoreOracle); + + /// @notice Emitted when the update eligibility delay is changed. + /// @param oldDelay The previous update eligibility delay value. + /// @param newDelay The new update eligibility delay value. + event UpdateEligibilityDelaySet(uint256 oldDelay, uint256 newDelay); + + /// @notice Emitted when the delegatee score eligibility threshold to earn the full earning power + /// for its stakers is updated. + /// @param oldThreshold The previous threshold value. + /// @param newThreshold The new threshold value set. + event DelegateeScoreEligibilityThresholdSet(uint256 oldThreshold, uint256 newThreshold); + + /// @notice Error thrown when a non-scoreOracle address tries to call the updateDelegateeScore + /// function. + error Unauthorized(bytes32 reason, address caller); + + /// @notice Error thrown when an attempt is made to update the score of a locked delegatee score. + error DelegateeScoreLocked(address delegatee); + + /// @notice After the 7-day stale oracle window, all stakers' earning power will be set to 100% of + /// their staked amounts. + uint256 public constant STALE_ORACLE_WINDOW = 7 days; // TODO: Update to appropriate time frame. + + /// @notice The address with the authority to update delegatee scores. + address public scoreOracle; + + /// @notice The timestamp of the last delegatee score update. + uint256 public lastOracleUpdateTime; + + /// @notice The minimum score a delegatee must have to be eligible for earning power. + /// @dev This threshold is used in the getEarningPower and getNewEarningPower functions. + /// @dev Delegates with scores below this threshold will have an earning power of 0, and scores + /// equal or above this threshold will have 100% earning power equal to the staked amount. + uint256 public delegateeScoreEligibilityThreshold; + + /// @notice The minimum delay required between staker earning power updates. + /// @dev This delay helps stakers react to changes in their delegatee's score without immediate + /// impact to their earning power. + uint256 public updateEligibilityDelay; + + /// @notice Mapping to store delegatee scores. + mapping(address delegatee => uint256 delegateeScore) public delegateeScores; + + /// @notice Mapping to store the last score update timestamp where a delegatee's eligibility + /// changed. + /// @dev Key is the delegatee's address, value is the block.timestamp of the last eligibility + /// update. + mapping(address delegatee => uint256 timestamp) public lastDelegateeEligibilityChangeTime; + + /// @notice Mapping to store the lock status of delegate scores. + mapping(address delegate => bool isLocked) public delegateeScoreLock; + + /// @notice Initializes the EarningPowerCalculator contract. + /// @param _owner The DAO governor address. + /// @param _scoreOracle The address of the trusted Karma address. + /// @param _delegateeScoreEligibilityThreshold The threshold for delegatee score eligibility to + /// have + /// the full earning power. + /// @param _updateEligibilityDelay The delay required between delegatee earning power + /// updates after falling below the eligibility threshold. + constructor( + address _owner, + address _scoreOracle, + uint256 _delegateeScoreEligibilityThreshold, + uint256 _updateEligibilityDelay + ) Ownable(_owner) { + scoreOracle = _scoreOracle; + delegateeScoreEligibilityThreshold = _delegateeScoreEligibilityThreshold; + updateEligibilityDelay = _updateEligibilityDelay; + + emit ScoreOracleSet(address(0), _scoreOracle); + emit DelegateeScoreEligibilityThresholdSet(0, _delegateeScoreEligibilityThreshold); + emit UpdateEligibilityDelaySet(0, _updateEligibilityDelay); + } + + /// @notice Calculates the earning power for a given delegatee and staking amount. + /// @param _amountStaked The amount of tokens staked. + /// @param _staker The address of the staker. + /// @param _delegatee The address of the delegatee. + /// @return _earningPower The calculated earning power. + function getEarningPower(uint256 _amountStaked, address _staker, address _delegatee) + external + view + returns (uint256 _earningPower) + { + // If the oracle has not been updated for more than the stale oracle window, return full earning + // power and is qualified for update. + if (block.timestamp - lastOracleUpdateTime > STALE_ORACLE_WINDOW) _earningPower = _amountStaked; + // If the delegatee's score is below the eligibility threshold, return 0 earning power. + else if (delegateeScores[_delegatee] < delegateeScoreEligibilityThreshold) _earningPower = 0; + // If the delegatee's score is above the eligibility threshold, return full earning power. + else _earningPower = _amountStaked; + } + + /// @notice Calculates the new earning power and determines if it qualifies for an update.` + /// @param _amountStaked The amount of tokens staked. + /// @param _staker The address of the staker. + /// @param _delegatee The address of the delegatee. + /// @param _oldEarningPower The previous earning power value. + /// @return _newEarningPower The newly calculated earning power. + /// @return _isQualifiedForUpdate Boolean indicating if the new earning power qualifies for an + /// update. + function getNewEarningPower( + uint256 _amountStaked, + address _staker, + address _delegatee, + uint256 _oldEarningPower + ) external view returns (uint256 _newEarningPower, bool _isQualifiedForUpdate) { + // If the oracle has not been updated for more than the stale oracle window, return full earning + // power and is qualified for update. + if (block.timestamp - lastOracleUpdateTime > STALE_ORACLE_WINDOW) { + _newEarningPower = _amountStaked; + _isQualifiedForUpdate = true; + // If the delegatee's score is below the eligibility threshold, return 0 earning power. + } else if (delegateeScores[_delegatee] < delegateeScoreEligibilityThreshold) { + // check if the update eligibility has passed, if it hasn't, it's not qualified for an update. + if ( + (lastDelegateeEligibilityChangeTime[_delegatee] + updateEligibilityDelay) > block.timestamp + ) _isQualifiedForUpdate = false; + // If the update eligibility has passed then it's qualified for an update. + else _isQualifiedForUpdate = true; + _newEarningPower = 0; + // If the delegatee's score is above the eligibility threshold, return full earning power and + // is qualified for an update. + } else { + _newEarningPower = _amountStaked; + _isQualifiedForUpdate = true; + } + } + + /// @notice Updates the score of a delegatee. + /// @dev This function can only be called by the authorized scoreOracle address. + /// @dev If the delegatee's score is locked, the update will be reverted. + /// @param _delegatee The address of the delegatee whose score is being updated. + /// @param _newScore The new score to be assigned to the delegatee. + function updateDelegateeScore(address _delegatee, uint256 _newScore) public { + if (msg.sender != scoreOracle) revert Unauthorized("not oracle", msg.sender); + if (delegateeScoreLock[_delegatee]) revert DelegateeScoreLocked(_delegatee); + _updateDelegateeScore(_delegatee, _newScore); + lastOracleUpdateTime = block.timestamp; + } + + /// @notice Overrides the score of a delegatee and locks it. + /// @dev This function can only be called by the contract owner. + /// @dev It updates the delegatee's score and then locks it to prevent further updates by the + /// scoreOracle. + /// @param _delegatee The address of the delegatee whose score is being overridden. + /// @param _newScore The new score to be assigned to the delegatee. + function overrideDelegateeScore(address _delegatee, uint256 _newScore) public onlyOwner { + _updateDelegateeScore(_delegatee, _newScore); + setDelegateeScoreLock(_delegatee, true); + } + + /// @notice Sets or removes the lock on a delegatee's score. + /// @dev This function can only be called by the contract owner. + /// @dev When a delegatee's score is locked, it cannot be updated by the scoreOracle. + /// @dev This function is useful for manually overriding and protecting a delegatee's score. + /// @param _delegatee The address of the delegatee whose score lock status is being modified. + /// @param _isLocked The new lock status to set. True to lock the score, false to unlock. + function setDelegateeScoreLock(address _delegatee, bool _isLocked) public onlyOwner { + emit DelegateeScoreLockSet(_delegatee, delegateeScoreLock[_delegatee], _isLocked); + delegateeScoreLock[_delegatee] = _isLocked; + } + + /// @notice Sets a new address as the ScoreOracle contract. + /// @dev This function can only be called by the contract owner. + /// @param _newScoreOracle The address of the new ScoreOracle contract. + function setScoreOracle(address _newScoreOracle) public onlyOwner { + emit ScoreOracleSet(scoreOracle, _newScoreOracle); + scoreOracle = _newScoreOracle; + } + + /// @notice Sets a new update eligibility delay. + /// @dev This function can only be called by the contract owner. + /// @param _newUpdateEligibilityDelay The new delay value to set. + function setUpdateEligibilityDelay(uint256 _newUpdateEligibilityDelay) public onlyOwner { + emit UpdateEligibilityDelaySet(updateEligibilityDelay, _newUpdateEligibilityDelay); + updateEligibilityDelay = _newUpdateEligibilityDelay; + } + + function setDelegateeScoreEligibilityThreshold(uint256 _newDelegateeScoreEligibilityThreshold) + public + onlyOwner + { + emit DelegateeScoreEligibilityThresholdSet( + delegateeScoreEligibilityThreshold, _newDelegateeScoreEligibilityThreshold + ); + delegateeScoreEligibilityThreshold = _newDelegateeScoreEligibilityThreshold; + } + + /// @notice Internal function to update a delegatee's score. + /// @dev This function updates the delegatee's score, emits an event, and records the update time. + /// @param _delegatee The address of the delegatee whose score is being updated. + /// @param _newScore The new score to be assigned to the delegatee. + function _updateDelegateeScore(address _delegatee, uint256 _newScore) internal { + uint256 _currentDelegateeScore = delegateeScores[_delegatee]; + bool _currentlyEligible = _currentDelegateeScore >= delegateeScoreEligibilityThreshold; + bool _newlyEligible = _newScore >= delegateeScoreEligibilityThreshold; + emit DelegateeScoreUpdated(_delegatee, _currentDelegateeScore, _newScore); + // Record the time if the new score crosses the eligibility threshold. + if (_currentlyEligible != _newlyEligible) { + lastDelegateeEligibilityChangeTime[_delegatee] = block.timestamp; + } + delegateeScores[_delegatee] = _newScore; + } +} diff --git a/src/interfaces/IEarningPowerCalculator.sol b/src/interfaces/IEarningPowerCalculator.sol index 49be579..43f9e65 100644 --- a/src/interfaces/IEarningPowerCalculator.sol +++ b/src/interfaces/IEarningPowerCalculator.sol @@ -1,12 +1,27 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity ^0.8.23; +/// @notice Interface for calculating earning power of a staker based on their delegate score in +/// GovernanceStaker. interface IEarningPowerCalculator { + /// @notice Calculates the earning power for a given delegate and staking amount. + /// @param _amountStaked The amount of tokens staked. + /// @param _staker The address of the staker. + /// @param _delegatee The address of the delegatee. + /// @return _earningPower The calculated earning power. function getEarningPower(uint256 _amountStaked, address _staker, address _delegatee) external view returns (uint256 _earningPower); + /// @notice Calculates the new earning power and determines if it qualifies for an update.` + /// @param _amountStaked The amount of tokens staked. + /// @param _staker The address of the staker. + /// @param _delegatee The address of the delegatee. + /// @param _oldEarningPower The previous earning power value. + /// @return _newEarningPower The newly calculated earning power. + /// @return _isQualifiedForUpdate Boolean indicating if the new earning power qualifies for an + /// update. function getNewEarningPower( uint256 _amountStaked, address _staker, diff --git a/test/BinaryEligibilityOracleEarningPowerCalculator.t.sol b/test/BinaryEligibilityOracleEarningPowerCalculator.t.sol new file mode 100644 index 0000000..9fc9957 --- /dev/null +++ b/test/BinaryEligibilityOracleEarningPowerCalculator.t.sol @@ -0,0 +1,591 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.23; + +import {Test, console2} from "forge-std/Test.sol"; +import {BinaryEligibilityOracleEarningPowerCalculator as EarningPowerCalculator} from + "src/BinaryEligibilityOracleEarningPowerCalculator.sol"; + +contract EarningPowerCalculatorTest is Test { + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + event DelegateeScoreUpdated(address indexed delegatee, uint256 oldScore, uint256 newScore); + event DelegateeScoreLockSet(address indexed delegatee, bool oldState, bool newState); + event ScoreOracleSet(address indexed oldScoreOracle, address indexed newScoreOracle); + event UpdateEligibilityDelaySet(uint256 oldDelay, uint256 newDelay); + event DelegateeScoreEligibilityThresholdSet(uint256 oldThreshold, uint256 newThreshold); + + error OwnableUnauthorizedAccount(address account); + + address public owner; + address public scoreOracle; + uint256 public delegateeScoreEligibilityThreshold; + uint256 public updateEligibilityDelay; + EarningPowerCalculator public calculator; + + function setUp() public { + owner = makeAddr("owner"); + scoreOracle = makeAddr("scoreOracle"); + delegateeScoreEligibilityThreshold = 50; + updateEligibilityDelay = 7 days; + + calculator = new EarningPowerCalculator( + owner, scoreOracle, delegateeScoreEligibilityThreshold, updateEligibilityDelay + ); + } +} + +contract Constructor is EarningPowerCalculatorTest { + function test_SetsOwnerAndContractParametersCorrectly() public view { + assertEq(calculator.owner(), owner); + assertEq(calculator.scoreOracle(), scoreOracle); + assertEq(calculator.delegateeScoreEligibilityThreshold(), delegateeScoreEligibilityThreshold); + assertEq(calculator.updateEligibilityDelay(), updateEligibilityDelay); + } + + function testFuzz_SetsOwnerAndContractParametersToArbitraryValues( + address _owner, + address _scoreOracle, + uint256 _delegateeScoreEligibilityThreshold, + uint256 _updateEligibilityDelay + ) public { + vm.assume(_owner != address(0)); + EarningPowerCalculator _calculator = new EarningPowerCalculator( + _owner, _scoreOracle, _delegateeScoreEligibilityThreshold, _updateEligibilityDelay + ); + assertEq(_calculator.owner(), _owner); + assertEq(_calculator.scoreOracle(), _scoreOracle); + assertEq(_calculator.delegateeScoreEligibilityThreshold(), _delegateeScoreEligibilityThreshold); + assertEq(_calculator.updateEligibilityDelay(), _updateEligibilityDelay); + } + + function testFuzz_EmitsEventsWhenOwnerAndContractParametersAreSetToArbitraryValues( + address _owner, + address _scoreOracle, + uint256 _delegateeScoreEligibilityThreshold, + uint256 _updateEligibilityDelay + ) public { + vm.assume(_owner != address(0)); + + vm.expectEmit(); + emit OwnershipTransferred(address(0), _owner); + vm.expectEmit(); + emit ScoreOracleSet(address(0), _scoreOracle); + vm.expectEmit(); + emit DelegateeScoreEligibilityThresholdSet(0, _delegateeScoreEligibilityThreshold); + vm.expectEmit(); + emit UpdateEligibilityDelaySet(0, _updateEligibilityDelay); + + EarningPowerCalculator _calculator = new EarningPowerCalculator( + _owner, _scoreOracle, _delegateeScoreEligibilityThreshold, _updateEligibilityDelay + ); + } +} + +contract GetEarningPower is EarningPowerCalculatorTest { + function testFuzz_ReturnsAmountStakedAsEarningPowerWhenStaleOracleWindowHasPassed( + uint256 _amountStaked, + address _staker, + address _delegatee, + uint256 _delegateeScore, + uint256 _timeSinceLastOracleUpdate + ) public { + _timeSinceLastOracleUpdate = bound( + _timeSinceLastOracleUpdate, + calculator.STALE_ORACLE_WINDOW() + 1, + type(uint256).max - block.timestamp + ); + vm.prank(scoreOracle); + calculator.updateDelegateeScore(_delegatee, _delegateeScore); + + vm.warp(block.timestamp + _timeSinceLastOracleUpdate); + assertEq(calculator.getEarningPower(_amountStaked, _staker, _delegatee), _amountStaked); + } + + function testFuzz_ReturnsZeroEarningPowerWhenDelegateeScoreIsBelowEligibilityThreshold( + uint256 _delegateeScore, + uint256 _amountStaked, + address _staker, + address _delegatee + ) public { + _delegateeScore = bound(_delegateeScore, 0, calculator.delegateeScoreEligibilityThreshold() - 1); + vm.prank(scoreOracle); + calculator.updateDelegateeScore(_delegatee, _delegateeScore); + assertEq(calculator.getEarningPower(_amountStaked, _staker, _delegatee), 0); + } + + function testFuzz_ReturnsAmountStakedAsEarningPowerWhenDelegateScoreIsAboveTheEligibilityThreshold( + uint256 _delegateeScore, + uint256 _amountStaked, + address _staker, + address _delegatee + ) public { + _delegateeScore = + bound(_delegateeScore, calculator.delegateeScoreEligibilityThreshold() + 1, type(uint256).max); + vm.prank(scoreOracle); + calculator.updateDelegateeScore(_delegatee, _delegateeScore); + assertEq(calculator.getEarningPower(_amountStaked, _staker, _delegatee), _amountStaked); + } +} + +contract GetNewEarningPower is EarningPowerCalculatorTest { + function testFuzz_ReturnsAmountStakedAndEligibleWhenStaleOracleWindowHasPassed( + uint256 _amountStaked, + address _staker, + address _delegatee, + uint256 _oldEarningPower, + uint256 _delegateeScore, + uint256 _timeSinceLastOracleUpdate + ) public { + _timeSinceLastOracleUpdate = bound( + _timeSinceLastOracleUpdate, + calculator.STALE_ORACLE_WINDOW() + 1, + type(uint256).max - block.timestamp + ); + vm.prank(scoreOracle); + calculator.updateDelegateeScore(_delegatee, _delegateeScore); + + vm.warp(block.timestamp + _timeSinceLastOracleUpdate); + (uint256 _earningPower, bool _isQualifiedForUpdate) = + calculator.getNewEarningPower(_amountStaked, _staker, _delegatee, _oldEarningPower); + assertEq(_earningPower, _amountStaked); + assertEq(_isQualifiedForUpdate, true); + } + + // Test Case: + // 1. Score >= Threshold: Delegatee is Eligible for full earning power. + // 2. Score < Threshold: Updated delegatee score falls below threshold. + // 4. _updateEligibilityDelay is NOT reached. + // Result: We expect 0 earning power and false for _isQualifiedForUpdate. + function testFuzz_ReturnsZeroEarningPowerAndNotEligibleWhenDelegateScoreFallsBelowEligibilityThresholdButWithinTheUpdateEligibilityDelay( + uint256 _amountStaked, + address _staker, + address _delegatee, + uint256 _oldEarningPower, + uint256 _delegateeScoreAboveThreshold, + uint256 _newDelegateeScoreBelowThreshold, + uint256 _timeShorterThanUpdateEligibilityDelay + ) public { + _delegateeScoreAboveThreshold = bound( + _delegateeScoreAboveThreshold, + calculator.delegateeScoreEligibilityThreshold(), + type(uint256).max + ); + _newDelegateeScoreBelowThreshold = bound( + _newDelegateeScoreBelowThreshold, 0, calculator.delegateeScoreEligibilityThreshold() - 1 + ); + // time shorter than the eligibility delay but at least 1 because vm.wrap can't take 0. + _timeShorterThanUpdateEligibilityDelay = + bound(_timeShorterThanUpdateEligibilityDelay, 1, calculator.updateEligibilityDelay() - 1); + + vm.startPrank(scoreOracle); + // First becomes an eligible delegatee + calculator.updateDelegateeScore(_delegatee, _delegateeScoreAboveThreshold); + // But then falls below the threshold. + calculator.updateDelegateeScore(_delegatee, _newDelegateeScoreBelowThreshold); + vm.stopPrank(); + + vm.warp( + calculator.lastDelegateeEligibilityChangeTime(_delegatee) + + _timeShorterThanUpdateEligibilityDelay + ); + (uint256 _earningPower, bool _isQualifiedForUpdate) = + calculator.getNewEarningPower(_amountStaked, _staker, _delegatee, _oldEarningPower); + assertEq(_earningPower, 0); + assertEq(_isQualifiedForUpdate, false); + } + + function testFuzz_ReturnsZeroEarningPowerAndEligibleWhenDelegateScoreIsBelowEligibilityThresholdButOutsideTheUpdateEligibilityDelay( + uint256 _amountStaked, + address _staker, + address _delegatee, + uint256 _oldEarningPower, + uint256 _delegateeScore, + uint256 _timeLengthBetweenUpdateEligibilityDelayAndStaleOracleWindow + ) public { + _delegateeScore = bound(_delegateeScore, 0, calculator.delegateeScoreEligibilityThreshold() - 1); + _timeLengthBetweenUpdateEligibilityDelayAndStaleOracleWindow = bound( + _timeLengthBetweenUpdateEligibilityDelayAndStaleOracleWindow, + calculator.updateEligibilityDelay(), + calculator.STALE_ORACLE_WINDOW() + ); + vm.prank(scoreOracle); + calculator.updateDelegateeScore(_delegatee, _delegateeScore); + + vm.warp(block.timestamp + _timeLengthBetweenUpdateEligibilityDelayAndStaleOracleWindow); + (uint256 _earningPower, bool _isQualifiedForUpdate) = + calculator.getNewEarningPower(_amountStaked, _staker, _delegatee, _oldEarningPower); + assertEq(_earningPower, 0); + assertEq(_isQualifiedForUpdate, true); + } + + // Test Case: + // 1. Score >= Threshold: Delegatee is Eligible for full earning power. + // 2. Score < Threshold: Updated delegatee score falls below threshold. + // 3. Score < Threshold: Right before the updateEligibilityDelay is reached, updated delegatee + // score is still below threshold + // 4. _updateEligibilityDelay is reached. + // Result: We expect 0 earning power and true for _isQualifiedForUpdate. + function testFuzz_ReturnsZeroEarningPowerAndEligibleWhenDelegateScoreFallsBelowEligibilityThresholdButOutsideTheUpdateEligibilityDelayWithRecentBelowThresholdScoreUpdate( + uint256 _amountStaked, + address _staker, + address _delegatee, + uint256 _oldEarningPower, + uint256 _delegateeScore, + uint256 _newDelegateeScore, + uint256 _updatedDelegateeScore, + uint256 _timeBetweenUpdateEligibilityDelayAndStaleOracleWindow, + uint256 _timeBeforeEligibilityDelay + ) public { + _delegateeScore = + bound(_delegateeScore, calculator.delegateeScoreEligibilityThreshold(), type(uint256).max); + _newDelegateeScore = + bound(_newDelegateeScore, 0, calculator.delegateeScoreEligibilityThreshold() - 1); + _updatedDelegateeScore = + bound(_updatedDelegateeScore, 0, calculator.delegateeScoreEligibilityThreshold() - 1); + + _timeBeforeEligibilityDelay = + bound(_timeBeforeEligibilityDelay, 0, calculator.updateEligibilityDelay() - 1); + + _timeBetweenUpdateEligibilityDelayAndStaleOracleWindow = bound( + _timeBetweenUpdateEligibilityDelayAndStaleOracleWindow, + calculator.updateEligibilityDelay(), + calculator.STALE_ORACLE_WINDOW() + ); + + vm.startPrank(scoreOracle); + // First becomes an eligible delegatee + calculator.updateDelegateeScore(_delegatee, _delegateeScore); + // But then falls below the threshold. + calculator.updateDelegateeScore(_delegatee, _newDelegateeScore); + vm.stopPrank(); + + // Updating the delegate score before `updateEligibilityDelay` with a score below the + // `delegateeScoreEligibilityThreshold` shouldn't affect the `_isQualifiedForUpdate` value. + vm.warp(block.timestamp + _timeBeforeEligibilityDelay); + vm.prank(scoreOracle); + calculator.updateDelegateeScore(_delegatee, _updatedDelegateeScore); + + // Warp to block.timestamp + _timeBetweenUpdateEligibilityDelayAndStaleOracleWindow; + vm.warp( + block.timestamp - _timeBeforeEligibilityDelay + + _timeBetweenUpdateEligibilityDelayAndStaleOracleWindow + ); + (uint256 _earningPower, bool _isQualifiedForUpdate) = + calculator.getNewEarningPower(_amountStaked, _staker, _delegatee, _oldEarningPower); + assertEq(_earningPower, 0); + assertEq(_isQualifiedForUpdate, true); + } + + function testFuzz_ReturnsStakedAmountAsEarningPowerAndEligibleWhenDelegateScoreIsAboveEligibilityThresholdNotMatterTheTimeSinceLastDelegateeEligibilityChangeTime( + uint256 _amountStaked, + address _staker, + address _delegatee, + uint256 _oldEarningPower, + uint256 _delegateeScore, + uint256 _timeSinceLastDelegateeEligibilityChangeTime + ) public { + _delegateeScore = + bound(_delegateeScore, calculator.delegateeScoreEligibilityThreshold(), type(uint256).max); + _timeSinceLastDelegateeEligibilityChangeTime = + bound(_timeSinceLastDelegateeEligibilityChangeTime, 0, type(uint256).max - block.timestamp); + vm.prank(scoreOracle); + calculator.updateDelegateeScore(_delegatee, _delegateeScore); + + vm.warp(block.timestamp + _timeSinceLastDelegateeEligibilityChangeTime); + (uint256 _earningPower, bool _isQualifiedForUpdate) = + calculator.getNewEarningPower(_amountStaked, _staker, _delegatee, _oldEarningPower); + assertEq(_earningPower, _amountStaked); + assertEq(_isQualifiedForUpdate, true); + } +} + +contract LastDelegateeEligibilityChangeTime is EarningPowerCalculatorTest { + // Score below the threshold => above threshold; lastDelegateeEligibilityChangeTime is updated; + function testFuzz_SetsLastDelegateeEligibilityChangeTimeWhenADelegateBecomesEligible( + address _delegatee, + uint256 _delegateeScoreAboveThreshold, + uint256 _randomTimestamp + ) public { + _delegateeScoreAboveThreshold = bound( + _delegateeScoreAboveThreshold, + calculator.delegateeScoreEligibilityThreshold(), + type(uint256).max + ); + vm.warp(_randomTimestamp); + vm.prank(scoreOracle); + calculator.updateDelegateeScore(_delegatee, _delegateeScoreAboveThreshold); + assertEq(calculator.lastDelegateeEligibilityChangeTime(_delegatee), _randomTimestamp); + } + + // Score above the threshold => below threshold; lastDelegateeEligibilityChangeTime is updated; + function testFuzz_SetsLastDelegateeEligibilityChangeTimeWhenADelegateBecomesIneligible( + address _delegatee, + uint256 _delegateeScoreAboveThreshold, + uint256 _delegateeScoreBelowThreshold, + uint256 _randomTimestamp + ) public { + _delegateeScoreAboveThreshold = bound( + _delegateeScoreAboveThreshold, + calculator.delegateeScoreEligibilityThreshold(), + type(uint256).max + ); + _delegateeScoreBelowThreshold = + bound(_delegateeScoreBelowThreshold, 0, calculator.delegateeScoreEligibilityThreshold() - 1); + + vm.prank(scoreOracle); + calculator.updateDelegateeScore(_delegatee, _delegateeScoreAboveThreshold); + + vm.warp(_randomTimestamp); + vm.prank(scoreOracle); + calculator.updateDelegateeScore(_delegatee, _delegateeScoreBelowThreshold); + assertEq(calculator.lastDelegateeEligibilityChangeTime(_delegatee), _randomTimestamp); + } + + // Score >= threshold to score < threshold; lastDelegateeEligibilityChangeTime is not + // updated. + function testFuzz_KeepsLastDelegateeEligibilityChangeTimeWhenAnIneligibleDelegateScoreIsUpdatedScoreBelowThreshold( + address _delegatee, + uint256 _delegateeScoreAboveThreshold, + uint256 _delegateeScoreBelowThreshold, + uint256 _updatedDelegateeScoreBelowThreshold, + uint256 _expectedTimestamp, + uint256 _randomTimestamp + ) public { + _delegateeScoreAboveThreshold = bound( + _delegateeScoreAboveThreshold, + calculator.delegateeScoreEligibilityThreshold(), + type(uint256).max + ); + _delegateeScoreBelowThreshold = + bound(_delegateeScoreBelowThreshold, 0, calculator.delegateeScoreEligibilityThreshold() - 1); + _updatedDelegateeScoreBelowThreshold = bound( + _updatedDelegateeScoreBelowThreshold, 0, calculator.delegateeScoreEligibilityThreshold() - 1 + ); + vm.assume(_expectedTimestamp < _randomTimestamp); + + vm.startPrank(scoreOracle); + calculator.updateDelegateeScore(_delegatee, _delegateeScoreAboveThreshold); + + // First time _delegatee becomes ineligible + vm.warp(_expectedTimestamp); + calculator.updateDelegateeScore(_delegatee, _delegateeScoreBelowThreshold); + console2.log(_expectedTimestamp); + + // Since the delegatee is already ineligible, a score update that's below the threshold + // shouldn't change the lastDelegateeEligibilityChangeTime to _randomTimestamp. + vm.warp(_randomTimestamp); + calculator.updateDelegateeScore(_delegatee, _updatedDelegateeScoreBelowThreshold); + vm.stopPrank(); + + assertEq(calculator.lastDelegateeEligibilityChangeTime(_delegatee), _expectedTimestamp); + } +} + +contract UpdateDelegateScore is EarningPowerCalculatorTest { + function testFuzz_UpdatesDelegateScore(address _delegatee, uint256 _newScore) public { + vm.prank(scoreOracle); + calculator.updateDelegateeScore(_delegatee, _newScore); + assertEq(calculator.delegateeScores(_delegatee), _newScore); + assertEq(calculator.lastOracleUpdateTime(), block.timestamp); + } + + function testFuzz_UpdatesExistingDelegateScore( + address _delegatee, + uint256 _firstScore, + uint256 _secondScore, + uint256 _timeInBetween + ) public { + vm.startPrank(scoreOracle); + calculator.updateDelegateeScore(_delegatee, _firstScore); + assertEq(calculator.delegateeScores(_delegatee), _firstScore); + assertEq(calculator.lastOracleUpdateTime(), block.timestamp); + + vm.warp(_timeInBetween); + + calculator.updateDelegateeScore(_delegatee, _secondScore); + assertEq(calculator.delegateeScores(_delegatee), _secondScore); + assertEq(calculator.lastOracleUpdateTime(), _timeInBetween); + vm.stopPrank(); + } + + function testFuzz_EmitsAnEventWhenDelegatesScoreIsUpdated(address _delegatee, uint256 _newScore) + public + { + vm.prank(scoreOracle); + vm.expectEmit(); + emit DelegateeScoreUpdated(_delegatee, 0, _newScore); + calculator.updateDelegateeScore(_delegatee, _newScore); + } + + function testFuzz_RevertIf_CallerIsNotTheScoreOracle( + address _caller, + address _delegatee, + uint256 _newScore + ) public { + vm.assume(_caller != scoreOracle); + vm.prank(_caller); + vm.expectRevert( + abi.encodeWithSelector( + EarningPowerCalculator.Unauthorized.selector, bytes32("not oracle"), _caller + ) + ); + calculator.updateDelegateeScore(_delegatee, _newScore); + } + + function testFuzz_RevertIf_DelegateeScoreLocked( + address _delegatee, + uint256 _overrideScore, + uint256 _newScore + ) public { + vm.prank(owner); + calculator.overrideDelegateeScore(_delegatee, _overrideScore); + vm.prank(scoreOracle); + vm.expectRevert( + abi.encodeWithSelector(EarningPowerCalculator.DelegateeScoreLocked.selector, _delegatee) + ); + calculator.updateDelegateeScore(_delegatee, _newScore); + } +} + +contract OverrideDelegateScore is EarningPowerCalculatorTest { + function testFuzz_OverrideDelegateScore(address _delegatee, uint256 _newScore, uint256 _timestamp) + public + { + vm.warp(_timestamp); + vm.prank(owner); + calculator.overrideDelegateeScore(_delegatee, _newScore); + assertEq(calculator.delegateeScores(_delegatee), _newScore); + assertEq(calculator.delegateeScoreLock(_delegatee), true); + } + + function testFuzz_EmitsEventWhenDelegateScoreIsOverridden( + address _delegatee, + uint256 _newScore, + uint256 _timestamp + ) public { + vm.warp(_timestamp); + vm.prank(owner); + vm.expectEmit(); + emit DelegateeScoreUpdated(_delegatee, 0, _newScore); + vm.expectEmit(); + emit DelegateeScoreLockSet(_delegatee, false, true); + calculator.overrideDelegateeScore(_delegatee, _newScore); + } + + function testFuzz_RevertIf_CallerIsNotOwner( + address _caller, + address _delegatee, + uint256 _newScore + ) public { + vm.assume(_caller != owner); + vm.prank(_caller); + vm.expectRevert(abi.encodeWithSelector(OwnableUnauthorizedAccount.selector, _caller)); + calculator.overrideDelegateeScore(_delegatee, _newScore); + assertEq(calculator.delegateeScoreLock(_delegatee), false); + } +} + +contract SetDelegateeScoreLock is EarningPowerCalculatorTest { + function testFuzz_LocksOrUnlocksADelegateeScore(address _delegatee, bool _isLocked) public { + vm.prank(owner); + calculator.setDelegateeScoreLock(_delegatee, _isLocked); + assertEq(calculator.delegateeScoreLock(_delegatee), _isLocked); + } + + function testFuzz_EmitsAnEventWhenDelegateScoreIsLockedOrUnlocked( + address _delegatee, + bool _isLocked + ) public { + vm.expectEmit(); + emit DelegateeScoreLockSet(_delegatee, calculator.delegateeScoreLock(_delegatee), _isLocked); + vm.prank(owner); + calculator.setDelegateeScoreLock(_delegatee, _isLocked); + } + + function testFuzz_RevertIf_CallerIsNotOwner(address _caller, address _delegatee, bool _isLocked) + public + { + vm.assume(_caller != owner); + vm.prank(_caller); + vm.expectRevert(abi.encodeWithSelector(OwnableUnauthorizedAccount.selector, _caller)); + calculator.setDelegateeScoreLock(_delegatee, _isLocked); + } +} + +contract SetScoreOracle is EarningPowerCalculatorTest { + function testFuzz_SetsTheScoreOracleAddress(address _newScoreOracle) public { + vm.prank(owner); + calculator.setScoreOracle(_newScoreOracle); + assertEq(calculator.scoreOracle(), _newScoreOracle); + } + + function testFuzz_EmitsAnEventWhenScoreOracleIsUpdated(address _newScoreOracle) public { + vm.prank(owner); + vm.expectEmit(); + emit ScoreOracleSet(scoreOracle, _newScoreOracle); + calculator.setScoreOracle(_newScoreOracle); + } + + function testFuzz_RevertIf_CallerIsNotOwner(address _caller, address _newScoreOracle) public { + vm.assume(_caller != owner); + vm.prank(_caller); + vm.expectRevert(abi.encodeWithSelector(OwnableUnauthorizedAccount.selector, _caller)); + calculator.setScoreOracle(_newScoreOracle); + } +} + +contract SetUpdateEligibilityDelay is EarningPowerCalculatorTest { + function testFuzz_SetsTheUpdateEligibilityDelay(uint256 _newUpdateEligibilityDelay) public { + vm.prank(owner); + calculator.setUpdateEligibilityDelay(_newUpdateEligibilityDelay); + assertEq(calculator.updateEligibilityDelay(), _newUpdateEligibilityDelay); + } + + function testFuzz_EmitsAnEventWhenUpdateEligibilityDelayIsUpdated( + uint256 _newUpdateEligibilityDelay + ) public { + vm.prank(owner); + vm.expectEmit(); + emit UpdateEligibilityDelaySet(updateEligibilityDelay, _newUpdateEligibilityDelay); + calculator.setUpdateEligibilityDelay(_newUpdateEligibilityDelay); + } + + function testFuzz_RevertIf_CallerIsNotOwner(address _caller, uint256 _newUpdateEligibilityDelay) + public + { + vm.assume(_caller != owner); + vm.prank(_caller); + vm.expectRevert(abi.encodeWithSelector(OwnableUnauthorizedAccount.selector, _caller)); + calculator.setUpdateEligibilityDelay(_newUpdateEligibilityDelay); + } +} + +contract SetDelegateScoreEligibilityThreshold is EarningPowerCalculatorTest { + function testFuzz_SetsTheDelegateScoreEligibilityThreshold( + uint256 _newDelegateScoreEligibilityThreshold + ) public { + vm.prank(owner); + calculator.setDelegateeScoreEligibilityThreshold(_newDelegateScoreEligibilityThreshold); + assertEq(calculator.delegateeScoreEligibilityThreshold(), _newDelegateScoreEligibilityThreshold); + } + + function testFuzz_EmitsAnEventWhenDelegateScoreEligibilityThresholdIsUpdated( + uint256 _newDelegateScoreEligibilityThreshold + ) public { + vm.prank(owner); + vm.expectEmit(); + emit DelegateeScoreEligibilityThresholdSet( + delegateeScoreEligibilityThreshold, _newDelegateScoreEligibilityThreshold + ); + calculator.setDelegateeScoreEligibilityThreshold(_newDelegateScoreEligibilityThreshold); + } + + function testFuzz_RevertIf_CallerIsNotOwner( + address _caller, + uint256 _newDelegateScoreEligibilityThreshold + ) public { + vm.assume(_caller != owner); + vm.prank(_caller); + vm.expectRevert(abi.encodeWithSelector(OwnableUnauthorizedAccount.selector, _caller)); + calculator.setDelegateeScoreEligibilityThreshold(_newDelegateScoreEligibilityThreshold); + } +} From f05b4759eee57c81ac591d3f997d8b22f9888030 Mon Sep 17 00:00:00 2001 From: garyghayrat Date: Wed, 2 Oct 2024 11:19:47 -0400 Subject: [PATCH 2/5] Update naming and comments Make `getEarningPower` logic more readable Make `getNewEarningPower` more readable Use instead of modifiers and remove duplicate events and errors Add test case for when owner is set to address 0 Make `STALE_ORACLE_WINDOW` immutable Update error naming Use internal functions Simplify `getNewEarningPower` logic Update comments and test names Rename tests Update natspec and rename vars Change `lastDelegateeEligibilityChangeTime` to `timeOfIneligibility` Move tests and correct names Rename var and remove unused var Refactor a test --- ...ligibilityOracleEarningPowerCalculator.sol | 220 +++++++----- ...gibilityOracleEarningPowerCalculator.t.sol | 340 ++++++++++-------- 2 files changed, 309 insertions(+), 251 deletions(-) diff --git a/src/BinaryEligibilityOracleEarningPowerCalculator.sol b/src/BinaryEligibilityOracleEarningPowerCalculator.sol index 64fe895..c90fd30 100644 --- a/src/BinaryEligibilityOracleEarningPowerCalculator.sol +++ b/src/BinaryEligibilityOracleEarningPowerCalculator.sol @@ -18,11 +18,11 @@ contract BinaryEligibilityOracleEarningPowerCalculator is Ownable, IEarningPower /// @param delegatee The address of the delegatee whose score lock status was updated. /// @param oldState The previous lock state of the delegatee's score. /// @param newState The new lock state of the delegatee's score. - event DelegateeScoreLockSet(address indexed delegatee, bool oldState, bool newState); + event DelegateeScoreLockStatusSet(address indexed delegatee, bool oldState, bool newState); - /// @notice Emitted when the ScoreOracle address is updated. - /// @param oldScoreOracle The address of the previous ScoreOracle. - /// @param newScoreOracle The address of the new ScoreOracle. + /// @notice Emitted when the `scoreOracle` address is updated. + /// @param oldScoreOracle The address of the previous `scoreOracle`. + /// @param newScoreOracle The address of the new `scoreOracle`. event ScoreOracleSet(address indexed oldScoreOracle, address indexed newScoreOracle); /// @notice Emitted when the update eligibility delay is changed. @@ -30,22 +30,21 @@ contract BinaryEligibilityOracleEarningPowerCalculator is Ownable, IEarningPower /// @param newDelay The new update eligibility delay value. event UpdateEligibilityDelaySet(uint256 oldDelay, uint256 newDelay); - /// @notice Emitted when the delegatee score eligibility threshold to earn the full earning power - /// for its stakers is updated. + /// @notice Emitted when the eligibility threshold score to earn full earning power is updated. /// @param oldThreshold The previous threshold value. - /// @param newThreshold The new threshold value set. - event DelegateeScoreEligibilityThresholdSet(uint256 oldThreshold, uint256 newThreshold); + /// @param newThreshold The new threshold value. + event DelegateeEligibilityThresholdScoreSet(uint256 oldThreshold, uint256 newThreshold); - /// @notice Error thrown when a non-scoreOracle address tries to call the updateDelegateeScore + /// @notice Error thrown when a non-score oracle address tries to call the `updateDelegateeScore` /// function. - error Unauthorized(bytes32 reason, address caller); + error BinaryEligibilityOracleEarningPowerCalculator__Unauthorized(bytes32 reason, address caller); /// @notice Error thrown when an attempt is made to update the score of a locked delegatee score. - error DelegateeScoreLocked(address delegatee); + error BinaryEligibilityOracleEarningPowerCalculator__DelegateeScoreLocked(address delegatee); - /// @notice After the 7-day stale oracle window, all stakers' earning power will be set to 100% of - /// their staked amounts. - uint256 public constant STALE_ORACLE_WINDOW = 7 days; // TODO: Update to appropriate time frame. + /// @notice The length of oracle downtime before, all stakers' earning power will be set to 100% + /// of their staked amounts. + uint256 public immutable STALE_ORACLE_WINDOW; /// @notice The address with the authority to update delegatee scores. address public scoreOracle; @@ -54,10 +53,10 @@ contract BinaryEligibilityOracleEarningPowerCalculator is Ownable, IEarningPower uint256 public lastOracleUpdateTime; /// @notice The minimum score a delegatee must have to be eligible for earning power. - /// @dev This threshold is used in the getEarningPower and getNewEarningPower functions. + /// @dev This threshold is used in the `getEarningPower` and `getNewEarningPower` functions. /// @dev Delegates with scores below this threshold will have an earning power of 0, and scores /// equal or above this threshold will have 100% earning power equal to the staked amount. - uint256 public delegateeScoreEligibilityThreshold; + uint256 public delegateeEligibilityThresholdScore; /// @notice The minimum delay required between staker earning power updates. /// @dev This delay helps stakers react to changes in their delegatee's score without immediate @@ -71,97 +70,97 @@ contract BinaryEligibilityOracleEarningPowerCalculator is Ownable, IEarningPower /// changed. /// @dev Key is the delegatee's address, value is the block.timestamp of the last eligibility /// update. - mapping(address delegatee => uint256 timestamp) public lastDelegateeEligibilityChangeTime; + mapping(address delegatee => uint256 timestamp) public timeOfIneligibility; /// @notice Mapping to store the lock status of delegate scores. - mapping(address delegate => bool isLocked) public delegateeScoreLock; + mapping(address delegate => bool isLocked) public delegateeScoreLockStatus; /// @notice Initializes the EarningPowerCalculator contract. /// @param _owner The DAO governor address. - /// @param _scoreOracle The address of the trusted Karma address. + /// @param _scoreOracle The address of the trusted oracle address. /// @param _delegateeScoreEligibilityThreshold The threshold for delegatee score eligibility to - /// have - /// the full earning power. + /// have the full earning power. /// @param _updateEligibilityDelay The delay required between delegatee earning power /// updates after falling below the eligibility threshold. constructor( address _owner, address _scoreOracle, + uint256 _staleOracleWindow, uint256 _delegateeScoreEligibilityThreshold, uint256 _updateEligibilityDelay ) Ownable(_owner) { - scoreOracle = _scoreOracle; - delegateeScoreEligibilityThreshold = _delegateeScoreEligibilityThreshold; - updateEligibilityDelay = _updateEligibilityDelay; - - emit ScoreOracleSet(address(0), _scoreOracle); - emit DelegateeScoreEligibilityThresholdSet(0, _delegateeScoreEligibilityThreshold); - emit UpdateEligibilityDelaySet(0, _updateEligibilityDelay); + _setScoreOracle(_scoreOracle); + STALE_ORACLE_WINDOW = _staleOracleWindow; + _setDelegateeScoreEligibilityThreshold(_delegateeScoreEligibilityThreshold); + _setUpdateEligibilityDelay(_updateEligibilityDelay); } /// @notice Calculates the earning power for a given delegatee and staking amount. /// @param _amountStaked The amount of tokens staked. - /// @param _staker The address of the staker. + /// @param /* _staker */ The address of the staker. /// @param _delegatee The address of the delegatee. - /// @return _earningPower The calculated earning power. - function getEarningPower(uint256 _amountStaked, address _staker, address _delegatee) + /// @return The calculated earning power. + function getEarningPower(uint256 _amountStaked, address, /* _staker */ address _delegatee) external view - returns (uint256 _earningPower) + returns (uint256) { - // If the oracle has not been updated for more than the stale oracle window, return full earning - // power and is qualified for update. - if (block.timestamp - lastOracleUpdateTime > STALE_ORACLE_WINDOW) _earningPower = _amountStaked; + // If the oracle has not updated eligibility scores for more than the stale oracle window, + // return full earning power. + if (block.timestamp - lastOracleUpdateTime > STALE_ORACLE_WINDOW) return _amountStaked; // If the delegatee's score is below the eligibility threshold, return 0 earning power. - else if (delegateeScores[_delegatee] < delegateeScoreEligibilityThreshold) _earningPower = 0; - // If the delegatee's score is above the eligibility threshold, return full earning power. - else _earningPower = _amountStaked; + if (delegateeScores[_delegatee] < delegateeEligibilityThresholdScore) return 0; + // If the delegatee's score is equal to or above the eligibility threshold, return full earning + // power. + return _amountStaked; } /// @notice Calculates the new earning power and determines if it qualifies for an update.` /// @param _amountStaked The amount of tokens staked. - /// @param _staker The address of the staker. + /// @param /* _staker */ The address of the staker. /// @param _delegatee The address of the delegatee. - /// @param _oldEarningPower The previous earning power value. - /// @return _newEarningPower The newly calculated earning power. - /// @return _isQualifiedForUpdate Boolean indicating if the new earning power qualifies for an + /// @param /* _oldEarningPower */ The previous earning power value. + /// @return The newly calculated earning power. + /// @return Boolean indicating if the new earning power qualifies for an /// update. function getNewEarningPower( uint256 _amountStaked, - address _staker, + address, /* _staker */ address _delegatee, - uint256 _oldEarningPower - ) external view returns (uint256 _newEarningPower, bool _isQualifiedForUpdate) { + uint256 /* _oldEarningPower */ + ) external view returns (uint256, bool) { // If the oracle has not been updated for more than the stale oracle window, return full earning // power and is qualified for update. - if (block.timestamp - lastOracleUpdateTime > STALE_ORACLE_WINDOW) { - _newEarningPower = _amountStaked; - _isQualifiedForUpdate = true; - // If the delegatee's score is below the eligibility threshold, return 0 earning power. - } else if (delegateeScores[_delegatee] < delegateeScoreEligibilityThreshold) { - // check if the update eligibility has passed, if it hasn't, it's not qualified for an update. - if ( - (lastDelegateeEligibilityChangeTime[_delegatee] + updateEligibilityDelay) > block.timestamp - ) _isQualifiedForUpdate = false; - // If the update eligibility has passed then it's qualified for an update. - else _isQualifiedForUpdate = true; - _newEarningPower = 0; - // If the delegatee's score is above the eligibility threshold, return full earning power and - // is qualified for an update. - } else { - _newEarningPower = _amountStaked; - _isQualifiedForUpdate = true; - } + if (block.timestamp - lastOracleUpdateTime > STALE_ORACLE_WINDOW) return (_amountStaked, true); + + // If the delegatee's score is below the eligibility threshold and the updateEligibilityDelay + // period has not elapsed, return 0 earning power and false for qualified to update. + if ( + delegateeScores[_delegatee] < delegateeEligibilityThresholdScore + && (timeOfIneligibility[_delegatee] + updateEligibilityDelay) > block.timestamp + ) return (0, false); + + // If the delegatee's score is below the eligibility threshold and the updateEligibilityDelay + // period has elapsed, return 0 earning power and true for qualified to update. + if (delegateeScores[_delegatee] < delegateeEligibilityThresholdScore) return (0, true); + + // If the delegatee's score is equal to or above the eligibility threshold, return full earning + // power and true for qualified to update. + return (_amountStaked, true); } - /// @notice Updates the score of a delegatee. - /// @dev This function can only be called by the authorized scoreOracle address. + /// @notice Updates the eligibility score of a delegatee. + /// @dev This function can only be called by the authorized `scoreOracle` address. /// @dev If the delegatee's score is locked, the update will be reverted. /// @param _delegatee The address of the delegatee whose score is being updated. /// @param _newScore The new score to be assigned to the delegatee. function updateDelegateeScore(address _delegatee, uint256 _newScore) public { - if (msg.sender != scoreOracle) revert Unauthorized("not oracle", msg.sender); - if (delegateeScoreLock[_delegatee]) revert DelegateeScoreLocked(_delegatee); + if (msg.sender != scoreOracle) { + revert BinaryEligibilityOracleEarningPowerCalculator__Unauthorized("not oracle", msg.sender); + } + if (delegateeScoreLockStatus[_delegatee]) { + revert BinaryEligibilityOracleEarningPowerCalculator__DelegateeScoreLocked(_delegatee); + } _updateDelegateeScore(_delegatee, _newScore); lastOracleUpdateTime = block.timestamp; } @@ -169,49 +168,50 @@ contract BinaryEligibilityOracleEarningPowerCalculator is Ownable, IEarningPower /// @notice Overrides the score of a delegatee and locks it. /// @dev This function can only be called by the contract owner. /// @dev It updates the delegatee's score and then locks it to prevent further updates by the - /// scoreOracle. + /// `scoreOracle`. /// @param _delegatee The address of the delegatee whose score is being overridden. /// @param _newScore The new score to be assigned to the delegatee. - function overrideDelegateeScore(address _delegatee, uint256 _newScore) public onlyOwner { + function overrideDelegateeScore(address _delegatee, uint256 _newScore) public { + _checkOwner(); _updateDelegateeScore(_delegatee, _newScore); - setDelegateeScoreLock(_delegatee, true); + _setDelegateeScoreLock(_delegatee, true); } /// @notice Sets or removes the lock on a delegatee's score. /// @dev This function can only be called by the contract owner. - /// @dev When a delegatee's score is locked, it cannot be updated by the scoreOracle. + /// @dev When a delegatee's score is locked, it cannot be updated by the `scoreOracle`. /// @dev This function is useful for manually overriding and protecting a delegatee's score. /// @param _delegatee The address of the delegatee whose score lock status is being modified. /// @param _isLocked The new lock status to set. True to lock the score, false to unlock. - function setDelegateeScoreLock(address _delegatee, bool _isLocked) public onlyOwner { - emit DelegateeScoreLockSet(_delegatee, delegateeScoreLock[_delegatee], _isLocked); - delegateeScoreLock[_delegatee] = _isLocked; + function setDelegateeScoreLock(address _delegatee, bool _isLocked) public { + _checkOwner(); + _setDelegateeScoreLock(_delegatee, _isLocked); } /// @notice Sets a new address as the ScoreOracle contract. /// @dev This function can only be called by the contract owner. /// @param _newScoreOracle The address of the new ScoreOracle contract. - function setScoreOracle(address _newScoreOracle) public onlyOwner { - emit ScoreOracleSet(scoreOracle, _newScoreOracle); - scoreOracle = _newScoreOracle; + function setScoreOracle(address _newScoreOracle) public { + _checkOwner(); + _setScoreOracle(_newScoreOracle); } /// @notice Sets a new update eligibility delay. /// @dev This function can only be called by the contract owner. /// @param _newUpdateEligibilityDelay The new delay value to set. - function setUpdateEligibilityDelay(uint256 _newUpdateEligibilityDelay) public onlyOwner { - emit UpdateEligibilityDelaySet(updateEligibilityDelay, _newUpdateEligibilityDelay); - updateEligibilityDelay = _newUpdateEligibilityDelay; + function setUpdateEligibilityDelay(uint256 _newUpdateEligibilityDelay) public { + _checkOwner(); + _setUpdateEligibilityDelay(_newUpdateEligibilityDelay); } + /// @notice Sets a new delegatee score eligibility threshold. + /// @dev This function can only be called by the contract owner. + /// @param _newDelegateeScoreEligibilityThreshold The new threshold value to set. function setDelegateeScoreEligibilityThreshold(uint256 _newDelegateeScoreEligibilityThreshold) public - onlyOwner { - emit DelegateeScoreEligibilityThresholdSet( - delegateeScoreEligibilityThreshold, _newDelegateeScoreEligibilityThreshold - ); - delegateeScoreEligibilityThreshold = _newDelegateeScoreEligibilityThreshold; + _checkOwner(); + _setDelegateeScoreEligibilityThreshold(_newDelegateeScoreEligibilityThreshold); } /// @notice Internal function to update a delegatee's score. @@ -219,14 +219,50 @@ contract BinaryEligibilityOracleEarningPowerCalculator is Ownable, IEarningPower /// @param _delegatee The address of the delegatee whose score is being updated. /// @param _newScore The new score to be assigned to the delegatee. function _updateDelegateeScore(address _delegatee, uint256 _newScore) internal { - uint256 _currentDelegateeScore = delegateeScores[_delegatee]; - bool _currentlyEligible = _currentDelegateeScore >= delegateeScoreEligibilityThreshold; - bool _newlyEligible = _newScore >= delegateeScoreEligibilityThreshold; - emit DelegateeScoreUpdated(_delegatee, _currentDelegateeScore, _newScore); + 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 (_currentlyEligible != _newlyEligible) { - lastDelegateeEligibilityChangeTime[_delegatee] = block.timestamp; - } + if (_previouslyEligible && !_newlyEligible) timeOfIneligibility[_delegatee] = block.timestamp; delegateeScores[_delegatee] = _newScore; } + + /// @notice Internal function to set or remove the lock on a delegatee's score. + /// @dev This function updates the lock status of a delegatee's score and emits an event. + /// @dev When a delegatee's score is locked, it cannot be updated by the `scoreOracle`. + /// @param _delegatee The address of the delegatee whose score lock status is being modified. + /// @param _isLocked The new lock status to set. True to lock the score, false to unlock. + function _setDelegateeScoreLock(address _delegatee, bool _isLocked) internal { + emit DelegateeScoreLockStatusSet(_delegatee, delegateeScoreLockStatus[_delegatee], _isLocked); + delegateeScoreLockStatus[_delegatee] = _isLocked; + } + + /// @notice Internal function to set a new score oracle address. + /// @dev This function updates the scoreOracle address and emits an event. + /// @param _newScoreOracle The address of the new score oracle. + function _setScoreOracle(address _newScoreOracle) internal { + emit ScoreOracleSet(scoreOracle, _newScoreOracle); + scoreOracle = _newScoreOracle; + } + + /// @notice Internal function to set a new update eligibility delay. + /// @dev This function updates the updateEligibilityDelay and emits an event. + /// @param _newUpdateEligibilityDelay The new delay value to set. + function _setUpdateEligibilityDelay(uint256 _newUpdateEligibilityDelay) internal { + emit UpdateEligibilityDelaySet(updateEligibilityDelay, _newUpdateEligibilityDelay); + updateEligibilityDelay = _newUpdateEligibilityDelay; + } + + /// @notice Internal function to set a new delegatee score eligibility threshold. + /// @dev This function updates the delegateeEligibilityThresholdScore and emits an event. + /// @param _newDelegateeScoreEligibilityThreshold The new threshold value to set. + function _setDelegateeScoreEligibilityThreshold(uint256 _newDelegateeScoreEligibilityThreshold) + internal + { + emit DelegateeEligibilityThresholdScoreSet( + delegateeEligibilityThresholdScore, _newDelegateeScoreEligibilityThreshold + ); + delegateeEligibilityThresholdScore = _newDelegateeScoreEligibilityThreshold; + } } diff --git a/test/BinaryEligibilityOracleEarningPowerCalculator.t.sol b/test/BinaryEligibilityOracleEarningPowerCalculator.t.sol index 9fc9957..2f51346 100644 --- a/test/BinaryEligibilityOracleEarningPowerCalculator.t.sol +++ b/test/BinaryEligibilityOracleEarningPowerCalculator.t.sol @@ -2,21 +2,15 @@ pragma solidity ^0.8.23; import {Test, console2} from "forge-std/Test.sol"; -import {BinaryEligibilityOracleEarningPowerCalculator as EarningPowerCalculator} from - "src/BinaryEligibilityOracleEarningPowerCalculator.sol"; +import { + BinaryEligibilityOracleEarningPowerCalculator as EarningPowerCalculator, + Ownable +} from "src/BinaryEligibilityOracleEarningPowerCalculator.sol"; contract EarningPowerCalculatorTest is Test { - event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); - event DelegateeScoreUpdated(address indexed delegatee, uint256 oldScore, uint256 newScore); - event DelegateeScoreLockSet(address indexed delegatee, bool oldState, bool newState); - event ScoreOracleSet(address indexed oldScoreOracle, address indexed newScoreOracle); - event UpdateEligibilityDelaySet(uint256 oldDelay, uint256 newDelay); - event DelegateeScoreEligibilityThresholdSet(uint256 oldThreshold, uint256 newThreshold); - - error OwnableUnauthorizedAccount(address account); - address public owner; address public scoreOracle; + uint256 public staleOracleWindow; uint256 public delegateeScoreEligibilityThreshold; uint256 public updateEligibilityDelay; EarningPowerCalculator public calculator; @@ -24,11 +18,16 @@ contract EarningPowerCalculatorTest is Test { function setUp() public { owner = makeAddr("owner"); scoreOracle = makeAddr("scoreOracle"); + staleOracleWindow = 7 days; delegateeScoreEligibilityThreshold = 50; updateEligibilityDelay = 7 days; calculator = new EarningPowerCalculator( - owner, scoreOracle, delegateeScoreEligibilityThreshold, updateEligibilityDelay + owner, + scoreOracle, + staleOracleWindow, + delegateeScoreEligibilityThreshold, + updateEligibilityDelay ); } } @@ -37,7 +36,7 @@ contract Constructor is EarningPowerCalculatorTest { function test_SetsOwnerAndContractParametersCorrectly() public view { assertEq(calculator.owner(), owner); assertEq(calculator.scoreOracle(), scoreOracle); - assertEq(calculator.delegateeScoreEligibilityThreshold(), delegateeScoreEligibilityThreshold); + assertEq(calculator.delegateeEligibilityThresholdScore(), delegateeScoreEligibilityThreshold); assertEq(calculator.updateEligibilityDelay(), updateEligibilityDelay); } @@ -49,11 +48,15 @@ contract Constructor is EarningPowerCalculatorTest { ) public { vm.assume(_owner != address(0)); EarningPowerCalculator _calculator = new EarningPowerCalculator( - _owner, _scoreOracle, _delegateeScoreEligibilityThreshold, _updateEligibilityDelay + _owner, + _scoreOracle, + staleOracleWindow, + _delegateeScoreEligibilityThreshold, + _updateEligibilityDelay ); assertEq(_calculator.owner(), _owner); assertEq(_calculator.scoreOracle(), _scoreOracle); - assertEq(_calculator.delegateeScoreEligibilityThreshold(), _delegateeScoreEligibilityThreshold); + assertEq(_calculator.delegateeEligibilityThresholdScore(), _delegateeScoreEligibilityThreshold); assertEq(_calculator.updateEligibilityDelay(), _updateEligibilityDelay); } @@ -66,22 +69,43 @@ contract Constructor is EarningPowerCalculatorTest { vm.assume(_owner != address(0)); vm.expectEmit(); - emit OwnershipTransferred(address(0), _owner); + emit Ownable.OwnershipTransferred(address(0), _owner); vm.expectEmit(); - emit ScoreOracleSet(address(0), _scoreOracle); + emit EarningPowerCalculator.ScoreOracleSet(address(0), _scoreOracle); vm.expectEmit(); - emit DelegateeScoreEligibilityThresholdSet(0, _delegateeScoreEligibilityThreshold); + emit EarningPowerCalculator.DelegateeEligibilityThresholdScoreSet( + 0, _delegateeScoreEligibilityThreshold + ); vm.expectEmit(); - emit UpdateEligibilityDelaySet(0, _updateEligibilityDelay); + emit EarningPowerCalculator.UpdateEligibilityDelaySet(0, _updateEligibilityDelay); + + new EarningPowerCalculator( + _owner, + _scoreOracle, + staleOracleWindow, + _delegateeScoreEligibilityThreshold, + _updateEligibilityDelay + ); + } - EarningPowerCalculator _calculator = new EarningPowerCalculator( - _owner, _scoreOracle, _delegateeScoreEligibilityThreshold, _updateEligibilityDelay + function testFuzz_RevertIf_OwnerIsZeroAddress( + address _scoreOracle, + uint256 _delegateeScoreEligibilityThreshold, + uint256 _updateEligibilityDelay + ) public { + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableInvalidOwner.selector, address(0))); + new EarningPowerCalculator( + address(0), + _scoreOracle, + staleOracleWindow, + _delegateeScoreEligibilityThreshold, + _updateEligibilityDelay ); } } contract GetEarningPower is EarningPowerCalculatorTest { - function testFuzz_ReturnsAmountStakedAsEarningPowerWhenStaleOracleWindowHasPassed( + function testFuzz_EarningPowerIsAmountStakedAfterTheStaleOracleWindow( uint256 _amountStaked, address _staker, address _delegatee, @@ -100,26 +124,26 @@ contract GetEarningPower is EarningPowerCalculatorTest { assertEq(calculator.getEarningPower(_amountStaked, _staker, _delegatee), _amountStaked); } - function testFuzz_ReturnsZeroEarningPowerWhenDelegateeScoreIsBelowEligibilityThreshold( + function testFuzz_EarningPowerIsZeroIfBelowEligibilityThreshold( uint256 _delegateeScore, uint256 _amountStaked, address _staker, address _delegatee ) public { - _delegateeScore = bound(_delegateeScore, 0, calculator.delegateeScoreEligibilityThreshold() - 1); + _delegateeScore = bound(_delegateeScore, 0, calculator.delegateeEligibilityThresholdScore() - 1); vm.prank(scoreOracle); calculator.updateDelegateeScore(_delegatee, _delegateeScore); assertEq(calculator.getEarningPower(_amountStaked, _staker, _delegatee), 0); } - function testFuzz_ReturnsAmountStakedAsEarningPowerWhenDelegateScoreIsAboveTheEligibilityThreshold( + function testFuzz_EarningPowerIsAmountStakedIfAboveEligibilityThreshold( uint256 _delegateeScore, uint256 _amountStaked, address _staker, address _delegatee ) public { _delegateeScore = - bound(_delegateeScore, calculator.delegateeScoreEligibilityThreshold() + 1, type(uint256).max); + bound(_delegateeScore, calculator.delegateeEligibilityThresholdScore() + 1, type(uint256).max); vm.prank(scoreOracle); calculator.updateDelegateeScore(_delegatee, _delegateeScore); assertEq(calculator.getEarningPower(_amountStaked, _staker, _delegatee), _amountStaked); @@ -127,7 +151,7 @@ contract GetEarningPower is EarningPowerCalculatorTest { } contract GetNewEarningPower is EarningPowerCalculatorTest { - function testFuzz_ReturnsAmountStakedAndEligibleWhenStaleOracleWindowHasPassed( + function testFuzz_EarningPowerIsAmountStakedAfterStaleOracleWindow( uint256 _amountStaked, address _staker, address _delegatee, @@ -150,12 +174,7 @@ contract GetNewEarningPower is EarningPowerCalculatorTest { assertEq(_isQualifiedForUpdate, true); } - // Test Case: - // 1. Score >= Threshold: Delegatee is Eligible for full earning power. - // 2. Score < Threshold: Updated delegatee score falls below threshold. - // 4. _updateEligibilityDelay is NOT reached. - // Result: We expect 0 earning power and false for _isQualifiedForUpdate. - function testFuzz_ReturnsZeroEarningPowerAndNotEligibleWhenDelegateScoreFallsBelowEligibilityThresholdButWithinTheUpdateEligibilityDelay( + function testFuzz_EarningPowerChangeIsNotQualifiedIfDuringUpdateEligibility( uint256 _amountStaked, address _staker, address _delegatee, @@ -166,11 +185,11 @@ contract GetNewEarningPower is EarningPowerCalculatorTest { ) public { _delegateeScoreAboveThreshold = bound( _delegateeScoreAboveThreshold, - calculator.delegateeScoreEligibilityThreshold(), + calculator.delegateeEligibilityThresholdScore(), type(uint256).max ); _newDelegateeScoreBelowThreshold = bound( - _newDelegateeScoreBelowThreshold, 0, calculator.delegateeScoreEligibilityThreshold() - 1 + _newDelegateeScoreBelowThreshold, 0, calculator.delegateeEligibilityThresholdScore() - 1 ); // time shorter than the eligibility delay but at least 1 because vm.wrap can't take 0. _timeShorterThanUpdateEligibilityDelay = @@ -183,17 +202,14 @@ contract GetNewEarningPower is EarningPowerCalculatorTest { calculator.updateDelegateeScore(_delegatee, _newDelegateeScoreBelowThreshold); vm.stopPrank(); - vm.warp( - calculator.lastDelegateeEligibilityChangeTime(_delegatee) - + _timeShorterThanUpdateEligibilityDelay - ); + vm.warp(calculator.timeOfIneligibility(_delegatee) + _timeShorterThanUpdateEligibilityDelay); (uint256 _earningPower, bool _isQualifiedForUpdate) = calculator.getNewEarningPower(_amountStaked, _staker, _delegatee, _oldEarningPower); assertEq(_earningPower, 0); assertEq(_isQualifiedForUpdate, false); } - function testFuzz_ReturnsZeroEarningPowerAndEligibleWhenDelegateScoreIsBelowEligibilityThresholdButOutsideTheUpdateEligibilityDelay( + function testFuzz_QualifiedNoEarningPowerAfterUpdateDelayAndDelegateeScoreDecrease( uint256 _amountStaked, address _staker, address _delegatee, @@ -201,7 +217,7 @@ contract GetNewEarningPower is EarningPowerCalculatorTest { uint256 _delegateeScore, uint256 _timeLengthBetweenUpdateEligibilityDelayAndStaleOracleWindow ) public { - _delegateeScore = bound(_delegateeScore, 0, calculator.delegateeScoreEligibilityThreshold() - 1); + _delegateeScore = bound(_delegateeScore, 0, calculator.delegateeEligibilityThresholdScore() - 1); _timeLengthBetweenUpdateEligibilityDelayAndStaleOracleWindow = bound( _timeLengthBetweenUpdateEligibilityDelayAndStaleOracleWindow, calculator.updateEligibilityDelay(), @@ -217,14 +233,7 @@ contract GetNewEarningPower is EarningPowerCalculatorTest { assertEq(_isQualifiedForUpdate, true); } - // Test Case: - // 1. Score >= Threshold: Delegatee is Eligible for full earning power. - // 2. Score < Threshold: Updated delegatee score falls below threshold. - // 3. Score < Threshold: Right before the updateEligibilityDelay is reached, updated delegatee - // score is still below threshold - // 4. _updateEligibilityDelay is reached. - // Result: We expect 0 earning power and true for _isQualifiedForUpdate. - function testFuzz_ReturnsZeroEarningPowerAndEligibleWhenDelegateScoreFallsBelowEligibilityThresholdButOutsideTheUpdateEligibilityDelayWithRecentBelowThresholdScoreUpdate( + function testFuzz_QualifiedNoEarningPowerAfterUpdateDelayAndLastScoreUpdateUnderThreshold( uint256 _amountStaked, address _staker, address _delegatee, @@ -236,11 +245,11 @@ contract GetNewEarningPower is EarningPowerCalculatorTest { uint256 _timeBeforeEligibilityDelay ) public { _delegateeScore = - bound(_delegateeScore, calculator.delegateeScoreEligibilityThreshold(), type(uint256).max); + bound(_delegateeScore, calculator.delegateeEligibilityThresholdScore(), type(uint256).max); _newDelegateeScore = - bound(_newDelegateeScore, 0, calculator.delegateeScoreEligibilityThreshold() - 1); + bound(_newDelegateeScore, 0, calculator.delegateeEligibilityThresholdScore() - 1); _updatedDelegateeScore = - bound(_updatedDelegateeScore, 0, calculator.delegateeScoreEligibilityThreshold() - 1); + bound(_updatedDelegateeScore, 0, calculator.delegateeEligibilityThresholdScore() - 1); _timeBeforeEligibilityDelay = bound(_timeBeforeEligibilityDelay, 0, calculator.updateEligibilityDelay() - 1); @@ -275,7 +284,7 @@ contract GetNewEarningPower is EarningPowerCalculatorTest { assertEq(_isQualifiedForUpdate, true); } - function testFuzz_ReturnsStakedAmountAsEarningPowerAndEligibleWhenDelegateScoreIsAboveEligibilityThresholdNotMatterTheTimeSinceLastDelegateeEligibilityChangeTime( + function testFuzz_QualifiedEarningPowerIsAmountStakedAfterUpdateDelayAndEligibleScore( uint256 _amountStaked, address _staker, address _delegatee, @@ -284,7 +293,7 @@ contract GetNewEarningPower is EarningPowerCalculatorTest { uint256 _timeSinceLastDelegateeEligibilityChangeTime ) public { _delegateeScore = - bound(_delegateeScore, calculator.delegateeScoreEligibilityThreshold(), type(uint256).max); + bound(_delegateeScore, calculator.delegateeEligibilityThresholdScore(), type(uint256).max); _timeSinceLastDelegateeEligibilityChangeTime = bound(_timeSinceLastDelegateeEligibilityChangeTime, 0, type(uint256).max - block.timestamp); vm.prank(scoreOracle); @@ -298,26 +307,83 @@ contract GetNewEarningPower is EarningPowerCalculatorTest { } } -contract LastDelegateeEligibilityChangeTime is EarningPowerCalculatorTest { - // Score below the threshold => above threshold; lastDelegateeEligibilityChangeTime is updated; - function testFuzz_SetsLastDelegateeEligibilityChangeTimeWhenADelegateBecomesEligible( +contract UpdateDelegateeScore is EarningPowerCalculatorTest { + function testFuzz_UpdatesDelegateScore(address _delegatee, uint256 _newScore) public { + vm.prank(scoreOracle); + calculator.updateDelegateeScore(_delegatee, _newScore); + assertEq(calculator.delegateeScores(_delegatee), _newScore); + assertEq(calculator.lastOracleUpdateTime(), block.timestamp); + } + + function testFuzz_UpdatesExistingDelegateScore( address _delegatee, - uint256 _delegateeScoreAboveThreshold, - uint256 _randomTimestamp + uint256 _firstScore, + uint256 _secondScore, + uint256 _timeInBetween ) public { - _delegateeScoreAboveThreshold = bound( - _delegateeScoreAboveThreshold, - calculator.delegateeScoreEligibilityThreshold(), - type(uint256).max + vm.startPrank(scoreOracle); + calculator.updateDelegateeScore(_delegatee, _firstScore); + uint256 _initialScore = calculator.delegateeScores(_delegatee); + uint256 _expectedInitialUpdate = block.timestamp; + uint256 _initialScoreUpdate = calculator.lastOracleUpdateTime(); + + vm.warp(_timeInBetween); + calculator.updateDelegateeScore(_delegatee, _secondScore); + vm.stopPrank(); + + assertEq(_initialScore, _firstScore); + assertEq(_initialScoreUpdate, _expectedInitialUpdate); + assertEq(calculator.delegateeScores(_delegatee), _secondScore); + assertEq(calculator.lastOracleUpdateTime(), _timeInBetween); + } + + function testFuzz_EmitsAnEventWhenDelegatesScoreIsUpdated(address _delegatee, uint256 _newScore) + public + { + vm.prank(scoreOracle); + vm.expectEmit(); + emit EarningPowerCalculator.DelegateeScoreUpdated(_delegatee, 0, _newScore); + calculator.updateDelegateeScore(_delegatee, _newScore); + } + + function testFuzz_RevertIf_CallerIsNotTheScoreOracle( + address _caller, + address _delegatee, + uint256 _newScore + ) public { + vm.assume(_caller != scoreOracle); + vm.prank(_caller); + vm.expectRevert( + abi.encodeWithSelector( + EarningPowerCalculator.BinaryEligibilityOracleEarningPowerCalculator__Unauthorized.selector, + bytes32("not oracle"), + _caller + ) ); - vm.warp(_randomTimestamp); + calculator.updateDelegateeScore(_delegatee, _newScore); + } + + function testFuzz_RevertIf_DelegateeScoreLocked( + address _delegatee, + uint256 _overrideScore, + uint256 _newScore + ) public { + vm.prank(owner); + calculator.overrideDelegateeScore(_delegatee, _overrideScore); vm.prank(scoreOracle); - calculator.updateDelegateeScore(_delegatee, _delegateeScoreAboveThreshold); - assertEq(calculator.lastDelegateeEligibilityChangeTime(_delegatee), _randomTimestamp); + vm.expectRevert( + abi.encodeWithSelector( + EarningPowerCalculator + .BinaryEligibilityOracleEarningPowerCalculator__DelegateeScoreLocked + .selector, + _delegatee + ) + ); + calculator.updateDelegateeScore(_delegatee, _newScore); } // Score above the threshold => below threshold; lastDelegateeEligibilityChangeTime is updated; - function testFuzz_SetsLastDelegateeEligibilityChangeTimeWhenADelegateBecomesIneligible( + function testFuzz_CorrectlyUpdatesAfterDelegateeIsIneligible( address _delegatee, uint256 _delegateeScoreAboveThreshold, uint256 _delegateeScoreBelowThreshold, @@ -325,11 +391,11 @@ contract LastDelegateeEligibilityChangeTime is EarningPowerCalculatorTest { ) public { _delegateeScoreAboveThreshold = bound( _delegateeScoreAboveThreshold, - calculator.delegateeScoreEligibilityThreshold(), + calculator.delegateeEligibilityThresholdScore(), type(uint256).max ); _delegateeScoreBelowThreshold = - bound(_delegateeScoreBelowThreshold, 0, calculator.delegateeScoreEligibilityThreshold() - 1); + bound(_delegateeScoreBelowThreshold, 0, calculator.delegateeEligibilityThresholdScore() - 1); vm.prank(scoreOracle); calculator.updateDelegateeScore(_delegatee, _delegateeScoreAboveThreshold); @@ -337,12 +403,30 @@ contract LastDelegateeEligibilityChangeTime is EarningPowerCalculatorTest { vm.warp(_randomTimestamp); vm.prank(scoreOracle); calculator.updateDelegateeScore(_delegatee, _delegateeScoreBelowThreshold); - assertEq(calculator.lastDelegateeEligibilityChangeTime(_delegatee), _randomTimestamp); + assertEq(calculator.timeOfIneligibility(_delegatee), _randomTimestamp); + } + + // Score below the threshold => above threshold; lastDelegateeEligibilityChangeTime is not + // updated; + function testFuzz_DoesNotUpdatesAfterDelegateeIsEligible( + address _delegatee, + uint256 _delegateeScoreAboveThreshold, + uint256 _randomTimestamp + ) public { + _delegateeScoreAboveThreshold = bound( + _delegateeScoreAboveThreshold, + calculator.delegateeEligibilityThresholdScore(), + type(uint256).max + ); + vm.warp(_randomTimestamp); + vm.prank(scoreOracle); + calculator.updateDelegateeScore(_delegatee, _delegateeScoreAboveThreshold); + assertEq(calculator.timeOfIneligibility(_delegatee), 0); } // Score >= threshold to score < threshold; lastDelegateeEligibilityChangeTime is not // updated. - function testFuzz_KeepsLastDelegateeEligibilityChangeTimeWhenAnIneligibleDelegateScoreIsUpdatedScoreBelowThreshold( + function testFuzz_DoesNotUpdateIfStillIneligible( address _delegatee, uint256 _delegateeScoreAboveThreshold, uint256 _delegateeScoreBelowThreshold, @@ -352,13 +436,13 @@ contract LastDelegateeEligibilityChangeTime is EarningPowerCalculatorTest { ) public { _delegateeScoreAboveThreshold = bound( _delegateeScoreAboveThreshold, - calculator.delegateeScoreEligibilityThreshold(), + calculator.delegateeEligibilityThresholdScore(), type(uint256).max ); _delegateeScoreBelowThreshold = - bound(_delegateeScoreBelowThreshold, 0, calculator.delegateeScoreEligibilityThreshold() - 1); + bound(_delegateeScoreBelowThreshold, 0, calculator.delegateeEligibilityThresholdScore() - 1); _updatedDelegateeScoreBelowThreshold = bound( - _updatedDelegateeScoreBelowThreshold, 0, calculator.delegateeScoreEligibilityThreshold() - 1 + _updatedDelegateeScoreBelowThreshold, 0, calculator.delegateeEligibilityThresholdScore() - 1 ); vm.assume(_expectedTimestamp < _randomTimestamp); @@ -376,77 +460,11 @@ contract LastDelegateeEligibilityChangeTime is EarningPowerCalculatorTest { calculator.updateDelegateeScore(_delegatee, _updatedDelegateeScoreBelowThreshold); vm.stopPrank(); - assertEq(calculator.lastDelegateeEligibilityChangeTime(_delegatee), _expectedTimestamp); + assertEq(calculator.timeOfIneligibility(_delegatee), _expectedTimestamp); } } -contract UpdateDelegateScore is EarningPowerCalculatorTest { - function testFuzz_UpdatesDelegateScore(address _delegatee, uint256 _newScore) public { - vm.prank(scoreOracle); - calculator.updateDelegateeScore(_delegatee, _newScore); - assertEq(calculator.delegateeScores(_delegatee), _newScore); - assertEq(calculator.lastOracleUpdateTime(), block.timestamp); - } - - function testFuzz_UpdatesExistingDelegateScore( - address _delegatee, - uint256 _firstScore, - uint256 _secondScore, - uint256 _timeInBetween - ) public { - vm.startPrank(scoreOracle); - calculator.updateDelegateeScore(_delegatee, _firstScore); - assertEq(calculator.delegateeScores(_delegatee), _firstScore); - assertEq(calculator.lastOracleUpdateTime(), block.timestamp); - - vm.warp(_timeInBetween); - - calculator.updateDelegateeScore(_delegatee, _secondScore); - assertEq(calculator.delegateeScores(_delegatee), _secondScore); - assertEq(calculator.lastOracleUpdateTime(), _timeInBetween); - vm.stopPrank(); - } - - function testFuzz_EmitsAnEventWhenDelegatesScoreIsUpdated(address _delegatee, uint256 _newScore) - public - { - vm.prank(scoreOracle); - vm.expectEmit(); - emit DelegateeScoreUpdated(_delegatee, 0, _newScore); - calculator.updateDelegateeScore(_delegatee, _newScore); - } - - function testFuzz_RevertIf_CallerIsNotTheScoreOracle( - address _caller, - address _delegatee, - uint256 _newScore - ) public { - vm.assume(_caller != scoreOracle); - vm.prank(_caller); - vm.expectRevert( - abi.encodeWithSelector( - EarningPowerCalculator.Unauthorized.selector, bytes32("not oracle"), _caller - ) - ); - calculator.updateDelegateeScore(_delegatee, _newScore); - } - - function testFuzz_RevertIf_DelegateeScoreLocked( - address _delegatee, - uint256 _overrideScore, - uint256 _newScore - ) public { - vm.prank(owner); - calculator.overrideDelegateeScore(_delegatee, _overrideScore); - vm.prank(scoreOracle); - vm.expectRevert( - abi.encodeWithSelector(EarningPowerCalculator.DelegateeScoreLocked.selector, _delegatee) - ); - calculator.updateDelegateeScore(_delegatee, _newScore); - } -} - -contract OverrideDelegateScore is EarningPowerCalculatorTest { +contract OverrideDelegateeScore is EarningPowerCalculatorTest { function testFuzz_OverrideDelegateScore(address _delegatee, uint256 _newScore, uint256 _timestamp) public { @@ -454,7 +472,7 @@ contract OverrideDelegateScore is EarningPowerCalculatorTest { vm.prank(owner); calculator.overrideDelegateeScore(_delegatee, _newScore); assertEq(calculator.delegateeScores(_delegatee), _newScore); - assertEq(calculator.delegateeScoreLock(_delegatee), true); + assertEq(calculator.delegateeScoreLockStatus(_delegatee), true); } function testFuzz_EmitsEventWhenDelegateScoreIsOverridden( @@ -465,9 +483,9 @@ contract OverrideDelegateScore is EarningPowerCalculatorTest { vm.warp(_timestamp); vm.prank(owner); vm.expectEmit(); - emit DelegateeScoreUpdated(_delegatee, 0, _newScore); + emit EarningPowerCalculator.DelegateeScoreUpdated(_delegatee, 0, _newScore); vm.expectEmit(); - emit DelegateeScoreLockSet(_delegatee, false, true); + emit EarningPowerCalculator.DelegateeScoreLockStatusSet(_delegatee, false, true); calculator.overrideDelegateeScore(_delegatee, _newScore); } @@ -478,9 +496,9 @@ contract OverrideDelegateScore is EarningPowerCalculatorTest { ) public { vm.assume(_caller != owner); vm.prank(_caller); - vm.expectRevert(abi.encodeWithSelector(OwnableUnauthorizedAccount.selector, _caller)); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, _caller)); calculator.overrideDelegateeScore(_delegatee, _newScore); - assertEq(calculator.delegateeScoreLock(_delegatee), false); + assertEq(calculator.delegateeScoreLockStatus(_delegatee), false); } } @@ -488,7 +506,7 @@ contract SetDelegateeScoreLock is EarningPowerCalculatorTest { function testFuzz_LocksOrUnlocksADelegateeScore(address _delegatee, bool _isLocked) public { vm.prank(owner); calculator.setDelegateeScoreLock(_delegatee, _isLocked); - assertEq(calculator.delegateeScoreLock(_delegatee), _isLocked); + assertEq(calculator.delegateeScoreLockStatus(_delegatee), _isLocked); } function testFuzz_EmitsAnEventWhenDelegateScoreIsLockedOrUnlocked( @@ -496,7 +514,9 @@ contract SetDelegateeScoreLock is EarningPowerCalculatorTest { bool _isLocked ) public { vm.expectEmit(); - emit DelegateeScoreLockSet(_delegatee, calculator.delegateeScoreLock(_delegatee), _isLocked); + emit EarningPowerCalculator.DelegateeScoreLockStatusSet( + _delegatee, calculator.delegateeScoreLockStatus(_delegatee), _isLocked + ); vm.prank(owner); calculator.setDelegateeScoreLock(_delegatee, _isLocked); } @@ -506,7 +526,7 @@ contract SetDelegateeScoreLock is EarningPowerCalculatorTest { { vm.assume(_caller != owner); vm.prank(_caller); - vm.expectRevert(abi.encodeWithSelector(OwnableUnauthorizedAccount.selector, _caller)); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, _caller)); calculator.setDelegateeScoreLock(_delegatee, _isLocked); } } @@ -521,14 +541,14 @@ contract SetScoreOracle is EarningPowerCalculatorTest { function testFuzz_EmitsAnEventWhenScoreOracleIsUpdated(address _newScoreOracle) public { vm.prank(owner); vm.expectEmit(); - emit ScoreOracleSet(scoreOracle, _newScoreOracle); + emit EarningPowerCalculator.ScoreOracleSet(scoreOracle, _newScoreOracle); calculator.setScoreOracle(_newScoreOracle); } function testFuzz_RevertIf_CallerIsNotOwner(address _caller, address _newScoreOracle) public { vm.assume(_caller != owner); vm.prank(_caller); - vm.expectRevert(abi.encodeWithSelector(OwnableUnauthorizedAccount.selector, _caller)); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, _caller)); calculator.setScoreOracle(_newScoreOracle); } } @@ -545,7 +565,9 @@ contract SetUpdateEligibilityDelay is EarningPowerCalculatorTest { ) public { vm.prank(owner); vm.expectEmit(); - emit UpdateEligibilityDelaySet(updateEligibilityDelay, _newUpdateEligibilityDelay); + emit EarningPowerCalculator.UpdateEligibilityDelaySet( + updateEligibilityDelay, _newUpdateEligibilityDelay + ); calculator.setUpdateEligibilityDelay(_newUpdateEligibilityDelay); } @@ -554,18 +576,18 @@ contract SetUpdateEligibilityDelay is EarningPowerCalculatorTest { { vm.assume(_caller != owner); vm.prank(_caller); - vm.expectRevert(abi.encodeWithSelector(OwnableUnauthorizedAccount.selector, _caller)); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, _caller)); calculator.setUpdateEligibilityDelay(_newUpdateEligibilityDelay); } } -contract SetDelegateScoreEligibilityThreshold is EarningPowerCalculatorTest { - function testFuzz_SetsTheDelegateScoreEligibilityThreshold( +contract SetDelegateeScoreEligibilityThreshold is EarningPowerCalculatorTest { + function testFuzz_CorrectlySetTheDelegateeScoreEligibilityThreshold( uint256 _newDelegateScoreEligibilityThreshold ) public { vm.prank(owner); calculator.setDelegateeScoreEligibilityThreshold(_newDelegateScoreEligibilityThreshold); - assertEq(calculator.delegateeScoreEligibilityThreshold(), _newDelegateScoreEligibilityThreshold); + assertEq(calculator.delegateeEligibilityThresholdScore(), _newDelegateScoreEligibilityThreshold); } function testFuzz_EmitsAnEventWhenDelegateScoreEligibilityThresholdIsUpdated( @@ -573,7 +595,7 @@ contract SetDelegateScoreEligibilityThreshold is EarningPowerCalculatorTest { ) public { vm.prank(owner); vm.expectEmit(); - emit DelegateeScoreEligibilityThresholdSet( + emit EarningPowerCalculator.DelegateeEligibilityThresholdScoreSet( delegateeScoreEligibilityThreshold, _newDelegateScoreEligibilityThreshold ); calculator.setDelegateeScoreEligibilityThreshold(_newDelegateScoreEligibilityThreshold); @@ -585,7 +607,7 @@ contract SetDelegateScoreEligibilityThreshold is EarningPowerCalculatorTest { ) public { vm.assume(_caller != owner); vm.prank(_caller); - vm.expectRevert(abi.encodeWithSelector(OwnableUnauthorizedAccount.selector, _caller)); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, _caller)); calculator.setDelegateeScoreEligibilityThreshold(_newDelegateScoreEligibilityThreshold); } } From ae7097bd6dc6891eb96dd568974a35e96c9dc60a Mon Sep 17 00:00:00 2001 From: garyghayrat Date: Mon, 7 Oct 2024 20:25:23 -0400 Subject: [PATCH 3/5] Remove vm.assume and update natspec --- src/BinaryEligibilityOracleEarningPowerCalculator.sol | 7 +++---- test/BinaryEligibilityOracleEarningPowerCalculator.t.sol | 1 - 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/BinaryEligibilityOracleEarningPowerCalculator.sol b/src/BinaryEligibilityOracleEarningPowerCalculator.sol index c90fd30..784f116 100644 --- a/src/BinaryEligibilityOracleEarningPowerCalculator.sol +++ b/src/BinaryEligibilityOracleEarningPowerCalculator.sol @@ -66,10 +66,9 @@ contract BinaryEligibilityOracleEarningPowerCalculator is Ownable, IEarningPower /// @notice Mapping to store delegatee scores. mapping(address delegatee => uint256 delegateeScore) public delegateeScores; - /// @notice Mapping to store the last score update timestamp where a delegatee's eligibility - /// changed. - /// @dev Key is the delegatee's address, value is the block.timestamp of the last eligibility - /// update. + /// @notice Mapping to store the last score update timestamp where a delegatee became ineligible. + /// @dev Key is the delegatee's address, value is the block.timestamp of when a delegatee's score + /// went below the `delegateeEligibilityThresholdScore`. mapping(address delegatee => uint256 timestamp) public timeOfIneligibility; /// @notice Mapping to store the lock status of delegate scores. diff --git a/test/BinaryEligibilityOracleEarningPowerCalculator.t.sol b/test/BinaryEligibilityOracleEarningPowerCalculator.t.sol index 2f51346..4bf974b 100644 --- a/test/BinaryEligibilityOracleEarningPowerCalculator.t.sol +++ b/test/BinaryEligibilityOracleEarningPowerCalculator.t.sol @@ -444,7 +444,6 @@ contract UpdateDelegateeScore is EarningPowerCalculatorTest { _updatedDelegateeScoreBelowThreshold = bound( _updatedDelegateeScoreBelowThreshold, 0, calculator.delegateeEligibilityThresholdScore() - 1 ); - vm.assume(_expectedTimestamp < _randomTimestamp); vm.startPrank(scoreOracle); calculator.updateDelegateeScore(_delegatee, _delegateeScoreAboveThreshold); From 7ea2e9939ab7ba0e7491d2e6a3368c11b15d12bb Mon Sep 17 00:00:00 2001 From: garyghayrat Date: Mon, 7 Oct 2024 20:38:06 -0400 Subject: [PATCH 4/5] Rename tests --- test/BinaryEligibilityOracleEarningPowerCalculator.t.sol | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/test/BinaryEligibilityOracleEarningPowerCalculator.t.sol b/test/BinaryEligibilityOracleEarningPowerCalculator.t.sol index 4bf974b..94eabd6 100644 --- a/test/BinaryEligibilityOracleEarningPowerCalculator.t.sol +++ b/test/BinaryEligibilityOracleEarningPowerCalculator.t.sol @@ -383,7 +383,7 @@ contract UpdateDelegateeScore is EarningPowerCalculatorTest { } // Score above the threshold => below threshold; lastDelegateeEligibilityChangeTime is updated; - function testFuzz_CorrectlyUpdatesAfterDelegateeIsIneligible( + function testFuzz_UpdatesTimeOfIneligibilityWhenDelegateeScoreDropsBelowThreshold( address _delegatee, uint256 _delegateeScoreAboveThreshold, uint256 _delegateeScoreBelowThreshold, @@ -408,7 +408,7 @@ contract UpdateDelegateeScore is EarningPowerCalculatorTest { // Score below the threshold => above threshold; lastDelegateeEligibilityChangeTime is not // updated; - function testFuzz_DoesNotUpdatesAfterDelegateeIsEligible( + function testFuzz_ReturnsCorrectTimeOfIneligibilityWhenDelegateeBecomesEligible( address _delegatee, uint256 _delegateeScoreAboveThreshold, uint256 _randomTimestamp @@ -424,9 +424,7 @@ contract UpdateDelegateeScore is EarningPowerCalculatorTest { assertEq(calculator.timeOfIneligibility(_delegatee), 0); } - // Score >= threshold to score < threshold; lastDelegateeEligibilityChangeTime is not - // updated. - function testFuzz_DoesNotUpdateIfStillIneligible( + function testFuzz_ReturnsCorrectTimeOfIneligibilityWhenAnIneligibleDelegateeScoreIsUpdatedWithAScoreBelowThreshold( address _delegatee, uint256 _delegateeScoreAboveThreshold, uint256 _delegateeScoreBelowThreshold, From 8669241f256d9775fbca5b9fb33a2a753f8f9da4 Mon Sep 17 00:00:00 2001 From: garyghayrat Date: Tue, 8 Oct 2024 11:49:57 -0400 Subject: [PATCH 5/5] Remove natspec --- src/interfaces/IEarningPowerCalculator.sol | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/interfaces/IEarningPowerCalculator.sol b/src/interfaces/IEarningPowerCalculator.sol index 43f9e65..48b39b3 100644 --- a/src/interfaces/IEarningPowerCalculator.sol +++ b/src/interfaces/IEarningPowerCalculator.sol @@ -4,24 +4,11 @@ pragma solidity ^0.8.23; /// @notice Interface for calculating earning power of a staker based on their delegate score in /// GovernanceStaker. interface IEarningPowerCalculator { - /// @notice Calculates the earning power for a given delegate and staking amount. - /// @param _amountStaked The amount of tokens staked. - /// @param _staker The address of the staker. - /// @param _delegatee The address of the delegatee. - /// @return _earningPower The calculated earning power. function getEarningPower(uint256 _amountStaked, address _staker, address _delegatee) external view returns (uint256 _earningPower); - /// @notice Calculates the new earning power and determines if it qualifies for an update.` - /// @param _amountStaked The amount of tokens staked. - /// @param _staker The address of the staker. - /// @param _delegatee The address of the delegatee. - /// @param _oldEarningPower The previous earning power value. - /// @return _newEarningPower The newly calculated earning power. - /// @return _isQualifiedForUpdate Boolean indicating if the new earning power qualifies for an - /// update. function getNewEarningPower( uint256 _amountStaked, address _staker,