-
Notifications
You must be signed in to change notification settings - Fork 46
add new staking #192
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: add-version-reporting
Are you sure you want to change the base?
add new staking #192
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,7 +17,8 @@ contract ProxyStorage is EternalStorage { | |
| Consensus, | ||
| BlockReward, | ||
| ProxyStorage, | ||
| Voting | ||
| Voting, | ||
| Staking | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -101,6 +102,8 @@ contract ProxyStorage is EternalStorage { | |
| success = EternalStorageProxy(payable(address(this))).upgradeTo(_contractAddress); | ||
| } else if (_contractType == uint256(ContractTypes.Voting)) { | ||
| success = EternalStorageProxy(payable(getVoting())).upgradeTo(_contractAddress); | ||
| } else if (_contractType == uint256(ContractTypes.Staking)) { | ||
| success = EternalStorageProxy(payable(getStaking())).upgradeTo(_contractAddress); | ||
| } | ||
|
|
||
| if (success) { | ||
|
|
@@ -118,13 +121,15 @@ contract ProxyStorage is EternalStorage { | |
| _contractType == uint256(ContractTypes.Consensus) || | ||
| _contractType == uint256(ContractTypes.BlockReward) || | ||
| _contractType == uint256(ContractTypes.ProxyStorage) || | ||
| _contractType == uint256(ContractTypes.Voting); | ||
| _contractType == uint256(ContractTypes.Voting) || | ||
| _contractType == uint256(ContractTypes.Staking); | ||
| } | ||
|
|
||
| bytes32 internal constant OWNER = keccak256(abi.encodePacked("owner")); | ||
| bytes32 internal constant CONSENSUS = keccak256(abi.encodePacked("consensus")); | ||
| bytes32 internal constant BLOCK_REWARD = keccak256(abi.encodePacked("blockReward")); | ||
| bytes32 internal constant VOTING = keccak256(abi.encodePacked("voting")); | ||
| bytes32 internal constant STAKING = keccak256(abi.encodePacked("staking")); | ||
| bytes32 internal constant PROXY_STORAGE_ADDRESSES_INITIALIZED = keccak256(abi.encodePacked("proxyStorageAddressesInitialized")); | ||
|
|
||
| function _setConsensus(address _consensus) private { | ||
|
|
@@ -142,4 +147,8 @@ contract ProxyStorage is EternalStorage { | |
| function getVoting() public view returns(address){ | ||
| return addressStorage[VOTING]; | ||
| } | ||
|
|
||
| function getStaking() public view returns(address){ | ||
| return addressStorage[STAKING]; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see anything wrtiting too addressStorage[STAKING]. There's no _setStaking, |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,319 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity ^0.8.0; | ||
|
|
||
| import "@openzeppelin/contracts/utils/math/Math.sol"; | ||
| import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; | ||
|
|
||
|
|
||
| /// @title StakingRewards | ||
| /// @notice Records validator/user reward weights per cycle and allows users to claim their proportional ETH rewards. | ||
| /// @dev The block reward contract records weights. The owner/operator funds and finalizes each cycle reward. | ||
| contract StakingRewards is ReentrancyGuard { | ||
| using Math for uint256; | ||
|
|
||
| /// @notice Address of the trusted block reward contract. | ||
| /// @dev Only this address can call `recordBlockReward`. | ||
| address public immutable blockReward; | ||
|
|
||
| /// @notice Contract owner. | ||
| address public owner; | ||
|
|
||
| /// @notice Optional operator allowed to finalize cycle rewards. | ||
| address public operator; | ||
|
|
||
| /// @notice Whether reward recording is paused. | ||
| /// @dev When paused, `recordBlockReward` calls will be ignored. | ||
| bool public paused; | ||
|
|
||
| /// @notice User reward weight per cycle. | ||
| /// @dev cycle => user => weight. | ||
| mapping(uint256 => mapping(address => uint256)) public userCycleWeights; | ||
|
|
||
| /// @notice Total reward weight recorded for each cycle. | ||
| /// @dev Used as the denominator when calculating proportional rewards. | ||
| mapping(uint256 => uint256) public totalCycleWeights; | ||
|
|
||
| /// @notice ETH reward amount assigned to each cycle. | ||
| /// @dev cycle => reward amount in wei. | ||
| mapping(uint256 => uint256) public cycleRewards; | ||
|
|
||
| /// @notice Whether a cycle reward has been finalized. | ||
| /// @dev Users can only claim rewards for finalized cycles. | ||
| mapping(uint256 => bool) public cycleFinalized; | ||
|
|
||
| error NotBlockReward(); | ||
| error NotOperatorOrOwner(); | ||
| error NotOwner(); | ||
| error ZeroAddress(); | ||
| error ZeroReward(); | ||
| error CycleAlreadyFinalized(); | ||
| error CycleNotFinalized(); | ||
| error NoRewardsToClaim(); | ||
| error NoTotalWeight(); | ||
| error RewardTransferFailed(); | ||
| error ContractPaused(); | ||
|
|
||
| /// @notice Emitted when reward weights are recorded for a cycle. | ||
| /// @param cycle The cycle being recorded. | ||
| /// @param totalWeightAdded The total valid weight added during this call. | ||
| /// @param receiversLength Number of receivers passed in. | ||
| /// @param rewardWeightsLength Number of reward weights passed in. | ||
| event BlockRewardRecorded( | ||
| uint256 indexed cycle, | ||
| uint256 totalWeightAdded, | ||
| uint256 receiversLength, | ||
| uint256 rewardWeightsLength | ||
| ); | ||
|
|
||
| /// @notice Emitted when a cycle reward is funded and finalized. | ||
| /// @param cycle The finalized cycle. | ||
| /// @param rewardAmount The ETH reward amount assigned to the cycle. | ||
| event CycleRewardFinalized( | ||
| uint256 indexed cycle, | ||
| uint256 rewardAmount | ||
| ); | ||
|
|
||
| /// @notice Emitted when a user claims a reward. | ||
| /// @param user The user claiming the reward. | ||
| /// @param cycle The cycle being claimed. | ||
| /// @param rewardAmount The ETH amount claimed. | ||
| /// @param userWeight The user's recorded weight for the cycle. | ||
| event RewardClaimed( | ||
| address indexed user, | ||
| uint256 indexed cycle, | ||
| uint256 rewardAmount, | ||
| uint256 userWeight | ||
| ); | ||
|
|
||
| /// @notice Emitted when the operator is updated. | ||
| /// @param oldOperator Previous operator address. | ||
| /// @param newOperator New operator address. | ||
| event OperatorUpdated(address indexed oldOperator, address indexed newOperator); | ||
|
|
||
| /// @notice Emitted when ownership is transferred. | ||
| /// @param oldOwner Previous owner address. | ||
| /// @param newOwner New owner address. | ||
| event OwnershipTransferred(address indexed oldOwner, address indexed newOwner); | ||
|
|
||
| /// @notice Emitted when the paused state is updated. | ||
| /// @param paused The new paused state. | ||
| event Paused(bool paused); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You might want to rename this collides with OpenZepllin convention, may break indexers/ external tooling. |
||
|
|
||
| /// @dev Restricts access to the block reward contract. | ||
| modifier onlyBlockReward() { | ||
| if (msg.sender != blockReward) revert NotBlockReward(); | ||
| _; | ||
| } | ||
|
|
||
| /// @dev Restricts access to the owner or operator. | ||
| modifier onlyOperatorOrOwner() { | ||
| if (msg.sender != owner && msg.sender != operator) revert NotOperatorOrOwner(); | ||
| _; | ||
| } | ||
|
|
||
| /// @dev Restricts access to the owner. | ||
| modifier onlyOwner() { | ||
| if (msg.sender != owner) revert NotOwner(); | ||
| _; | ||
| } | ||
|
|
||
| /// @dev Restricts access when the contract is paused. | ||
| modifier whenNotPaused() { | ||
| if (paused) revert ContractPaused(); | ||
| _; | ||
| } | ||
|
|
||
| /// @notice Deploys the staking rewards contract. | ||
| /// @param _blockReward Address of the trusted block reward contract. | ||
| /// @param _operator Address of the initial operator. | ||
| constructor(address _blockReward, address _operator) { | ||
| if (_blockReward == address(0)) revert ZeroAddress(); | ||
| if (_operator == address(0)) revert ZeroAddress(); | ||
|
|
||
| blockReward = _blockReward; | ||
| owner = msg.sender; | ||
| operator = _operator; | ||
| } | ||
|
|
||
| /// @notice Updates the operator address. | ||
| /// @dev Can only be called by the owner. | ||
| /// @param newOperator New operator address. | ||
| function setOperator(address newOperator) external onlyOwner { | ||
| if (newOperator == address(0)) revert ZeroAddress(); | ||
|
|
||
| address oldOperator = operator; | ||
| operator = newOperator; | ||
|
|
||
| emit OperatorUpdated(oldOperator, newOperator); | ||
| } | ||
|
|
||
| /// @notice Transfers ownership to a new owner. | ||
| /// @dev Can only be called by the current owner. | ||
| /// @param newOwner New owner address. | ||
| function transferOwnership(address newOwner) external onlyOwner { | ||
| if (newOwner == address(0)) revert ZeroAddress(); | ||
|
|
||
| address oldOwner = owner; | ||
| owner = newOwner; | ||
|
|
||
| emit OwnershipTransferred(oldOwner, newOwner); | ||
| } | ||
|
|
||
| /// @notice Pauses reward recording. | ||
| /// @dev When paused, `recordBlockReward` calls will be ignored. | ||
| function setPaused(bool newPauseValue) external onlyOwner { | ||
| paused = newPauseValue; | ||
|
|
||
| emit Paused(newPauseValue); | ||
| } | ||
|
|
||
| /// @notice Records user reward weights for a cycle. | ||
| /// @dev | ||
| /// Does not revert if `receivers.length != rewardWeights.length`. | ||
| /// Instead, it processes only the smaller length. | ||
| /// | ||
| /// Invalid entries are skipped: | ||
| /// - receiver is address(0) | ||
| /// - weight is 0 | ||
| /// | ||
| /// This protects the system from full-call reverts caused by bad individual entries. | ||
| /// | ||
| /// @param cycle The cycle being recorded. | ||
| /// @param receivers List of reward receivers. | ||
| /// @param rewardWeights List of corresponding reward weights. | ||
| function recordBlockReward( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be worth looking to limit number of delegates currently getDelegatorsForRewardDistribution is unlimited and this call will add state growth per block per delegate (might be a potential attack vector) |
||
| uint256 cycle, | ||
| address[] calldata receivers, | ||
| uint256[] calldata rewardWeights | ||
| ) external onlyBlockReward { | ||
| if (cycleFinalized[cycle]) return; | ||
|
|
||
| if (paused) return; | ||
|
|
||
| uint256 receiversLength = receivers.length; | ||
| uint256 weightLength = rewardWeights.length; | ||
|
|
||
| uint256 length = receiversLength < weightLength | ||
| ? receiversLength | ||
| : weightLength; | ||
|
|
||
| uint256 totalWeightAdded; | ||
|
|
||
| for (uint256 i; i < length; ) { | ||
| address receiver = receivers[i]; | ||
| uint256 weight = rewardWeights[i]; | ||
|
|
||
| if (receiver != address(0) && weight != 0) { | ||
| userCycleWeights[cycle][receiver] += weight; | ||
| totalWeightAdded += weight; | ||
| } | ||
|
|
||
| unchecked { | ||
| ++i; | ||
| } | ||
| } | ||
|
|
||
| if (totalWeightAdded != 0) { | ||
| totalCycleWeights[cycle] += totalWeightAdded; | ||
| } | ||
|
|
||
| emit BlockRewardRecorded( | ||
| cycle, | ||
| totalWeightAdded, | ||
| receiversLength, | ||
| weightLength | ||
| ); | ||
| } | ||
|
|
||
| /// @notice Funds and finalizes a cycle reward. | ||
| /// @dev | ||
| /// Once finalized, no more weights can be added for that cycle. | ||
| /// Users can only claim after the cycle has been finalized. | ||
| /// | ||
| /// This prevents users from claiming before the final reward amount is known. | ||
| /// | ||
| /// @param cycle The cycle to finalize. | ||
| function finalizeCycleReward(uint256 cycle) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. might be worth gating this with a require to ensure the cycle is over require(cycle != IConsensus(...).getCurrentCycleStartBlock()) |
||
| external | ||
| payable | ||
| onlyOperatorOrOwner | ||
| whenNotPaused | ||
| { | ||
| if (msg.value == 0) revert ZeroReward(); | ||
| if (cycleFinalized[cycle]) revert CycleAlreadyFinalized(); | ||
| if (totalCycleWeights[cycle] == 0) revert NoTotalWeight(); | ||
|
|
||
| cycleRewards[cycle] = msg.value; | ||
| cycleFinalized[cycle] = true; | ||
|
|
||
| emit CycleRewardFinalized(cycle, msg.value); | ||
| } | ||
|
|
||
| /// @notice Claims rewards for multiple finalized cycles. | ||
| /// @dev Reverts if any cycle in the list is not claimable by the caller. | ||
| /// @param cycles List of cycle IDs to claim. | ||
| function claimRewards(uint256[] calldata cycles) external whenNotPaused nonReentrant { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should pausing allow withdraws still? i.m.o pausing should just gate recording and finalization only |
||
| uint256 length = cycles.length; | ||
|
|
||
| for (uint256 i; i < length; ) { | ||
| _claimReward(cycles[i]); | ||
|
|
||
| unchecked { | ||
| ++i; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// @notice Claims reward for one finalized cycle. | ||
| /// @param cycle The cycle to claim. | ||
| function claimReward(uint256 cycle) external whenNotPaused nonReentrant { | ||
| _claimReward(cycle); | ||
| } | ||
|
|
||
| /// @notice Returns a user's pending reward for a cycle. | ||
| /// @dev Returns 0 if the cycle is not finalized, user has no weight, or total weight is 0. | ||
| /// @param user The user address. | ||
| /// @param cycle The cycle ID. | ||
| /// @return rewardAmount The pending ETH reward amount in wei. | ||
| function pendingReward(address user, uint256 cycle) | ||
| external | ||
| view | ||
| returns (uint256 rewardAmount) | ||
| { | ||
| if (!cycleFinalized[cycle]) return 0; | ||
|
|
||
| uint256 userWeight = userCycleWeights[cycle][user]; | ||
| if (userWeight == 0) return 0; | ||
|
|
||
| uint256 totalWeight = totalCycleWeights[cycle]; | ||
| if (totalWeight == 0) return 0; | ||
|
|
||
| rewardAmount = Math.mulDiv(cycleRewards[cycle], userWeight, totalWeight); | ||
| } | ||
|
|
||
| /// @notice Internal reward claim logic. | ||
| /// @dev | ||
| /// Clears the user's cycle weight before transferring ETH to prevent re-claiming. | ||
| /// Uses `call` instead of `transfer` to avoid the 2300 gas stipend limitation. | ||
| /// | ||
| /// @param cycle The cycle to claim. | ||
| function _claimReward(uint256 cycle) internal { | ||
| if (!cycleFinalized[cycle]) revert CycleNotFinalized(); | ||
|
|
||
| uint256 userWeight = userCycleWeights[cycle][msg.sender]; | ||
| if (userWeight == 0) revert NoRewardsToClaim(); | ||
|
|
||
| uint256 totalWeight = totalCycleWeights[cycle]; | ||
| if (totalWeight == 0) revert NoTotalWeight(); | ||
|
|
||
| uint256 rewardAmount = Math.mulDiv(cycleRewards[cycle], userWeight, totalWeight); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nothing major but this will accumulate dust from rounding per cycle so some rewards will be lost over time. Just something to be aware of |
||
| if (rewardAmount == 0) revert NoRewardsToClaim(); | ||
|
|
||
| userCycleWeights[cycle][msg.sender] = 0; | ||
|
|
||
| (bool success, ) = payable(msg.sender).call{value: rewardAmount}(""); | ||
| if (!success) revert RewardTransferFailed(); | ||
|
|
||
| emit RewardClaimed(msg.sender, cycle, rewardAmount, userWeight); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this may need wrapping in a try, if this reverts every node will fail to process the block and we risk a chain halt