Skip to content

Commit

Permalink
Merge pull request #273 from axel-muller/I266-node-operator-configura…
Browse files Browse the repository at this point in the history
…tion

Node operator support implementation
  • Loading branch information
SurfingNerd authored Nov 21, 2024
2 parents 50d9342 + c742bd4 commit 0c86b33
Show file tree
Hide file tree
Showing 7 changed files with 730 additions and 132 deletions.
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 @@ contract StakingHbbft is Initializable, OwnableUpgradeable, ReentrancyGuardUpgra

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 @@ contract StakingHbbft is Initializable, OwnableUpgradeable, ReentrancyGuardUpgra
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 @@ contract StakingHbbft is Initializable, OwnableUpgradeable, ReentrancyGuardUpgra

// ============================================== 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 @@ contract StakingHbbft is Initializable, OwnableUpgradeable, ReentrancyGuardUpgra
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,15 +530,27 @@ contract StakingHbbft is Initializable, OwnableUpgradeable, ReentrancyGuardUpgra
/// 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]
- _setNodeOperator(stakingAddress,_nodeOperatorAddress,_operatorShare)
- _poolNodeOperatorLastChangeEpoch[_stakingAddress] = stakingEpoch
- _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)
- 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

Expand Down Expand Up @@ -547,6 +584,17 @@ contract StakingHbbft is Initializable, OwnableUpgradeable, ReentrancyGuardUpgra
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 @@ contract StakingHbbft is Initializable, OwnableUpgradeable, ReentrancyGuardUpgra

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 @@ contract StakingHbbft is Initializable, OwnableUpgradeable, ReentrancyGuardUpgra
}
}

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 @@ contract StakingHbbft is Initializable, OwnableUpgradeable, ReentrancyGuardUpgra
}
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

0 comments on commit 0c86b33

Please sign in to comment.