Skip to content
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

Node operator support implementation #273

Merged
merged 2 commits into from
Nov 21, 2024
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
2 changes: 1 addition & 1 deletion .solhint.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
"code-complexity": ["warn", 25],
"function-max-lines": ["warn", 160],
"func-visibility": ["warn", { "ignoreConstructors": true }],
"max-states-count": ["warn", 35]
"max-states-count": ["warn", 37]
}
}
152 changes: 129 additions & 23 deletions contracts/StakingHbbft.sol
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,23 @@

IBonusScoreSystem public bonusScoreContract;

/// @dev Address of node operator for specified pool.
mapping(address => address) public poolNodeOperator;

/// @dev Node operator share percent of total pool rewards.
mapping(address => uint256) public poolNodeOperatorShare;

/// @dev The epoch number in which the operator's address can be changed.
mapping(address => uint256) internal _poolNodeOperatorLastChangeEpoch;

// ============================================== Constants =======================================================

/// @dev The max number of candidates (including validators). This limit was determined through stress testing.
uint256 public constant MAX_CANDIDATES = 3000;

uint256 public constant MAX_NODE_OPERATOR_SHARE_PERCENT = 2000;
uint256 public constant PERCENT_DENOMINATOR = 10000;

// ================================================ Events ========================================================

/// @dev Emitted by the `claimOrderedWithdraw` function to signal the staker withdrew the specified
Expand Down Expand Up @@ -232,6 +244,16 @@
uint256 delegatorsReward
);

/// @dev Emitted by the `_setNodeOperator` function.
/// @param poolStakingAddress The pool for which node operator was configured.
/// @param nodeOperatorAddress Address of node operator address related to `poolStakingAddress`.
/// @param operatorShare Node operator share percent.
event SetNodeOperator(
address indexed poolStakingAddress,
address indexed nodeOperatorAddress,
uint256 operatorShare
);

