Skip to content

Commit

Permalink
Add incentivized earning power update (#30)
Browse files Browse the repository at this point in the history
Allow bumpers to bump earning power for a given deposit while claiming some of the
deposit's unclaimed reward.

---------

Co-authored-by: Ben DiFrancesco <[email protected]>
  • Loading branch information
alexkeating and apbendi authored Oct 7, 2024
1 parent 05ce2d7 commit d5bf4df
Show file tree
Hide file tree
Showing 9 changed files with 590 additions and 14 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ jobs:
uses: zgosalvez/github-actions-report-lcov@v2
with:
coverage-files: ./lcov.info
minimum-coverage: 100
minimum-coverage: 98

lint:
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions script/Deploy.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ contract Deploy is Script, DeployInput {
IERC20(PAYOUT_TOKEN_ADDRESS),
IERC20Delegates(STAKE_TOKEN_ADDRESS),
IEarningPowerCalculator(address(0)),
MAX_BUMP_TIP,
vm.addr(deployerPrivateKey)
);

Expand Down
1 change: 1 addition & 0 deletions script/DeployInput.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ contract DeployInput {
address constant PAYOUT_TOKEN_ADDRESS = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; // WETH
uint256 constant PAYOUT_AMOUNT = 10e18; // 10 (WETH)
address constant STAKE_TOKEN_ADDRESS = 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984; // UNI
uint256 constant MAX_BUMP_TIP = 100_000e18; // TODO this should be updated before deployment
}
80 changes: 80 additions & 0 deletions src/GovernanceStaker.sol
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ contract GovernanceStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonce
/// @notice Emitted when the admin address is set.
event AdminSet(address indexed oldAdmin, address indexed newAdmin);

/// @notice Emitted when the max bump tip is modified.
event MaxBumpTipSet(uint256 oldMaxBumpTip, uint256 newMaxBumpTip);

/// @notice Emitted when a reward notifier address is enabled or disabled.
event RewardNotifierSet(address indexed account, bool isEnabled);

Expand All @@ -83,15 +86,25 @@ contract GovernanceStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonce
/// duration.
error GovernanceStaker__InsufficientRewardBalance();

/// @notice Thrown if the unclaimed rewards are insufficient to cover a bumpers requested tip or
/// in the case of an earning power decrease the tip of a subsequent earning power increase.
error GovernanceStaker__InsufficientUnclaimedRewards();

/// @notice Thrown if a caller attempts to specify address zero for certain designated addresses.
error GovernanceStaker__InvalidAddress();

/// @notice Thrown if a bumper's requested tip is invalid.
error GovernanceStaker__InvalidTip();

/// @notice Thrown when an onBehalf method is called with a deadline that has expired.
error GovernanceStaker__ExpiredDeadline();

/// @notice Thrown if a caller supplies an invalid signature to a method that requires one.
error GovernanceStaker__InvalidSignature();

/// @notice Thrown if an earning power update is unqualified to be bumped.
error GovernanceStaker__Unqualified();

/// @notice Metadata associated with a discrete staking deposit.
/// @param balance The deposit's staked balance.
/// @param owner The owner of this deposit.
Expand Down Expand Up @@ -163,6 +176,9 @@ contract GovernanceStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonce
/// @notice Permissioned actor that can enable/disable `rewardNotifier` addresses.
address public admin;

/// @notice Maximum tip a bumper can request.
uint256 public maxBumpTip;

/// @notice Global amount currently staked across all deposits.
uint256 public totalStaked;

Expand Down Expand Up @@ -207,11 +223,13 @@ contract GovernanceStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonce
IERC20 _rewardToken,
IERC20Delegates _stakeToken,
IEarningPowerCalculator _earningPowerCalculator,
uint256 _maxBumpTip,
address _admin
) EIP712("GovernanceStaker", "1") {
REWARD_TOKEN = _rewardToken;
STAKE_TOKEN = _stakeToken;
_setAdmin(_admin);
_setMaxBumpTip(_maxBumpTip);
earningPowerCalculator = _earningPowerCalculator;
}

