Skip to content

Conversation

alexkeating
Copy link
Collaborator

No description provided.

@github-actions
Copy link

Coverage after merging feature/reward-distributor into main will be

72.83%

Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
src
   DelegationSurrogate.sol100%100%100%100%
   DelegationSurrogateVotes.sol100%100%100%100%
   RewardDistributor.sol0%0%0%0%173–175, 179–181, 187–189, 196–199, 205–207, 214–215, 223–224, 226–227, 241–242, 252–255, 262–264, 264–265, 267, 278, 283, 283, 285, 287–288, 290, 292, 295, 295–296, 299, 299–300, 304–305, 305–306, 309, 314, 316, 319, 322–323, 327, 333, 339–342, 346–349, 354–356, 361, 363, 365, 365, 367, 374, 380, 385–387, 391–393, 395, 399, 401–402, 406–408, 417–419, 427, 432, 439, 444–445, 447, 449–450, 453, 456–457, 459, 461, 463, 466, 468–469, 469–470, 474, 479–480, 480, 486–487, 487, 494, 499, 499, 517–518, 518, 522, 524, 524, 524–525, 527–528, 531–532, 534, 534, 542–543, 543, 545, 551–553
   RewardDistributorDelegateInitializer.sol0%0%0%0%10–11, 13–14, 16, 16, 18–19
   Staker.sol100%100%100%100%
src/calculators
   BinaryEligibilityOracleEarningPowerCalculator.sol100%100%100%100%
   BinaryVotingPowerEarningPowerCalculator.sol0%0%0%0%13, 32–34, 37, 44–46, 48–49, 52, 58–60, 64, 66, 66–69, 72, 76–78, 81–82, 86–87
   IdentityEarningPowerCalculator.sol100%100%100%100%
src/extensions
   StakerCapDeposits.sol100%100%100%100%
   StakerDelegateSurrogateVotes.sol100%100%100%100%
   StakerOnBehalf.sol100%100%100%100%
   StakerPermitAndStake.sol100%100%100%100%
src/notifiers
   MintRewardNotifier.sol100%100%100%100%
   RewardTokenNotifierBase.sol100%100%100%100%
   TransferFromRewardNotifier.sol100%100%100%100%
   TransferRewardNotifier.sol100%100%100%100%

/// @notice The maximum value to which the claim fee can be set.
/// @dev For anything other than a zero value, this immutable parameter should be set in the
/// constructor of a concrete implementation inheriting from Staker.
uint256 public immutable MAX_CLAIM_FEE;

Check failure

Code scanning / Slither

Uninitialized state variables High

Comment on lines +214 to +217
function lastTimeRewardDistributed() public view virtual returns (uint256) {
if (rewardEndTime <= block.timestamp) return rewardEndTime;
else return block.timestamp;
}

Check notice

Code scanning / Slither

Block timestamp Low

RewardDistributor.lastTimeRewardDistributed() uses timestamp for comparisons
Dangerous comparisons:
- rewardEndTime <= block.timestamp
Comment on lines +439 to +475
function _claimReward(
DepositIdentifier _depositId,
DelegateReward storage deposit,
address _claimer
) internal virtual returns (uint256) {
_checkpointGlobalReward();
_checkpointReward(deposit);

uint256 _reward = deposit.scaledUnclaimedRewardCheckpoint / SCALE_FACTOR;
// Intentionally reverts due to overflow if unclaimed rewards are less than fee.
uint256 _payout = _reward - claimFeeParameters.feeAmount;
if (_payout == 0) return 0;

// retain sub-wei dust that would be left due to the precision loss
deposit.scaledUnclaimedRewardCheckpoint =
deposit.scaledUnclaimedRewardCheckpoint - (_reward * SCALE_FACTOR);

uint256 _newEarningPower =
earningPowerCalculator.getEarningPower(0, deposit.owner, deposit.owner);

emit RewardClaimed(_depositId, _claimer, _payout, _newEarningPower);

totalEarningPower =
_calculateTotalEarningPower(deposit.earningPower, _newEarningPower, totalEarningPower);
depositorTotalEarningPower[deposit.owner] = _calculateTotalEarningPower(
deposit.earningPower, _newEarningPower, depositorTotalEarningPower[deposit.owner]
);
deposit.earningPower = _newEarningPower.toUint96();

SafeERC20.safeTransfer(REWARD_TOKEN, _claimer, _payout);
if (claimFeeParameters.feeAmount > 0) {
SafeERC20.safeTransfer(
REWARD_TOKEN, claimFeeParameters.feeCollector, claimFeeParameters.feeAmount
);
}
return _payout;
}
Comment on lines +517 to +546
function notifyRewardAmount(uint256 _amount) external virtual {
if (!isRewardNotifier[msg.sender]) revert Staker__Unauthorized("not notifier", msg.sender);

// We checkpoint the accumulator without updating the timestamp at which it was updated,
// because that second operation will be done after updating the reward rate.
rewardPerTokenAccumulatedCheckpoint = rewardPerTokenAccumulated();

if (block.timestamp >= rewardEndTime) {
scaledRewardRate = (_amount * SCALE_FACTOR) / REWARD_DURATION;
} else {
uint256 _remainingReward = scaledRewardRate * (rewardEndTime - block.timestamp);
scaledRewardRate = (_remainingReward + _amount * SCALE_FACTOR) / REWARD_DURATION;
}

rewardEndTime = block.timestamp + REWARD_DURATION;
lastCheckpointTime = block.timestamp;

if ((scaledRewardRate / SCALE_FACTOR) == 0) revert Staker__InvalidRewardRate();

// This check cannot _guarantee_ sufficient rewards have been transferred to the contract,
// because it cannot isolate the unclaimed rewards owed to stakers left in the balance. While
// this check is useful for preventing degenerate cases, it is not sufficient. Therefore, it is
// critical that only safe reward notifier contracts are approved to call this method by the
// admin.
if (
(scaledRewardRate * REWARD_DURATION) > (REWARD_TOKEN.balanceOf(address(this)) * SCALE_FACTOR)
) revert Staker__InsufficientRewardBalance();

emit RewardNotified(_amount, msg.sender);
}
Comment on lines 37 to 50
function getEarningPower(uint256, /* _amountStaked */ address, /* _staker */ address _delegatee)
external
view
virtual
override
returns (uint256)
{
uint48 _votingPowerTimepoint = _getVotingPowerTimepoint();
uint256 _votingPower =
IVotes(VOTING_POWER_TOKEN).getPastVotes(_delegatee, _votingPowerTimepoint);

if (_isOracleStale() || isOraclePaused) return _votingPower;
return _isDelegateeEligible(_delegatee) ? _votingPower : 0;
}

