Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 75 additions & 6 deletions protocol-contracts/staking/contracts/OperatorRewarder.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,35 @@ contract OperatorRewarder {
mapping(address => int256) private _rewardsPaid;
mapping(address => address) private _authorizedClaimers;

/// @notice Emitted when the beneficiary is transferred.
/**
* @notice Emitted when the beneficiary is transferred.
* @param oldBeneficiary The previous beneficiary address.
* @param newBeneficiary The new beneficiary address.
*/
event BeneficiaryTransferred(address oldBeneficiary, address newBeneficiary);

/// @notice Emitted when the contract is shut down.
event Shutdown();

/// @notice Emitted when the maximum fee is updated.
/**
* @notice Emitted when the maximum fee is updated.
* @param oldFee The previous maximum fee in basis points.
* @param newFee The new maximum fee in basis points.
*/
event MaxFeeUpdated(uint16 oldFee, uint16 newFee);

/// @notice Emitted when the fee is updated.
/**
* @notice Emitted when the fee is updated.
* @param oldFee The previous fee in basis points.
* @param newFee The new fee in basis points.
*/
event FeeUpdated(uint16 oldFee, uint16 newFee);

/// @notice Emitted when an address is authorized to claim rewards on behalf of the receiver address.
/**
* @notice Emitted when an address is authorized to claim rewards on behalf of the receiver address.
* @param receiver The address that will receive the rewards.
* @param claimer The address authorized to claim rewards on behalf of the receiver.
*/
event ClaimerAuthorized(address receiver, address claimer);

/// @notice Error for invalid claimer address.
Expand Down Expand Up @@ -326,19 +342,34 @@ contract OperatorRewarder {
return SafeCast.toUint256(SignedMath.max(0, allocation - _rewardsPaid[account]));
}

/**
* @notice Returns the total historical rewards distributed.
* @dev This amount is computed as the sum of:
* - the total rewards accumulated in the contract (see `_totalAssetsPlusPaidRewards()`) from
* the start of the contract
* - minus the fees not yet claimed by the beneficiary (see `_unpaidFee()`)
* @return The total historical rewards amount.
*/
function historicalReward() public view virtual returns (uint256) {
uint256 totalAssetsPlusPaidRewards = _totalAssetsPlusPaidRewards();
return totalAssetsPlusPaidRewards - _unpaidFee(totalAssetsPlusPaidRewards);
}

/**
* @notice Returns unpaid fee.
* @notice Returns unpaid fee (not yet claimed by the beneficiary).
* @return Amount of unpaid fee.
*/
function unpaidFee() public view virtual returns (uint256) {
return _unpaidFee(_totalAssetsPlusPaidRewards());
}

/**
* @notice Transfers the specified amount of tokens to the specified address.
* @dev If the amount to transfer is greater than the balance of the rewarder, it will first
* claim rewards from the ProtocolStaking contract.
* @param to The address to transfer the tokens to.
* @param amount The amount of tokens to transfer.
*/
function _doTransferOut(address to, uint256 amount) internal {
IERC20 token_ = token();
if (amount > token_.balanceOf(address(this))) {
Expand Down Expand Up @@ -370,7 +401,12 @@ contract OperatorRewarder {
function _claimFee() internal virtual {
uint256 totalAssetsPlusPaidRewards = _totalAssetsPlusPaidRewards();
uint256 unpaidFee_ = _unpaidFee(totalAssetsPlusPaidRewards);

// Update the last claim value used to define the next unpaid fee (see `_unpaidFee()`).
// This amount is exactly the same as `historicalReward()`, but we need to get the unpaid
// fee separately in order to send the fee to the beneficiary below.
_lastClaimTotalAssetsPlusPaidRewards = totalAssetsPlusPaidRewards - unpaidFee_;

if (unpaidFee_ > 0) {
_doTransferOut(beneficiary(), unpaidFee_);
}
Expand Down Expand Up @@ -410,19 +446,52 @@ contract OperatorRewarder {
_feeBasisPoints = basisPoints;
}

/**
* @notice Returns the total assets plus earned rewards plus paid rewards.
* @dev This amount is computed as the sum of:
* - the balance of the rewarder contract (includes: total claimed but unpaid rewards + total donation + unpaid fees)
* - the earned rewards by the operator staking contract (total earned but unpaid rewards)
* - the total rewards paid to the delegators (total paid rewards)
* @return Total assets plus earned rewards plus paid rewards.
*/
function _totalAssetsPlusPaidRewards() internal view returns (uint256) {
return
token().balanceOf(address(this)) +
(isShutdown() ? 0 : protocolStaking().earned(address(operatorStaking()))) +
_totalRewardsPaid;
}

/**
* @notice Returns the unpaid fee.
* @dev This amount is computed as a percentage (defined in basis points) of the amount of rewards
* accumulated since the last time fees were claimed. This works because:
* - claiming fees snapshots the historical rewards amount in the `_lastClaimTotalAssetsPlusPaidRewards`
* value, which monitors all rewards accumulated since the start of the contract by excluding fees.
* - the total rewards amount (`_totalAssetsPlusPaidRewards()`) is computed such as it is always
* increasing, except when fees are claimed (where fees are transferred to the beneficiary).
* This makes sure that the difference between both above values does not take claimed fees
* into account when computing the next unpaid fees.
* @param totalAssetsPlusPaidRewards The total assets plus earned rewards plus paid rewards.
* @return Amount of unpaid fee.
*/
function _unpaidFee(uint256 totalAssetsPlusPaidRewards) internal view returns (uint256) {
uint256 totalAssetsPlusPaidRewardsDelta = totalAssetsPlusPaidRewards - _lastClaimTotalAssetsPlusPaidRewards;
return (totalAssetsPlusPaidRewardsDelta * feeBasisPoints()) / 10_000;
}

/// @dev Compute total allocation based on number of shares and total shares. Must take paid rewards into account after.
/**
* @notice Compute total allocation based on number of shares and total shares.
* @param share The number of shares.
* @param total The total number of shares.
* @return The total allocation.
* The allocation corresponds to the rewards (both real and virtual) a user would receive if
* the current weight distribution, with their updated stake, was constant since the deployment
* of the protocol.
* Total reward amount is computed as the sum of:
* - historical rewards: total of all rewards generated up to that point
* - paid virtual rewards: a pool of "virtual" rewards that account for changes in the weight distribution
* Note: the `mulDiv` rounds down: floor(totalRewards * share / total)
*/
function _allocation(uint256 share, uint256 total) private view returns (uint256) {
return
SafeCast.toUint256(SafeCast.toInt256(historicalReward()) + _totalVirtualRewardsPaid).mulDiv(share, total);
Expand Down
23 changes: 20 additions & 3 deletions protocol-contracts/staking/contracts/OperatorStaking.sol
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,23 @@ contract OperatorStaking is ERC1363Upgradeable, ReentrancyGuardTransient, UUPSUp
bytes32 private constant OPERATOR_STAKING_STORAGE_LOCATION =
0x7fc851282090a0d8832502c48739eac98a0856539351f17cb5d5950c860fd200;

/// @dev Emitted when an operator is set or unset for a controller.
/**
* @dev Emitted when an operator is set or unset for a controller.
* @param controller The controller address.
* @param operator The operator address.
* @param approved True if the operator is approved, false if revoked.
*/
event OperatorSet(address indexed controller, address indexed operator, bool approved);

/// @dev Emitted when a redeem request is made.
/**
* @dev Emitted when a redeem request is made.
* @param controller The controller address for the redeem request.
* @param owner The owner of the shares being redeemed.
* @param requestId The unique identifier for the redeem request.
* @param sender The address that initiated the redeem request.
* @param shares The number of shares requested to redeem.
* @param releaseTime The timestamp when the shares can be released.
*/
event RedeemRequest(
address indexed controller,
address indexed owner,
Expand All @@ -60,7 +73,11 @@ contract OperatorStaking is ERC1363Upgradeable, ReentrancyGuardTransient, UUPSUp
uint48 releaseTime
);

/// @dev Emitted when the rewarder contract is set.
/**
* @dev Emitted when the rewarder contract is set.
* @param oldRewarder The previous rewarder contract address.
* @param newRewarder The new rewarder contract address.
*/
event RewarderSet(address oldRewarder, address newRewarder);

/// @dev Thrown when the caller is not the ProtocolStaking's owner.
Expand Down
75 changes: 64 additions & 11 deletions protocol-contracts/staking/contracts/ProtocolStaking.sol
Original file line number Diff line number Diff line change
Expand Up @@ -54,19 +54,53 @@ contract ProtocolStaking is AccessControlDefaultAdminRulesUpgradeable, ERC20Vote
bytes32 private constant MANAGER_ROLE = keccak256("MANAGER_ROLE");
bytes32 private constant ELIGIBLE_ACCOUNT_ROLE = keccak256("ELIGIBLE_ACCOUNT_ROLE");

/// @dev Emitted when tokens are staked by an account.
/**
* @dev Emitted when tokens are staked by an account.
* @param account The address of the account staking tokens.
* @param amount The amount of tokens staked.
*/
event TokensStaked(address indexed account, uint256 amount);
/// @dev Emitted when tokens are unstaked by an account.

/**
* @dev Emitted when tokens are unstaked by an account.
* @param account The address of the account unstaking tokens.
* @param amount The amount of tokens unstaked.
* @param releaseTime The timestamp when the tokens can be released.
*/
event TokensUnstaked(address indexed account, uint256 amount, uint48 releaseTime);
/// @dev Emitted when tokens are released to a recipient after the unstaking cooldown period.

/**
* @dev Emitted when tokens are released to a recipient after the unstaking cooldown period.
* @param recipient The address receiving the released tokens.
* @param amount The amount of tokens released.
*/
event TokensReleased(address indexed recipient, uint256 amount);
/// @dev Emitted when rewards of an account are claimed.

/**
* @dev Emitted when rewards of an account are claimed.
* @param account The address of the account whose rewards are claimed.
* @param recipient The address receiving the claimed rewards.
* @param amount The amount of rewards claimed.
*/
event RewardsClaimed(address indexed account, address indexed recipient, uint256 amount);
/// @dev Emitted when the reward rate is updated.

/**
* @dev Emitted when the reward rate is updated.
* @param rewardRate The new reward rate in tokens per second.
*/
event RewardRateSet(uint256 rewardRate);
/// @dev Emitted when the unstake cooldown is updated.

/**
* @dev Emitted when the unstake cooldown is updated.
* @param unstakeCooldownPeriod The new unstake cooldown period in seconds.
*/
event UnstakeCooldownPeriodSet(uint256 unstakeCooldownPeriod);
/// @dev Emitted when the reward recipient of an account is updated.

/**
* @dev Emitted when the reward recipient of an account is updated.
* @param account The address of the account whose reward recipient is updated.
* @param recipient The new reward recipient address.
*/
event RewardsRecipientSet(address indexed account, address indexed recipient);

/// @dev The account cannot be made eligible.
Expand All @@ -81,7 +115,17 @@ contract ProtocolStaking is AccessControlDefaultAdminRulesUpgradeable, ERC20Vote
_disableInitializers();
}

/// @dev Initializes this upgradeable protocol staking contract.
/**
* @dev Initializes this upgradeable protocol staking contract.
* @param name The name of the ERC20 token representing staked tokens.
* @param symbol The symbol of the ERC20 token representing staked tokens.
* @param version The version string for EIP712 domain separator.
* @param stakingToken_ The address of the token used for staking and rewards.
* @param governor The address granted the default admin role.
* @param manager The address granted the manager role.
* @param initialUnstakeCooldownPeriod The initial unstake cooldown period in seconds.
* @param initialRewardRate The initial reward rate in tokens per second.
*/
function initialize(
string memory name,
string memory symbol,
Expand Down Expand Up @@ -230,7 +274,10 @@ contract ProtocolStaking is AccessControlDefaultAdminRulesUpgradeable, ERC20Vote
return SafeCast.toUint256(SignedMath.max(0, SafeCast.toInt256(allocation) - $._paid[account]));
}

/// @dev Returns the staking token which is used for staking and rewards.
/**
* @dev Returns the staking token which is used for staking and rewards.
* @return The address of the staking token.
*/
function stakingToken() public view returns (address) {
return _getProtocolStakingStorage()._stakingToken;
}
Expand All @@ -244,12 +291,18 @@ contract ProtocolStaking is AccessControlDefaultAdminRulesUpgradeable, ERC20Vote
return Math.sqrt(amount);
}

/// @dev Returns the current total staked weight.
/**
* @dev Returns the current total staked weight.
* @return The total staked weight of all eligible accounts.
*/
function totalStakedWeight() public view returns (uint256) {
return _getProtocolStakingStorage()._totalEligibleStakedWeight;
}

/// @dev Returns the current unstake cooldown period in seconds.
/**
* @dev Returns the current unstake cooldown period in seconds.
* @return The unstake cooldown period in seconds.
*/
function unstakeCooldownPeriod() public view returns (uint256) {
return _getProtocolStakingStorage()._unstakeCooldownPeriod;
}
Expand Down