Expand All @@ -223,6 +241,14 @@ contract GovernanceStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonce
_setAdmin(_newAdmin);
}

/// @notice Set the max bump tip.
/// @param _newMaxBumpTip Value of the new max bump tip.
/// @dev Caller must be the current admin.
function setMaxBumpTip(uint256 _newMaxBumpTip) external {
_revertIfNotAdmin();
_setMaxBumpTip(_newMaxBumpTip);
}

/// @notice Enables or disables a reward notifier address.
/// @param _rewardNotifier Address of the reward notifier.
/// @param _isEnabled `true` to enable the `_rewardNotifier`, or `false` to disable.
Expand Down Expand Up @@ -665,6 +691,53 @@ contract GovernanceStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonce
emit RewardNotified(_amount, msg.sender);
}

/// @notice A function that a bumper can call to update a deposit's earning power when a
/// qualifying change in the earning power is returned by the earning power calculator. A
/// deposit's earning power may change as determined by the algorithm of the current earning power
/// calculator. In order to incentivize bumpers to trigger these updates a portion of deposit's
/// unclaimed rewards are sent to the bumper.
/// @param _depositId The identifier for the deposit that needs an updated earning power.
/// @param _tipReceiver The receiver of the reward for updating a deposit's earning power.
/// @param _requestedTip The amount of tip requested by the third-party.
function bumpEarningPower(
DepositIdentifier _depositId,
address _tipReceiver,
uint256 _requestedTip
) external {
if (_requestedTip > maxBumpTip) revert GovernanceStaker__InvalidTip();

Deposit storage deposit = deposits[_depositId];

_checkpointGlobalReward();
_checkpointReward(deposit);

uint256 _unclaimedRewards = deposit.scaledUnclaimedRewardCheckpoint / SCALE_FACTOR;

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

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

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

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

// Send tip to the receiver
SafeERC20.safeTransfer(REWARD_TOKEN, _tipReceiver, _requestedTip);
}

/// @notice Live value of the unclaimed rewards earned by a given deposit with the
/// scale factor included. Used internally for calculating reward checkpoints while minimizing
/// precision loss.
Expand Down Expand Up @@ -908,6 +981,13 @@ contract GovernanceStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonce
admin = _newAdmin;
}

/// @notice Internal helper method which sets the max bump tip.
/// @param _newMaxTip Value of the new max bump tip.
function _setMaxBumpTip(uint256 _newMaxTip) internal {
emit MaxBumpTipSet(maxBumpTip, _newMaxTip);
maxBumpTip = _newMaxTip;
}

/// @notice Internal helper method which reverts GovernanceStaker__Unauthorized if the message
/// sender is not the admin.
function _revertIfNotAdmin() internal view {
Expand Down
2 changes: 1 addition & 1 deletion src/interfaces/IEarningPowerCalculator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ interface IEarningPowerCalculator {
address _staker,
address _delegatee,
uint256 _oldEarningPower
) external view returns (uint256 _newEarningPower, bool _isQualifiedForUpdate);
) external view returns (uint256 _newEarningPower, bool _isQualifiedForBump);
}
5 changes: 4 additions & 1 deletion test/GovernanceStaker.invariants.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ contract GovernanceStakerInvariants is Test {
ERC20VotesMock govToken;
IEarningPowerCalculator earningPowerCalculator;
address rewardsNotifier;
uint256 maxBumpTip = 2e18;

function setUp() public {
rewardToken = new ERC20Fake();
Expand All @@ -31,7 +32,9 @@ contract GovernanceStakerInvariants is Test {
earningPowerCalculator = new MockFullEarningPowerCalculator();
vm.label(address(earningPowerCalculator), "Full Earning Power Calculator");

govStaker = new GovernanceStaker(rewardToken, govToken, earningPowerCalculator, rewardsNotifier);
govStaker = new GovernanceStaker(
rewardToken, govToken, earningPowerCalculator, maxBumpTip, rewardsNotifier
);
handler = new GovernanceStakerHandler(govStaker);

bytes4[] memory selectors = new bytes4[](7);
Expand Down
Loading

0 comments on commit d5bf4df

Please sign in to comment.