/**
* @dev Emitted when the minimum stake for a delegator is updated.
* @param minStake The new minimum stake value.
Expand All @@ -246,6 +268,7 @@

// ============================================== Errors =======================================================
error CannotClaimWithdrawOrderYet(address pool, address staker);
error OnlyOncePerEpoch(uint256 _epoch);
error MaxPoolsCountExceeded();
error MaxAllowedWithdrawExceeded(uint256 allowed, uint256 desired);
error NoStakesToRecover();
Expand All @@ -268,6 +291,8 @@
error InvalidStakingFixedEpochDuration();
error InvalidTransitionTimeFrame();
error InvalidWithdrawAmount(address pool, address delegator, uint256 amount);
error InvalidNodeOperatorConfiguration(address _operator, uint256 _share);
error InvalidNodeOperatorShare(uint256 _share);
error WithdrawNotAllowed();
error ZeroWidthrawAmount();
error ZeroWidthrawDisallowPeriod();
Expand Down Expand Up @@ -505,18 +530,30 @@
/// they want to create a pool. This is a wrapper for the `stake` function.
/// @param _miningAddress The mining address of the candidate. The mining address is bound to the staking address
/// (msg.sender). This address cannot be equal to `msg.sender`.
function addPool(address _miningAddress, bytes calldata _publicKey, bytes16 _ip) external payable gasPriceIsValid {
/// @param _nodeOperatorAddress Address of node operator, will receive `_operatorShare` of epoch rewards.
/// @param _operatorShare Percent of epoch rewards to send to `_nodeOperatorAddress`.
/// Integer value with 2 decimal places, e.g. 1% = 100, 10.25% = 1025.
function addPool(
address _miningAddress,
address _nodeOperatorAddress,
uint256 _operatorShare,
bytes calldata _publicKey,
bytes16 _ip
) external payable gasPriceIsValid {
address stakingAddress = msg.sender;
uint256 amount = msg.value;
validatorSetContract.setStakingAddress(_miningAddress, stakingAddress);
// The staking address and the staker are the same.
_stake(stakingAddress, stakingAddress, amount);
poolInfo[stakingAddress].publicKey = _publicKey;
poolInfo[stakingAddress].internetAddress = _ip;

_setNodeOperator(stakingAddress, _nodeOperatorAddress, _operatorShare);

_stake(stakingAddress, stakingAddress, amount);

emit PlacedStake(stakingAddress, stakingAddress, stakingEpoch, amount);
}

Check notice

Code scanning / Slither

Reentrancy vulnerabilities Low

Reentrancy in StakingHbbft.addPool(address,address,uint256,bytes,bytes16):
External calls:
- validatorSetContract.setStakingAddress(_miningAddress,stakingAddress)
State variables written after the call(s):
- _stake(stakingAddress,stakingAddress,amount)
- _delegatorStakeSnapshot[_stakingAddress][_delegator][stakingEpoch] = stakeAmount[_stakingAddress][_delegator]
- _stake(stakingAddress,stakingAddress,amount)
- _poolsLikelihood.push(0)
- _poolsLikelihood[index] = newValue
- _stake(stakingAddress,stakingAddress,amount)
- _poolsLikelihoodSum = _poolsLikelihoodSum - oldValue + newValue
- _stake(stakingAddress,stakingAddress,amount)
- _poolsToBeElected.push(_stakingAddress)
- _stake(stakingAddress,stakingAddress,amount)
- _stakeAmountByEpoch[_poolStakingAddress][_staker][stakingEpoch] += _amount
- _stake(stakingAddress,stakingAddress,amount)
- _stakeSnapshotLastEpoch[_stakingAddress][_delegator] = stakingEpoch
- poolInfo[stakingAddress].publicKey = _publicKey
- poolInfo[stakingAddress].internetAddress = _ip
- _setNodeOperator(stakingAddress,_nodeOperatorAddress,_operatorShare)
- poolNodeOperator[_stakingAddress] = _operatorAddress
- _setNodeOperator(stakingAddress,_nodeOperatorAddress,_operatorShare)
- poolNodeOperatorLastChangeEpoch[_stakingAddress] = stakingEpoch
- _setNodeOperator(stakingAddress,_nodeOperatorAddress,_operatorShare)
- poolNodeOperatorShare[_stakingAddress] = _operatorSharePercent
- _stake(stakingAddress,stakingAddress,amount)
- poolToBeElectedIndex[_stakingAddress] = length
- _stake(stakingAddress,stakingAddress,amount)
- stakeAmount[_poolStakingAddress][_staker] = newStakeAmount
- _stake(stakingAddress,stakingAddress,amount)
- stakeAmountTotal[_poolStakingAddress] += _amount
- _stake(stakingAddress,stakingAddress,amount)
- totalStakedAmount += _amount
/// @dev Removes the candidate's or validator's pool from the `pools` array (a list of active pools which
/// can be retrieved by the `getPools` getter). When a candidate or validator wants to remove their pool,
/// they should call this function from their staking address.
Expand Down Expand Up @@ -547,6 +584,17 @@
poolInfo[msg.sender].port = _port;
}

/// @dev Set's the pool node operator configuration for a specific ethereum address.
/// @param _operatorAddress Node operator address.
/// @param _operatorShare Node operator reward share percent.
function setNodeOperator(address _operatorAddress, uint256 _operatorShare) external {
if (validatorSetContract.miningByStakingAddress(msg.sender) == address(0)) {
revert PoolNotExist(msg.sender);
}

_setNodeOperator(msg.sender, _operatorAddress, _operatorShare);
}

/// @dev Removes a specified pool from the `pools` array (a list of active pools which can be retrieved by the
/// `getPools` getter). Called by the `ValidatorSetHbbft._removeMaliciousValidator` internal function,
/// and the `ValidatorSetHbbft.handleFailedKeyGeneration` function
Expand Down Expand Up @@ -623,38 +671,35 @@

uint256 poolReward = msg.value;
uint256 totalStake = snapshotPoolTotalStakeAmount[stakingEpoch][_poolStakingAddress];
uint256 validatorStake = snapshotPoolValidatorStakeAmount[stakingEpoch][_poolStakingAddress];

uint256 validatorReward = 0;
PoolRewardShares memory shares = _splitPoolReward(_poolStakingAddress, poolReward, _validatorMinRewardPercent);

if (totalStake > validatorStake) {
address[] memory delegators = poolDelegators(_poolStakingAddress);
address[] memory delegators = poolDelegators(_poolStakingAddress);
for (uint256 i = 0; i < delegators.length; ++i) {
uint256 delegatorReward = (shares.delegatorsShare *
_getDelegatorStake(stakingEpoch, _poolStakingAddress, delegators[i])) / totalStake;

uint256 validatorFixedReward = (poolReward * _validatorMinRewardPercent) / 100;
uint256 rewardsToDisribute = poolReward - validatorFixedReward;

validatorReward = validatorFixedReward + (rewardsToDisribute * validatorStake) / totalStake;

for (uint256 i = 0; i < delegators.length; ++i) {
uint256 delegatorReward = (rewardsToDisribute *
_getDelegatorStake(stakingEpoch, _poolStakingAddress, delegators[i])) / totalStake;
stakeAmount[_poolStakingAddress][delegators[i]] += delegatorReward;
_stakeAmountByEpoch[_poolStakingAddress][delegators[i]][stakingEpoch] += delegatorReward;
}

stakeAmount[_poolStakingAddress][delegators[i]] += delegatorReward;
_stakeAmountByEpoch[_poolStakingAddress][delegators[i]][stakingEpoch] += delegatorReward;
}
} else {
// Whole pool stake belongs to the pool owner
// and he received all the rewards.
validatorReward = poolReward;
if (shares.nodeOperatorShare != 0) {
_rewardNodeOperator(_poolStakingAddress, shares.nodeOperatorShare);
}

stakeAmount[_poolStakingAddress][_poolStakingAddress] += validatorReward;
stakeAmount[_poolStakingAddress][_poolStakingAddress] += shares.validatorShare;

stakeAmountTotal[_poolStakingAddress] += poolReward;
totalStakedAmount += poolReward;

_setLikelihood(_poolStakingAddress);

emit RestakeReward(_poolStakingAddress, stakingEpoch, validatorReward, poolReward - validatorReward);
emit RestakeReward(
_poolStakingAddress,
stakingEpoch,
shares.validatorShare,
poolReward - shares.validatorShare
);
}

/// @dev Orders coins withdrawal from the staking address of the specified pool to the
Expand Down Expand Up @@ -1433,6 +1478,43 @@
}
}

function _setNodeOperator(
address _stakingAddress,
address _operatorAddress,
uint256 _operatorSharePercent
) private {
if (_operatorSharePercent > MAX_NODE_OPERATOR_SHARE_PERCENT) {
revert InvalidNodeOperatorShare(_operatorSharePercent);
}

if (_operatorAddress == address(0) && _operatorSharePercent != 0) {
revert InvalidNodeOperatorConfiguration(_operatorAddress, _operatorSharePercent);
}

uint256 lastChangeEpoch = _poolNodeOperatorLastChangeEpoch[_stakingAddress];
if (lastChangeEpoch != 0 && lastChangeEpoch == stakingEpoch) {
revert OnlyOncePerEpoch(stakingEpoch);
}

poolNodeOperator[_stakingAddress] = _operatorAddress;
poolNodeOperatorShare[_stakingAddress] = _operatorSharePercent;

_poolNodeOperatorLastChangeEpoch[_stakingAddress] = stakingEpoch;

emit SetNodeOperator(_stakingAddress, _operatorAddress, _operatorSharePercent);
}

function _rewardNodeOperator(address _stakingAddress, uint256 _operatorShare) private {
address nodeOperator = poolNodeOperator[_stakingAddress];

if (!_poolDelegators[_stakingAddress].contains(nodeOperator)) {
_addPoolDelegator(_stakingAddress, nodeOperator);
}

stakeAmount[_stakingAddress][nodeOperator] += _operatorShare;
_stakeAmountByEpoch[_stakingAddress][nodeOperator][stakingEpoch] += _operatorShare;
}

function _getDelegatorStake(
uint256 _stakingEpoch,
address _stakingAddress,
Expand Down Expand Up @@ -1469,6 +1551,30 @@
}
return (false, 0);
}

function _splitPoolReward(
address _poolAddress,
uint256 _poolReward,
uint256 _validatorMinRewardPercent
) private view returns (PoolRewardShares memory shares) {
uint256 totalStake = snapshotPoolTotalStakeAmount[stakingEpoch][_poolAddress];
uint256 validatorStake = snapshotPoolValidatorStakeAmount[stakingEpoch][_poolAddress];

uint256 validatorFixedReward = (_poolReward * _validatorMinRewardPercent) / 100;

shares.delegatorsShare = _poolReward - validatorFixedReward;

uint256 operatorSharePercent = poolNodeOperatorShare[_poolAddress];
if (poolNodeOperator[_poolAddress] != address(0) && operatorSharePercent != 0) {
shares.nodeOperatorShare = (_poolReward * operatorSharePercent) / PERCENT_DENOMINATOR;
}

shares.validatorShare =
validatorFixedReward -
shares.nodeOperatorShare +
(shares.delegatorsShare * validatorStake) /
totalStake;
}
}

// slither-disable-end unused-return
6 changes: 6 additions & 0 deletions contracts/interfaces/IStakingHbbft.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
pragma solidity =0.8.25;

interface IStakingHbbft {
struct PoolRewardShares {
uint256 validatorShare;
uint256 nodeOperatorShare;
uint256 delegatorsShare;
}

struct StakingParams {
address _validatorSetContract;
address _bonusScoreContract;
Expand Down
2 changes: 2 additions & 0 deletions test/BlockRewardHbbft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1301,6 +1301,8 @@ describe('BlockRewardHbbft', () => {

await stakingHbbft.connect(stakingAddress).addPool(
miningAddress.address,
ethers.ZeroAddress,
0n,
ethers.zeroPadBytes("0x00", 64),
ethers.zeroPadBytes("0x00", 16),
{ value: MIN_STAKE }
Expand Down
4 changes: 4 additions & 0 deletions test/KeyGenHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,8 @@ describe('KeyGenHistory', () => {

await stakingHbbft.connect(await ethers.getSigner(newPoolStakingAddress)).addPool(
newPoolMiningAddress,
ethers.ZeroAddress,
0n,
ethers.zeroPadBytes("0x00", 64),
ethers.zeroPadBytes("0x00", 16),
{ value: candidateMinStake }
Expand Down Expand Up @@ -724,6 +726,8 @@ describe('KeyGenHistory', () => {

await stakingHbbft.connect(await ethers.getSigner(poolStakingAddress2)).addPool(
poolMiningAddress2,
ethers.ZeroAddress,
0n,
ethers.zeroPadBytes("0x00", 64),
ethers.zeroPadBytes("0x00", 16),
{ value: candidateMinStake }
Expand Down
Loading
Loading