Check notice

Code scanning / Slither

Block timestamp Low

Comment on lines 52 to 73
function getNewEarningPower(
uint256, /* _amountStaked */
address, /* _staker */
address _delegatee,
uint256 /* _oldEarningPower */
) external view virtual override returns (uint256, bool) {
uint48 _votingPowerTimepoint = _getVotingPowerTimepoint();
uint256 _votingPower =
IVotes(VOTING_POWER_TOKEN).getPastVotes(_delegatee, _votingPowerTimepoint);

// TODO: Do we want the same fallback behavior.
// Should we instead accept the stale values if paused?
if (_isOracleStale() || isOraclePaused) return (_votingPower, true);

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

return (_votingPower, true);
}

Check notice

Code scanning / Slither

Block timestamp Low

uint256 _votingPower =
IVotes(VOTING_POWER_TOKEN).getPastVotes(_delegatee, _votingPowerTimepoint);

if (_isOracleStale() || isOraclePaused) return _votingPower;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep this but read other epc, from staker

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the address we'd need to look at, the address which represents the delegate in this case, is the one passed as the _staker to the EPC, i.e. the owner of the "deposit" in the DelegateCompensationStaker, right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good question. I updated the owner and delegate to be the same address for both spikes so it shouldn't matter, but it may matter for future implementations. Which would be better? Happy to choose either.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make the EPC methods public on BinaryEligibilityEPC, add to existing pr, add tests for new public methods

Comment on lines +65 to +67
function _fetchOrDeploySurrogate(address) internal pure override returns (DelegationSurrogate) {
revert();
}

Check warning

Code scanning / Slither

Dead-code Warning

depositorTotalEarningPower[_delegate] += _earningPower;
deposits[_depositId] = Deposit({
balance: 0,
delegatee: address(0),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think delegating to address(0) would revert in Staker. The easiest way to handle it is probably just to pick a non-zero, non "real" address to set as the delegate for every "deposit"

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this will revert unless we have to create surrogates.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now, I think it is probably better to make this the provided delegate address to avoid issues in the earning power calculator

return _depositId;
}

function _fetchOrDeploySurrogate(address) internal pure override returns (DelegationSurrogate) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would we want this to revert? Wouldn't we need to do this once for whatever the fake "delegatee" is for every deposti?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I am missing something, do we still have the concept of a delegatee for deposit? I was under the impression delegates were simply accumulating rewards and not delegating them. I think we can revert if my understanding is correct as this is only used in _stake and _alterDelegatee which we no longer use.

uint48 public votingPowerUpdateFrequency;
uint48 public immutable UPDATE_START_TIME;
address public immutable VOTING_POWER_TOKEN;
BinaryEligibilityOracleEarningPowerCalculator public eligibilityModule;

Check warning

Code scanning / Slither

State variables that could be declared immutable Warning

BinaryEligibilityOracleEarningPowerCalculator public eligibilityModule;

constructor(
address _owner,

Check notice

Code scanning / Slither

Local variable shadowing Low

constructor(
address _owner,
address _eligibilityAddress,
address _votingPowerToken,

Check notice

Code scanning / Slither

Missing zero address validation Low

Comment on lines +75 to +78
function _isOracleStale() internal view returns (bool) {
return block.timestamp - eligibilityModule.lastOracleUpdateTime()
> eligibilityModule.STALE_ORACLE_WINDOW();
}

Check notice

Code scanning / Slither

Block timestamp Low

VOTING_POWER_TOKEN = _votingPowerToken;
}

function getEarningPower(uint256, /* _amountStaked */ address, /* _staker */ address _delegatee)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's switch to use _staker

returns (uint256)
{
uint48 _votingPowerTimepoint = _getVotingPowerTimepoint();
uint256 _votingPower =
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switch to square root calculation

import {DelegationSurrogate} from "./DelegationSurrogate.sol";
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";

abstract contract DelegateCompensationStaker is Staker {
Copy link
Collaborator Author

@alexkeating alexkeating Jun 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Testing strategy

  1. Use fakes for other EPC, and token
  2. Add some integration post phase 1

Staker

  1. Simple smoke stakes, and other methods revert. Where there is smoke there is fire testing.
  2. Minimum, tests that demonstrate the functionality is left still works and interacts properly with the new changes i.e. reward distribution and claiming.
  3. At least one test for every public methods to make sure the happy is working.

@alexkeating alexkeating closed this Aug 4, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants