diff --git a/.solhint.json b/.solhint.json index b5103bd4..ac0a038e 100644 --- a/.solhint.json +++ b/.solhint.json @@ -5,6 +5,6 @@ "code-complexity": ["warn", 25], "function-max-lines": ["warn", 160], "func-visibility": ["warn", { "ignoreConstructors": true }], - "max-states-count": ["warn", 32] + "max-states-count": ["warn", 35] } } diff --git a/contracts/BlockRewardHbbft.sol b/contracts/BlockRewardHbbft.sol index 7c571475..35ec78fe 100644 --- a/contracts/BlockRewardHbbft.sol +++ b/contracts/BlockRewardHbbft.sol @@ -9,6 +9,7 @@ import { IBlockRewardHbbft } from "./interfaces/IBlockRewardHbbft.sol"; import { IStakingHbbft } from "./interfaces/IStakingHbbft.sol"; import { IValidatorSetHbbft } from "./interfaces/IValidatorSetHbbft.sol"; import { IGovernancePot } from "./interfaces/IGovernancePot.sol"; +import { IConnectivityTrackerHbbft } from "./interfaces/IConnectivityTrackerHbbft.sol"; import { SYSTEM_ADDRESS } from "./lib/Constants.sol"; import { Unauthorized, ValidatorsListEmpty, ZeroAddress } from "./lib/Errors.sol"; import { TransferUtils } from "./utils/TransferUtils.sol"; @@ -75,7 +76,7 @@ contract BlockRewardHbbft is uint256 public governancePotShareDenominator; /// @dev the address of the `ConnectivityTrackerHbbft` contract. - address public connectivityTracker; + IConnectivityTrackerHbbft public connectivityTracker; /// @dev flag indicating whether it is needed to end current epoch earlier. bool public earlyEpochEnd; @@ -115,7 +116,7 @@ contract BlockRewardHbbft is /// @dev Ensures the caller is the ConnectivityTracker contract address. modifier onlyConnectivityTracker() { - if (msg.sender != connectivityTracker) { + if (msg.sender != address(connectivityTracker)) { revert Unauthorized(); } _; @@ -151,7 +152,7 @@ contract BlockRewardHbbft is __ReentrancyGuard_init(); validatorSetContract = IValidatorSetHbbft(_validatorSet); - connectivityTracker = _connectivityTracker; + connectivityTracker = IConnectivityTrackerHbbft(_connectivityTracker); validatorMinRewardPercent[0] = VALIDATOR_MIN_REWARD_PERCENT; @@ -160,6 +161,17 @@ contract BlockRewardHbbft is governancePotAddress = payable(0xDA0da0da0Da0Da0Da0DA00DA0da0da0DA0DA0dA0); governancePotShareNominator = 1; governancePotShareDenominator = 10; + + uint256[] memory governancePotShareNominatorParams = new uint256[](11); + for (uint256 i = 0; i < governancePotShareNominatorParams.length; i++) { + governancePotShareNominatorParams[i] = 10 + i; + } + + initAllowedChangeableParameter( + "setGovernancePotShareNominator(uint256)", + "governancePotShareNominatorParams()", + governancePotShareNominatorParams + ); } /// @dev adds the transfered value to the delta pot. @@ -216,7 +228,7 @@ contract BlockRewardHbbft is revert ZeroAddress(); } - connectivityTracker = _connectivityTracker; + connectivityTracker = IConnectivityTrackerHbbft(_connectivityTracker); emit SetConnectivityTracker(_connectivityTracker); } @@ -383,6 +395,8 @@ contract BlockRewardHbbft is function _closeEpoch(IStakingHbbft stakingContract) private returns (uint256) { uint256 stakingEpoch = stakingContract.stakingEpoch(); + connectivityTracker.penaliseFaultyValidators(stakingEpoch); + uint256 nativeTotalRewardAmount = 0; // Distribute rewards among validator pools if (stakingEpoch != 0) { @@ -448,7 +462,7 @@ contract BlockRewardHbbft is // those calls are able to fail. // more details: https://github.com/DMDcoin/diamond-contracts-core/issues/231 IGovernancePot governancePot = IGovernancePot(governancePotAddress); - + // solhint-disable no-empty-blocks try governancePot.switchPhase() { // all good, we just wanted to catch. @@ -472,10 +486,6 @@ contract BlockRewardHbbft is uint256 numRewardedValidators = 0; for (uint256 i = 0; i < validators.length; ++i) { - if (validatorSetContract.isValidatorBanned(validators[i])) { - continue; - } - uint256 validatorStakeAmount = stakingContract.getPoolValidatorStakeAmount( stakingEpoch, validatorSetContract.stakingByMiningAddress(validators[i]) diff --git a/contracts/BonusScoreSystem.sol b/contracts/BonusScoreSystem.sol new file mode 100644 index 00000000..8822ee2e --- /dev/null +++ b/contracts/BonusScoreSystem.sol @@ -0,0 +1,274 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity =0.8.25; + +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; + +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; + +import { ScoringFactor, IBonusScoreSystem } from "./interfaces/IBonusScoreSystem.sol"; +import { IStakingHbbft } from "./interfaces/IStakingHbbft.sol"; +import { Unauthorized, ZeroAddress } from "./lib/Errors.sol"; + +/// @dev Stores validators bonus score based on their behavior. +/// Validator with a higher bonus score has a higher likelihood to be elected. +contract BonusScoreSystem is Initializable, OwnableUpgradeable, ReentrancyGuardUpgradeable, IBonusScoreSystem { + uint256 public constant DEFAULT_STAND_BY_FACTOR = 15; + uint256 public constant DEFAULT_NO_STAND_BY_FACTOR = 15; + uint256 public constant DEFAULT_NO_KEY_WRITE_FACTOR = 100; + uint256 public constant DEFAULT_BAD_PERF_FACTOR = 100; + + uint256 public constant MIN_SCORE = 1; + uint256 public constant MAX_SCORE = 1000; + + IStakingHbbft public stakingHbbft; + address public validatorSetHbbft; + address public connectivityTracker; + + /// @dev Current bonus score factors bonus/penalty value + mapping(ScoringFactor => uint256) private _factors; + + /// @dev Validators mining address to current bonus score mapping + mapping(address => uint256) private _validatorScore; + + /// @dev Timestamp of validator stand by reward/penalty + mapping(address => uint256) private _standByScoreChangeTimestamp; + + /// @dev Emitted by the `_updateValidatorScore` function when validator's score changes for one + /// of the {ScoringFactor} reasons described in `factor` + /// @param miningAddress Validator's mining address. + /// @param factor Scoring factor type. + /// @param newScore New validator's bonus score value. + event ValidatorScoreChanged(address indexed miningAddress, ScoringFactor indexed factor, uint256 newScore); + + /// @dev Emitted by the `updateScoringFactor` function when bonus/penalty for specified + /// scoring factor changed by contract owner (DAO contract). + /// @param factor Scoring factor type. + /// @param value New scoring factor bonus/penalty value. + event UpdateScoringFactor(ScoringFactor indexed factor, uint256 value); + + /// @dev Emitted by the `setStakingContract` function when StakingHbbft contract address changes + /// @param _staking New StakingHbbft contract address + event SetStakingContract(address indexed _staking); + + /// @dev Emitted by the `setValidatorSetContract` function when ValidatorSetHbbft contract address changes + /// @param _validatorSet New ValidatorSetHbbft contract address + event SetValidatorSetContract(address indexed _validatorSet); + + /// @dev Emitted by the `setConnectivityTrackerContract` function + /// when ConnectivityTrackerHbbft contract address changes + /// @param _connectivityTracker New ConnectivityTrackerHbbft contract address + event SetConnectivityTrackerContract(address indexed _connectivityTracker); + + error ZeroFactorValue(); + error InvalidScoringFactor(); + error InvalidIntervalStartTimestamp(); + + modifier onlyValidatorSet() { + if (msg.sender != validatorSetHbbft) { + revert Unauthorized(); + } + _; + } + + modifier onlyConnectivityTracker() { + if (msg.sender != connectivityTracker) { + revert Unauthorized(); + } + _; + } + + modifier validAddress(address _address) { + if (_address == address(0)) { + revert ZeroAddress(); + } + _; + } + + /// @dev Contract initializer. + /// @param _owner Contract owner address. + /// @param _validatorSetHbbft ValidatorSetHbbft contract address. + /// @param _connectivityTracker ConnectivityTrackerHbbft contract address. + /// @param _stakingHbbft StakingHbbft contract address. + function initialize( + address _owner, + address _validatorSetHbbft, + address _connectivityTracker, + address _stakingHbbft + ) external initializer { + if ( + _owner == address(0) || + _validatorSetHbbft == address(0) || + _connectivityTracker == address(0) || + _stakingHbbft == address(0) + ) { + revert ZeroAddress(); + } + + __Ownable_init(_owner); + __ReentrancyGuard_init(); + + validatorSetHbbft = _validatorSetHbbft; + connectivityTracker = _connectivityTracker; + stakingHbbft = IStakingHbbft(_stakingHbbft); + + _setInitialScoringFactors(); + } + + function setStakingContract(address _staking) external onlyOwner validAddress(_staking) { + stakingHbbft = IStakingHbbft(_staking); + + emit SetStakingContract(_staking); + } + + function setValidatorSetContract(address _validatorSet) external onlyOwner validAddress(_validatorSet) { + validatorSetHbbft = _validatorSet; + + emit SetValidatorSetContract(_validatorSet); + } + + function setConnectivityTrackerContract(address _address) external onlyOwner validAddress(_address) { + connectivityTracker = _address; + + emit SetConnectivityTrackerContract(_address); + } + + /// TODO: Define value guards. + function updateScoringFactor(ScoringFactor factor, uint256 value) external onlyOwner { + if (value == 0) { + revert ZeroFactorValue(); + } + + _factors[factor] = value; + + emit UpdateScoringFactor(factor, value); + } + + /// @dev Reward a validator who could not get into the current set, but was available. + /// @param mining Validator mining address + /// @param availableSince Timestamp from which the validator is available. + function rewardStandBy(address mining, uint256 availableSince) external onlyValidatorSet nonReentrant { + _updateScoreStandBy(mining, ScoringFactor.StandByBonus, availableSince); + } + + /// @dev Penalise validator marked as unavailable. + /// @param mining Validator mining address + /// @param unavailableSince Timestamp from which the validator is unavailable. + function penaliseNoStandBy(address mining, uint256 unavailableSince) external onlyValidatorSet nonReentrant { + _updateScoreStandBy(mining, ScoringFactor.NoStandByPenalty, unavailableSince); + } + + /// @dev Penalise validator for missed Part/ACK. + /// @param mining Validator mining address + function penaliseNoKeyWrite(address mining) external onlyValidatorSet nonReentrant { + // timeInterval argument in _updateValidatorScore function call here is irrelevant, + // because penalty score amount does not depend on time. + _updateValidatorScore(mining, ScoringFactor.NoKeyWritePenalty, 0); + } + + /// @dev Penalise validator for bad performance (lost connectivity). + /// Zero `time` value means full score decrease value (= DEFAULT_BAD_PERF_FACTOR) + /// @param mining Validator mining address + /// @param time Time interval from the moment when the validator was marked as faulty until full reconnect. + function penaliseBadPerformance(address mining, uint256 time) external onlyConnectivityTracker nonReentrant { + _updateValidatorScore(mining, ScoringFactor.BadPerformancePenalty, time); + } + + /// @dev Returns current bonus/penalty value for specified scoring factor `factor` + /// @param factor Type of scoring factor. + /// @return value Scoring factor value. + function getScoringFactorValue(ScoringFactor factor) public view returns (uint256) { + return _factors[factor]; + } + + /// @dev Returns time in seconds needed to accumulate single score point depending on scoring `factor` + /// @param factor Type of scroing factor. + /// @return value time interval in seconds. + function getTimePerScorePoint(ScoringFactor factor) public view returns (uint256) { + uint256 fixedEpochDuration = stakingHbbft.stakingFixedEpochDuration(); + + return fixedEpochDuration / getScoringFactorValue(factor); + } + + /// @dev Get current validator score. + /// @param mining Validator mining address. + /// @return value current validator score. + function getValidatorScore(address mining) public view returns (uint256) { + // will return current validator score or MIN_SCORE if score has not been recorded before. + return Math.max(_validatorScore[mining], MIN_SCORE); + } + + /// @dev Initialize default scoring factors bonus/penalty values. + function _setInitialScoringFactors() private { + _factors[ScoringFactor.StandByBonus] = DEFAULT_STAND_BY_FACTOR; + _factors[ScoringFactor.NoStandByPenalty] = DEFAULT_NO_STAND_BY_FACTOR; + _factors[ScoringFactor.NoKeyWritePenalty] = DEFAULT_NO_KEY_WRITE_FACTOR; + _factors[ScoringFactor.BadPerformancePenalty] = DEFAULT_BAD_PERF_FACTOR; + } + + function _updateScoreStandBy(address mining, ScoringFactor factor, uint256 availabilityTimestamp) private { + // Take the latest point in time, to calculate stand by interval. + // If _standByScoreChangeTimestamp > availabilityTimestamp means we have already given + // stand by bonus/penalty previously. + uint256 intervalStart = Math.max(_standByScoreChangeTimestamp[mining], availabilityTimestamp); + + if (intervalStart >= block.timestamp) { + revert InvalidIntervalStartTimestamp(); + } + + _updateValidatorScore(mining, factor, block.timestamp - intervalStart); + + _standByScoreChangeTimestamp[mining] = block.timestamp; + } + + /// @dev Update current validator score + /// @param mining Validator mining address + /// @param factor Type of scoring factor - reason to change validator score + /// @param timeInterval Interval of time used to calculate score change + /// Emits {ValidatorScoreChanged} event + function _updateValidatorScore(address mining, ScoringFactor factor, uint256 timeInterval) private { + bool isScoreIncrease = _isScoreIncrease(factor); + + uint256 scorePoints = _getAccumulatedScorePoints(factor, timeInterval); + uint256 currentScore = getValidatorScore(mining); + + uint256 newScore; + + if (isScoreIncrease) { + newScore = Math.min(MAX_SCORE, currentScore + scorePoints); + } else { + newScore = currentScore - Math.min(currentScore, scorePoints); + + // slither-disable-next-line incorrect-equality + if (newScore == 0) { + newScore = MIN_SCORE; + } + } + + _validatorScore[mining] = newScore; + + stakingHbbft.updatePoolLikelihood(mining, newScore); + + emit ValidatorScoreChanged(mining, factor, newScore); + } + + function _getAccumulatedScorePoints(ScoringFactor factor, uint256 timeInterval) private view returns (uint256) { + uint256 scoringFactorValue = getScoringFactorValue(factor); + + // slither-disable-start incorrect-equality + if (factor == ScoringFactor.NoKeyWritePenalty) { + return scoringFactorValue; + } else if (factor == ScoringFactor.BadPerformancePenalty && timeInterval == 0) { + return scoringFactorValue; + } else { + // Eliminate a risk to get more points than defined MAX for given `factor` + return Math.min(timeInterval / getTimePerScorePoint(factor), scoringFactorValue); + } + // slither-disable-end incorrect-equality + } + + function _isScoreIncrease(ScoringFactor factor) private pure returns (bool) { + return factor == ScoringFactor.StandByBonus; + } +} diff --git a/contracts/ConnectivityTrackerHbbft.sol b/contracts/ConnectivityTrackerHbbft.sol index 7c17effb..198d2926 100644 --- a/contracts/ConnectivityTrackerHbbft.sol +++ b/contracts/ConnectivityTrackerHbbft.sol @@ -10,6 +10,9 @@ import { IConnectivityTrackerHbbft } from "./interfaces/IConnectivityTrackerHbbf import { IValidatorSetHbbft } from "./interfaces/IValidatorSetHbbft.sol"; import { IStakingHbbft } from "./interfaces/IStakingHbbft.sol"; import { IBlockRewardHbbft } from "./interfaces/IBlockRewardHbbft.sol"; +import { IBonusScoreSystem } from "./interfaces/IBonusScoreSystem.sol"; + +import { Unauthorized, ZeroAddress } from "./lib/Errors.sol"; contract ConnectivityTrackerHbbft is Initializable, OwnableUpgradeable, IConnectivityTrackerHbbft { using EnumerableSet for EnumerableSet.AddressSet; @@ -26,6 +29,11 @@ contract ConnectivityTrackerHbbft is Initializable, OwnableUpgradeable, IConnect mapping(uint256 => EnumerableSet.AddressSet) private _flaggedValidators; mapping(uint256 => mapping(address => EnumerableSet.AddressSet)) private _reporters; + mapping(address => uint256) private _disconnectTimestamp; + mapping(uint256 => bool) private _epochPenaltiesSent; + + IBonusScoreSystem public bonusScoreContract; + event SetMinReportAgeBlocks(uint256 _minReportAge); event SetEarlyEpochEndToleranceLevel(uint256 _level); event ReportMissingConnectivity(address indexed reporter, address indexed validator, uint256 indexed blockNumber); @@ -35,11 +43,11 @@ contract ConnectivityTrackerHbbft is Initializable, OwnableUpgradeable, IConnect error AlreadyReported(address reporter, address validator); error CannotReportByFlaggedValidator(address reporter); - error InvalidAddress(); error InvalidBlock(); error OnlyValidator(); error ReportTooEarly(); error UnknownReconnectReporter(address reporter, address validator); + error EpochPenaltiesAlreadySent(uint256 epoch); /// @custom:oz-upgrades-unsafe-allow constructor constructor() { @@ -47,20 +55,29 @@ contract ConnectivityTrackerHbbft is Initializable, OwnableUpgradeable, IConnect _disableInitializers(); } + modifier onlyBlockRewardContract() { + if (msg.sender != address(blockRewardContract)) { + revert Unauthorized(); + } + _; + } + function initialize( address _contractOwner, address _validatorSetContract, address _stakingContract, address _blockRewardContract, + address _bonusScoreContract, uint256 _minReportAgeBlocks ) external initializer { if ( _contractOwner == address(0) || _validatorSetContract == address(0) || _stakingContract == address(0) || - _blockRewardContract == address(0) + _blockRewardContract == address(0) || + _bonusScoreContract == address(0) ) { - revert InvalidAddress(); + revert ZeroAddress(); } __Ownable_init(_contractOwner); @@ -68,6 +85,7 @@ contract ConnectivityTrackerHbbft is Initializable, OwnableUpgradeable, IConnect validatorSetContract = IValidatorSetHbbft(_validatorSetContract); stakingContract = IStakingHbbft(_stakingContract); blockRewardContract = IBlockRewardHbbft(_blockRewardContract); + bonusScoreContract = IBonusScoreSystem(_bonusScoreContract); minReportAgeBlocks = _minReportAgeBlocks; earlyEpochEndToleranceLevel = 2; @@ -100,6 +118,10 @@ contract ConnectivityTrackerHbbft is Initializable, OwnableUpgradeable, IConnect // slither-disable-next-line unused-return _reporters[epoch][validator].add(msg.sender); + if (isFaultyValidator(validator, epoch)) { + _disconnectTimestamp[validator] = block.timestamp; + } + _decideEarlyEpochEndNeeded(epoch); emit ReportMissingConnectivity(msg.sender, validator, blockNumber); @@ -111,13 +133,21 @@ contract ConnectivityTrackerHbbft is Initializable, OwnableUpgradeable, IConnect uint256 epoch = currentEpoch(); uint256 currentScore = getValidatorConnectivityScore(epoch, validator); - if (currentScore != 0) { + // slither-disable-next-line unused-return + _reporters[epoch][validator].remove(msg.sender); + + if (currentScore == 1) { // slither-disable-next-line unused-return - _reporters[epoch][validator].remove(msg.sender); + _flaggedValidators[epoch].remove(validator); + + // All reporters confirmed that this validator reconnected, + // decrease validator bonus score for bad performance based on disconnect time interval. + if (_disconnectTimestamp[validator] != 0) { + uint256 disconnectPeriod = block.timestamp - _disconnectTimestamp[validator]; - if (currentScore == 1) { - // slither-disable-next-line unused-return - _flaggedValidators[epoch].remove(validator); + bonusScoreContract.penaliseBadPerformance(validator, disconnectPeriod); + + delete _disconnectTimestamp[validator]; } } @@ -126,8 +156,22 @@ contract ConnectivityTrackerHbbft is Initializable, OwnableUpgradeable, IConnect emit ReportReconnect(msg.sender, validator, blockNumber); } - function getValidatorConnectivityScore(uint256 epoch, address validator) public view returns (uint256) { - return _reporters[epoch][validator].length(); + function penaliseFaultyValidators(uint256 epoch) external onlyBlockRewardContract { + if (_epochPenaltiesSent[epoch]) { + revert EpochPenaltiesAlreadySent(epoch); + } + + _epochPenaltiesSent[epoch] = true; + + address[] memory flaggedValidators = getFlaggedValidatorsByEpoch(epoch); + + for (uint256 i = 0; i < flaggedValidators.length; ++i) { + if (!isFaultyValidator(flaggedValidators[i], epoch)) { + continue; + } + + bonusScoreContract.penaliseBadPerformance(flaggedValidators[i], 0); + } } /// @dev Returns true if the specified validator was reported by the specified reporter at the given epoch. @@ -135,6 +179,14 @@ contract ConnectivityTrackerHbbft is Initializable, OwnableUpgradeable, IConnect return _reporters[currentEpoch()][validator].contains(reporter); } + function getValidatorConnectivityScore(uint256 epoch, address validator) public view returns (uint256) { + return _reporters[epoch][validator].length(); + } + + function isFaultyValidator(address validator, uint256 epoch) public view returns (bool) { + return getValidatorConnectivityScore(epoch, validator) >= _getReportersThreshold(epoch); + } + function checkReportMissingConnectivityCallable( address caller, address validator, @@ -166,8 +218,16 @@ contract ConnectivityTrackerHbbft is Initializable, OwnableUpgradeable, IConnect } } + function getFlaggedValidatorsByEpoch(uint256 epoch) public view returns (address[] memory) { + return _flaggedValidators[epoch].values(); + } + function getFlaggedValidators() public view returns (address[] memory) { - return _flaggedValidators[currentEpoch()].values(); + return getFlaggedValidatorsByEpoch(currentEpoch()); + } + + function getFlaggedValidatorsCount(uint256 epoch) public view returns (uint256) { + return _flaggedValidators[epoch].length(); } function currentEpoch() public view returns (uint256) { @@ -189,14 +249,39 @@ contract ConnectivityTrackerHbbft is Initializable, OwnableUpgradeable, IConnect return _countFaultyValidators(epoch); } - function _countFaultyValidators(uint256 epoch) private view returns (uint256) { - address[] memory flaggedValidators = getFlaggedValidators(); + function _decideEarlyEpochEndNeeded(uint256 epoch) private { + // skip checks since notification has already been sent + if (isEarlyEpochEnd[epoch]) { + return; + } + + uint256 threshold = earlyEpochEndThreshold(); + uint256 faultyValidatorsCount = _countFaultyValidators(epoch); + + // threshold has not been passed + if (faultyValidatorsCount < threshold) { + return; + } - uint256 unflaggedValidatorsCount = validatorSetContract.getCurrentValidatorsCount() - flaggedValidators.length; // 16 - 4 = 12 + isEarlyEpochEnd[epoch] = true; + blockRewardContract.notifyEarlyEpochEnd(); + + emit NotifyEarlyEpochEnd(epoch, block.number); + } + + function _getReportersThreshold(uint256 epoch) private view returns (uint256) { + uint256 unflaggedValidatorsCount = validatorSetContract.getCurrentValidatorsCount() - + getFlaggedValidatorsCount(epoch); + + return (2 * unflaggedValidatorsCount) / 3 + 1; + } - uint256 reportersThreshold = (2 * unflaggedValidatorsCount) / 3 + 1; // 24 / 3 + 1 = 9 + function _countFaultyValidators(uint256 epoch) private view returns (uint256) { + uint256 reportersThreshold = _getReportersThreshold(epoch); uint256 result = 0; + address[] memory flaggedValidators = getFlaggedValidatorsByEpoch(epoch); + for (uint256 i = 0; i < flaggedValidators.length; ++i) { address validator = flaggedValidators[i]; @@ -226,18 +311,4 @@ contract ConnectivityTrackerHbbft is Initializable, OwnableUpgradeable, IConnect revert ReportTooEarly(); } } - - function _decideEarlyEpochEndNeeded(uint256 epoch) private { - uint256 threshold = earlyEpochEndThreshold(); - uint256 faultyValidatorsCount = _countFaultyValidators(epoch); - - if (faultyValidatorsCount < threshold) { - return; - } - - isEarlyEpochEnd[epoch] = true; - blockRewardContract.notifyEarlyEpochEnd(); - - emit NotifyEarlyEpochEnd(epoch, block.number); - } } diff --git a/contracts/KeyGenHistory.sol b/contracts/KeyGenHistory.sol index 68ef9789..f11c1a00 100644 --- a/contracts/KeyGenHistory.sol +++ b/contracts/KeyGenHistory.sol @@ -38,8 +38,6 @@ contract KeyGenHistory is Initializable, OwnableUpgradeable, IKeyGenHistory { /// more infos: https://github.com/DMDcoin/hbbft-posdao-contracts/issues/106 uint256 public currentKeyGenRound; - event NewValidatorsSet(address[] newValidatorSet); - error AcksAlreadySubmitted(); error IncorrectEpoch(); error IncorrectRound(uint256 expected, uint256 submited); diff --git a/contracts/StakingHbbft.sol b/contracts/StakingHbbft.sol index f4127508..509a09d2 100644 --- a/contracts/StakingHbbft.sol +++ b/contracts/StakingHbbft.sol @@ -11,6 +11,7 @@ import { ValueGuards } from "./ValueGuards.sol"; import { IBlockRewardHbbft } from "./interfaces/IBlockRewardHbbft.sol"; import { IStakingHbbft } from "./interfaces/IStakingHbbft.sol"; import { IValidatorSetHbbft } from "./interfaces/IValidatorSetHbbft.sol"; +import { IBonusScoreSystem } from "./interfaces/IBonusScoreSystem.sol"; import { Unauthorized, ZeroAddress, ZeroGasPrice } from "./lib/Errors.sol"; import { TransferUtils } from "./utils/TransferUtils.sol"; @@ -38,6 +39,10 @@ contract StakingHbbft is Initializable, OwnableUpgradeable, ReentrancyGuardUpgra /// @dev The limit of the minimum delegator stake (DELEGATOR_MIN_STAKE). uint256 public delegatorMinStake; + /// @dev current limit of how many funds can + /// be staked on a single validator. + uint256 public maxStakeAmount; + /// @dev The current amount of staking coins ordered for withdrawal from the specified /// pool by the specified staker. Used by the `orderWithdraw`, `claimOrderedWithdraw` and other functions. /// The first parameter is the pool staking address, the second one is the staker address. @@ -101,6 +106,10 @@ contract StakingHbbft is Initializable, OwnableUpgradeable, ReentrancyGuardUpgra /// The pool staking address is accepted as a parameter. mapping(address => uint256) public stakeAmountTotal; + /// @dev Returns the total amount of staking coins currently staked on all pools. + /// Doesn't include the amount ordered for withdrawal. + uint256 public totalStakedAmount; + /// @dev The address of the `ValidatorSetHbbft` contract. IValidatorSetHbbft public validatorSetContract; @@ -112,9 +121,6 @@ contract StakingHbbft is Initializable, OwnableUpgradeable, ReentrancyGuardUpgra mapping(address => PoolInfo) public poolInfo; - /// @dev current limit of how many funds can - /// be staked on a single validator. - uint256 public maxStakeAmount; mapping(address => bool) public abandonedAndRemoved; @@ -133,6 +139,8 @@ contract StakingHbbft is Initializable, OwnableUpgradeable, ReentrancyGuardUpgra /// @dev Number of last epoch when stake snapshot was taken. pool => delegator => epoch mapping(address => mapping(address => uint256)) internal _stakeSnapshotLastEpoch; + IBonusScoreSystem public bonusScoreContract; + // ============================================== Constants ======================================================= /// @dev The max number of candidates (including validators). This limit was determined through stress testing. @@ -224,14 +232,19 @@ contract StakingHbbft is Initializable, OwnableUpgradeable, ReentrancyGuardUpgra uint256 delegatorsReward ); - /** * @dev Emitted when the minimum stake for a delegator is updated. * @param minStake The new minimum stake value. */ event SetDelegatorMinStake(uint256 minStake); -// ============================================== Errors ======================================================= + /** + * @dev Emitted when the BonusScoreSystem contract address is changed. + * @param _address BonusScoreSystem contract address. + */ + event SetBonusScoreContract(address _address); + + // ============================================== Errors ======================================================= error CannotClaimWithdrawOrderYet(address pool, address staker); error MaxPoolsCountExceeded(); error MaxAllowedWithdrawExceeded(uint256 allowed, uint256 desired); @@ -240,7 +253,6 @@ contract StakingHbbft is Initializable, OwnableUpgradeable, ReentrancyGuardUpgra error PoolAbandoned(address pool); error PoolCannotBeRemoved(address pool); error PoolEmpty(address pool); - error PoolMiningBanned(address pool); error PoolNotExist(address pool); error PoolStakeLimitExceeded(address pool, address delegator); error InitialStakingPoolsListEmpty(); @@ -285,6 +297,13 @@ contract StakingHbbft is Initializable, OwnableUpgradeable, ReentrancyGuardUpgra _; } + modifier onlyBonusScoreContract() { + if (msg.sender != address(bonusScoreContract)) { + revert Unauthorized(); + } + _; + } + // =============================================== Setters ======================================================== /// @custom:oz-upgrades-unsafe-allow constructor @@ -335,6 +354,8 @@ contract StakingHbbft is Initializable, OwnableUpgradeable, ReentrancyGuardUpgra __ReentrancyGuard_init(); validatorSetContract = IValidatorSetHbbft(stakingParams._validatorSetContract); + bonusScoreContract = IBonusScoreSystem(stakingParams._bonusScoreContract); + address[] calldata initStakingAddresses = stakingParams._initialStakingAddresses; for (uint256 i = 0; i < initStakingAddresses.length; ++i) { @@ -362,7 +383,7 @@ contract StakingHbbft is Initializable, OwnableUpgradeable, ReentrancyGuardUpgra "delegatorMinStake()", delegatorMinStakeAllowedParams ); - + delegatorMinStake = stakingParams._delegatorMinStake; candidateMinStake = stakingParams._candidateMinStake; @@ -429,6 +450,16 @@ contract StakingHbbft is Initializable, OwnableUpgradeable, ReentrancyGuardUpgra poolInfo[_validatorAddress].port = _port; } + function setBonusScoreContract(address _bonusScoreContract) external onlyOwner { + if (_bonusScoreContract == address(0)) { + revert ZeroAddress(); + } + + bonusScoreContract = IBonusScoreSystem(_bonusScoreContract); + + emit SetBonusScoreContract(_bonusScoreContract); + } + /// @dev Increments the serial number of the current staking epoch. /// Called by the `ValidatorSetHbbft.newValidatorSet` at the last block of the finished staking epoch. function incrementStakingEpoch() external onlyValidatorSetContract { @@ -635,6 +666,7 @@ contract StakingHbbft is Initializable, OwnableUpgradeable, ReentrancyGuardUpgra stakeAmount[_poolStakingAddress][_poolStakingAddress] += validatorReward; stakeAmountTotal[_poolStakingAddress] += poolReward; + totalStakedAmount += poolReward; _setLikelihood(_poolStakingAddress); @@ -660,12 +692,7 @@ contract StakingHbbft is Initializable, OwnableUpgradeable, ReentrancyGuardUpgra address staker = msg.sender; - if ( - !_isWithdrawAllowed( - validatorSetContract.miningByStakingAddress(_poolStakingAddress), - staker != _poolStakingAddress - ) - ) { + if (!areStakeAndWithdrawAllowed()) { revert WithdrawNotAllowed(); } @@ -686,6 +713,7 @@ contract StakingHbbft is Initializable, OwnableUpgradeable, ReentrancyGuardUpgra newOrderedAmountTotal = newOrderedAmountTotal + amount; newStakeAmount = newStakeAmount - amount; newStakeAmountTotal = newStakeAmountTotal - amount; + totalStakedAmount -= amount; orderWithdrawEpoch[_poolStakingAddress][staker] = stakingEpoch; } else { uint256 amount = uint256(-_amount); @@ -693,6 +721,7 @@ contract StakingHbbft is Initializable, OwnableUpgradeable, ReentrancyGuardUpgra newOrderedAmountTotal = newOrderedAmountTotal - amount; newStakeAmount = newStakeAmount + amount; newStakeAmountTotal = newStakeAmountTotal + amount; + totalStakedAmount += amount; } orderedWithdrawAmount[_poolStakingAddress][staker] = newOrderedAmount; orderedWithdrawAmountTotal[_poolStakingAddress] = newOrderedAmountTotal; @@ -757,12 +786,7 @@ contract StakingHbbft is Initializable, OwnableUpgradeable, ReentrancyGuardUpgra revert CannotClaimWithdrawOrderYet(_poolStakingAddress, staker); } - if ( - !_isWithdrawAllowed( - validatorSetContract.miningByStakingAddress(_poolStakingAddress), - staker != _poolStakingAddress - ) - ) { + if (!areStakeAndWithdrawAllowed()) { revert WithdrawNotAllowed(); } @@ -806,6 +830,7 @@ contract StakingHbbft is Initializable, OwnableUpgradeable, ReentrancyGuardUpgra uint256 gatheredPerStakingAddress = stakeAmountTotal[stakingAddress]; stakeAmountTotal[stakingAddress] = 0; + totalStakedAmount -= gatheredPerStakingAddress; address[] memory delegators = poolDelegators(stakingAddress); for (uint256 j = 0; j < delegators.length; ++j) { @@ -855,6 +880,12 @@ contract StakingHbbft is Initializable, OwnableUpgradeable, ReentrancyGuardUpgra snapshotPoolValidatorStakeAmount[_epoch][_stakingPool] = stakeAmount[_stakingPool][_stakingPool]; } + function updatePoolLikelihood(address mining, uint256 validatorScore) external onlyBonusScoreContract { + address stakingAddress = validatorSetContract.stakingByMiningAddress(mining); + + _updateLikelihood(stakingAddress, validatorScore); + } + // =============================================== Getters ======================================================== /// @dev Returns an array of the current active pools (the staking addresses of candidates and validators). @@ -945,6 +976,12 @@ contract StakingHbbft is Initializable, OwnableUpgradeable, ReentrancyGuardUpgra return _pools.contains(_stakingAddress); } + /// @dev Returns a flag indicating whether a specified address is in the `_pools` or `poolsInactive` array. + /// @param _stakingAddress The staking address of the pool. + function isPoolValid(address _stakingAddress) public view returns (bool) { + return _pools.contains(_stakingAddress) || _poolsInactive.contains(_stakingAddress); + } + /// @dev Returns the maximum amount which can be withdrawn from the specified pool by the specified staker /// at the moment. Used by the `withdraw` and `moveStake` functions. /// @param _poolStakingAddress The pool staking address from which the withdrawal will be made. @@ -952,10 +989,7 @@ contract StakingHbbft is Initializable, OwnableUpgradeable, ReentrancyGuardUpgra function maxWithdrawAllowed(address _poolStakingAddress, address _staker) public view returns (uint256) { address miningAddress = validatorSetContract.miningByStakingAddress(_poolStakingAddress); - if ( - !_isWithdrawAllowed(miningAddress, _poolStakingAddress != _staker) || - abandonedAndRemoved[_poolStakingAddress] - ) { + if (!areStakeAndWithdrawAllowed() || abandonedAndRemoved[_poolStakingAddress]) { return 0; } @@ -987,7 +1021,7 @@ contract StakingHbbft is Initializable, OwnableUpgradeable, ReentrancyGuardUpgra function maxWithdrawOrderAllowed(address _poolStakingAddress, address _staker) public view returns (uint256) { address miningAddress = validatorSetContract.miningByStakingAddress(_poolStakingAddress); - if (!_isWithdrawAllowed(miningAddress, _poolStakingAddress != _staker)) { + if (!areStakeAndWithdrawAllowed()) { return 0; } @@ -1237,24 +1271,26 @@ contract StakingHbbft is Initializable, OwnableUpgradeable, ReentrancyGuardUpgra /// @dev Calculates (updates) the probability of being selected as a validator for the specified pool /// and updates the total sum of probability coefficients. Actually, the probability is equal to the - /// amount totally staked into the pool. See the `getPoolsLikelihood` getter. + /// amount totally staked into the pool multiplied by validator bonus score. See the `getPoolsLikelihood` getter. /// Used by the staking and withdrawal functions. /// @param _poolStakingAddress The address of the pool for which the probability coefficient must be updated. function _setLikelihood(address _poolStakingAddress) private { + address miningAddress = validatorSetContract.miningByStakingAddress(_poolStakingAddress); + uint256 validatorBonusScore = bonusScoreContract.getValidatorScore(miningAddress); + + _updateLikelihood(_poolStakingAddress, validatorBonusScore); + } + + function _updateLikelihood(address _poolStakingAddress, uint256 validatorBonusScore) private { (bool isToBeElected, uint256 index) = _isPoolToBeElected(_poolStakingAddress); if (!isToBeElected) return; uint256 oldValue = _poolsLikelihood[index]; - uint256 newValue = stakeAmountTotal[_poolStakingAddress]; + uint256 newValue = stakeAmountTotal[_poolStakingAddress] * validatorBonusScore; _poolsLikelihood[index] = newValue; - - if (newValue >= oldValue) { - _poolsLikelihoodSum = _poolsLikelihoodSum + (newValue - oldValue); - } else { - _poolsLikelihoodSum = _poolsLikelihoodSum - (oldValue - newValue); - } + _poolsLikelihoodSum = _poolsLikelihoodSum - oldValue + newValue; } /// @dev The internal function used by the `_stake` and `moveStake` functions. @@ -1276,10 +1312,6 @@ contract StakingHbbft is Initializable, OwnableUpgradeable, ReentrancyGuardUpgra revert InsufficientStakeAmount(_poolStakingAddress, _staker); } - if (validatorSetContract.isValidatorBanned(poolMiningAddress)) { - revert PoolMiningBanned(_poolStakingAddress); - } - if (abandonedAndRemoved[_poolStakingAddress]) { revert PoolAbandoned(_poolStakingAddress); } @@ -1313,6 +1345,7 @@ contract StakingHbbft is Initializable, OwnableUpgradeable, ReentrancyGuardUpgra _stakeAmountByEpoch[_poolStakingAddress][_staker][stakingEpoch] += _amount; stakeAmountTotal[_poolStakingAddress] += _amount; + totalStakedAmount += _amount; if (selfStake) { // `staker` places a stake for himself and becomes a candidate @@ -1371,6 +1404,7 @@ contract StakingHbbft is Initializable, OwnableUpgradeable, ReentrancyGuardUpgra ? amountByEpoch - _amount : 0; stakeAmountTotal[_poolStakingAddress] -= _amount; + totalStakedAmount -= _amount; if (newStakeAmount == 0) { _withdrawCheckPool(_poolStakingAddress, _staker); @@ -1452,26 +1486,6 @@ contract StakingHbbft is Initializable, OwnableUpgradeable, ReentrancyGuardUpgra return (false, 0); } - /// @dev Returns `true` if withdrawal from the pool of the specified candidate/validator is allowed at the moment. - /// Used by all withdrawal functions. - /// @param _miningAddress The mining address of the validator's pool. - /// @param _isDelegator Whether the withdrawal is requested by a delegator, not by a candidate/validator. - function _isWithdrawAllowed(address _miningAddress, bool _isDelegator) private view returns (bool) { - if (_isDelegator) { - if (validatorSetContract.areDelegatorsBanned(_miningAddress)) { - // The delegator cannot withdraw from the banned validator pool until the ban is expired - return false; - } - } else { - if (validatorSetContract.isValidatorBanned(_miningAddress)) { - // The banned validator cannot withdraw from their pool until the ban is expired - return false; - } - } - - return areStakeAndWithdrawAllowed(); - } - function _delegatorRewardShare( bool _minRewardPercentExceeded, uint256 _totalStake, diff --git a/contracts/TxPermissionHbbft.sol b/contracts/TxPermissionHbbft.sol index 8ccdede8..bca63041 100644 --- a/contracts/TxPermissionHbbft.sol +++ b/contracts/TxPermissionHbbft.sol @@ -12,13 +12,19 @@ import { IKeyGenHistory } from "./interfaces/IKeyGenHistory.sol"; import { IValidatorSetHbbft } from "./interfaces/IValidatorSetHbbft.sol"; import { IConnectivityTrackerHbbft } from "./interfaces/IConnectivityTrackerHbbft.sol"; -import { DEFAULT_BLOCK_GAS_LIMIT, DEFAULT_GAS_PRICE, MIN_BLOCK_GAS_LIMIT } from "./lib/Constants.sol"; +import { DEFAULT_BLOCK_GAS_LIMIT, DEFAULT_GAS_PRICE } from "./lib/Constants.sol"; import { ZeroAddress } from "./lib/Errors.sol"; /// @dev Controls the use of zero gas price by validators in service transactions, /// protecting the network against "transaction spamming" by malicious validators. /// The protection logic is declared in the `allowedTxTypes` function. contract TxPermissionHbbft is Initializable, OwnableUpgradeable, ITxPermission, ValueGuards { + struct AllowanceCheckResult { + uint32 mask; + bool knownFunc; + bool cache; + } + // =============================================== Storage ======================================================== // WARNING: since this contract is upgradeable, do not remove @@ -61,9 +67,6 @@ contract TxPermissionHbbft is Initializable, OwnableUpgradeable, ITxPermission, // Function signatures - // bytes4(keccak256("reportMalicious(address,uint256,bytes)")) - bytes4 public constant REPORT_MALICIOUS_SIGNATURE = 0xc476dd40; - // bytes4(keccak256("writePart(uint256,uint256,bytes)")) bytes4 public constant WRITE_PART_SIGNATURE = 0x2d4de124; @@ -220,13 +223,7 @@ contract TxPermissionHbbft is Initializable, OwnableUpgradeable, ITxPermission, /// The limit can be changed by the owner (typical the DAO) /// @param _value The new minimum gas price. function setMinimumGasPrice(uint256 _value) public onlyOwner withinAllowedRange(_value) { - // currently, we do not allow to set the minimum gas price to 0, - // that would open pandoras box, and the consequences of doing that, - // requires deeper research. - if (_value == 0) { - revert InvalidMinGasPrice(); - } - + // param value validation is done in the {ValueGuards-withinAllowedRange} modifier minimumGasPrice = _value; emit GasPriceChanged(_value); @@ -235,13 +232,7 @@ contract TxPermissionHbbft is Initializable, OwnableUpgradeable, ITxPermission, /// @dev set's the block gas limit. /// IN HBBFT, there must be consens about the block gas limit. function setBlockGasLimit(uint256 _value) public onlyOwner withinAllowedRange(_value) { - // we make some check that the block gas limit can not be set to low, - // to prevent the chain to be completly inoperatable. - // this value is chosen arbitrarily - if (_value < MIN_BLOCK_GAS_LIMIT) { - revert InvalidBlockGasLimit(); - } - + // param value validation is done in the {ValueGuards-withinAllowedRange} modifier blockGasLimit = _value; emit BlockGasLimitChanged(_value); @@ -316,24 +307,6 @@ contract TxPermissionHbbft is Initializable, OwnableUpgradeable, ITxPermission, } if (_to == address(validatorSetContract)) { - // The rules for the ValidatorSet contract - if (signature == REPORT_MALICIOUS_SIGNATURE) { - uint256 paramsSize = _data.length - 4 > 64 ? 64 : _data.length - 4; - bytes memory abiParams = _memcpy(_data, paramsSize, 4); - - (address maliciousMiningAddress, uint256 blockNumber) = abi.decode(abiParams, (address, uint256)); - - // The `reportMalicious()` can only be called by the validator's mining address - // when the calling is allowed - // slither-disable-next-line unused-return - (bool callable, ) = validatorSetContract.reportMaliciousCallable( - _sender, - maliciousMiningAddress, - blockNumber - ); - return (callable ? CALL : NONE, false); - } - if (signature == ANNOUNCE_AVAILABILITY_SIGNATURE) { return (validatorSetContract.canCallAnnounceAvailability(_sender) ? CALL : NONE, false); } @@ -430,8 +403,9 @@ contract TxPermissionHbbft is Initializable, OwnableUpgradeable, ITxPermission, } if (_to == address(connectivityTracker)) { - if (signature == REPORT_MISSING_CONNECTIVITY_SELECTOR || signature == REPORT_RECONNECT_SELECTOR) { - return _handleCallToConnectivityTracker(_sender, signature, _data); + AllowanceCheckResult memory result = _handleCallToConnectivityTracker(_sender, signature, _data); + if (result.knownFunc) { + return (result.mask, result.cache); } // if there is another external call to ConnectivityTracker contracts. @@ -503,35 +477,39 @@ contract TxPermissionHbbft is Initializable, OwnableUpgradeable, ITxPermission, address sender, bytes4 selector, bytes memory _calldata - ) internal view returns (uint32 typesMask, bool cache) { + ) internal view returns (AllowanceCheckResult memory) { // 3 x 32 bytes calldata args = 96 bytes uint256 paramsSize = _calldata.length - 4 > 96 ? 96 : _calldata.length - 4; bytes memory params = _memcpy(_calldata, paramsSize, 4); - (address validator, uint256 blockNumber, bytes32 blockHash) = abi.decode(params, (address, uint256, bytes32)); + AllowanceCheckResult memory result = AllowanceCheckResult({ mask: NONE, knownFunc: true, cache: false }); if (selector == REPORT_MISSING_CONNECTIVITY_SELECTOR) { - uint32 mask = NONE; + (address validator, uint256 blockNumber, bytes32 blockHash) = abi.decode( + params, + (address, uint256, bytes32) + ); try connectivityTracker.checkReportMissingConnectivityCallable(sender, validator, blockNumber, blockHash) { - mask = CALL; + result.mask = CALL; } catch { - mask = NONE; + result.mask = NONE; } - - return (mask, false); - } - - if (selector == REPORT_RECONNECT_SELECTOR) { - uint32 mask = NONE; + } else if (selector == REPORT_RECONNECT_SELECTOR) { + (address validator, uint256 blockNumber, bytes32 blockHash) = abi.decode( + params, + (address, uint256, bytes32) + ); try connectivityTracker.checkReportReconnectCallable(sender, validator, blockNumber, blockHash) { - mask = CALL; + result.mask = CALL; } catch { - mask = NONE; + result.mask = NONE; } - - return (mask, false); + } else { + result.knownFunc = false; } + + return result; } } diff --git a/contracts/ValidatorSetHbbft.sol b/contracts/ValidatorSetHbbft.sol index eb07e6ba..7c64b7f9 100644 --- a/contracts/ValidatorSetHbbft.sol +++ b/contracts/ValidatorSetHbbft.sol @@ -8,7 +8,7 @@ import { IKeyGenHistory } from "./interfaces/IKeyGenHistory.sol"; import { IRandomHbbft } from "./interfaces/IRandomHbbft.sol"; import { IStakingHbbft } from "./interfaces/IStakingHbbft.sol"; import { IValidatorSetHbbft } from "./interfaces/IValidatorSetHbbft.sol"; -import { SYSTEM_ADDRESS } from "./lib/Constants.sol"; +import { IBonusScoreSystem } from "./interfaces/IBonusScoreSystem.sol"; import { Unauthorized, ValidatorsListEmpty, ZeroAddress } from "./lib/Errors.sol"; /// @dev Stores the current validator set and contains the logic for choosing new validators @@ -22,23 +22,20 @@ contract ValidatorSetHbbft is Initializable, OwnableUpgradeable, IValidatorSetHb address[] internal _pendingValidators; address[] internal _previousValidators; - /// @dev Stores the validators that have reported the specific validator as malicious for the specified epoch. - // slither-disable-next-line uninitialized-state - mapping(address => mapping(uint256 => address[])) internal _maliceReportedForBlock; + /// @custom:oz-renamed-from _maliceReportedForBlock + mapping(address => mapping(uint256 => address[])) internal _unused1; - /// @dev How many times a given mining address was banned. - mapping(address => uint256) public banCounter; + /// @custom:oz-renamed-from banCounter + mapping(address => uint256) public _unused2; - /// @dev Returns the time when the ban will be lifted for the specified mining address. - mapping(address => uint256) public bannedUntil; + /// @custom:oz-renamed-from bannedUntil + mapping(address => uint256) public _unused3; - /// @dev Returns the timestamp after which the ban will be lifted for delegators - /// of the specified pool (mining address). - mapping(address => uint256) public bannedDelegatorsUntil; + /// @custom:oz-renamed-from bannedDelegatorsUntil + mapping(address => uint256) public _unused4; - /// @dev The reason for the latest ban of the specified mining address. See the `_removeMaliciousValidator` - /// internal function description for the list of possible reasons. - mapping(address => bytes32) public banReason; + /// @custom:oz-renamed-from banReason + mapping(address => bytes32) public _unused5; /// @dev The address of the `BlockRewardHbbft` contract. address public blockRewardContract; @@ -58,15 +55,11 @@ contract ValidatorSetHbbft is Initializable, OwnableUpgradeable, IValidatorSetHb /// @dev The `RandomHbbft` contract address. address public randomContract; - /// @dev The number of times the specified validator (mining address) reported misbehaviors during the specified - /// staking epoch. Used by the `reportMaliciousCallable` getter and `reportMalicious` function to determine - /// whether a validator reported too often. - mapping(address => mapping(uint256 => uint256)) public reportingCounter; + /// @custom:oz-renamed-from reportingCounter + mapping(address => mapping(uint256 => uint256)) public _unused6; - /// @dev How many times all validators reported misbehaviors during the specified staking epoch. - /// Used by the `reportMaliciousCallable` getter and `reportMalicious` function to determine - /// whether a validator reported too often. - mapping(uint256 => uint256) public reportingCounterTotal; + /// @custom:oz-renamed-from reportingCounterTotal + mapping(uint256 => uint256) public _unused7; /// @dev A staking address bound to a specified mining address. /// See the `_setStakingAddress` internal function. @@ -95,20 +88,15 @@ contract ValidatorSetHbbft is Initializable, OwnableUpgradeable, IValidatorSetHb /// @dev The max number of validators. uint256 public maxValidators; - /// @dev duration of ban in epochs - uint256 public banDuration; + /// @custom:oz-renamed-from banDuration + uint256 public _unused8; /// @dev time in seconds after which the inactive validator is considered abandoned uint256 public validatorInactivityThreshold; - // ================================================ Events ======================================================== + IBonusScoreSystem public bonusScoreSystem; - /// @dev Emitted by the `reportMalicious` function to signal that a specified validator reported - /// misbehavior by a specified malicious validator at a specified block number. - /// @param reportingValidator The mining address of the reporting validator. - /// @param maliciousValidator The mining address of the malicious validator. - /// @param blockNumber The block number at which the `maliciousValidator` misbehaved. - event ReportedMalicious(address reportingValidator, address maliciousValidator, uint256 blockNumber); + // ================================================ Events ======================================================== event ValidatorAvailable(address validator, uint256 timestamp); @@ -117,8 +105,8 @@ contract ValidatorSetHbbft is Initializable, OwnableUpgradeable, IValidatorSetHb event ValidatorUnavailable(address validator, uint256 timestamp); event SetMaxValidators(uint256 _count); - event SetBanDuration(uint256 _value); event SetValidatorInactivityThreshold(uint256 _value); + event SetBonusScoreContract(address _address); error AnnounceBlockNumberTooOld(); error CantAnnounceAvailability(); @@ -160,14 +148,6 @@ contract ValidatorSetHbbft is Initializable, OwnableUpgradeable, IValidatorSetHb _; } - /// @dev Ensures the caller is the SYSTEM_ADDRESS. See https://wiki.parity.io/Validator-Set.html - modifier onlySystem() { - if (msg.sender != SYSTEM_ADDRESS) { - revert Unauthorized(); - } - _; - } - /// @custom:oz-upgrades-unsafe-allow constructor constructor() { // Prevents initialization of implementation contract @@ -186,30 +166,24 @@ contract ValidatorSetHbbft is Initializable, OwnableUpgradeable, IValidatorSetHb /// @dev Initializes the network parameters. Used by the /// constructor of the `InitializerHbbft` contract. /// @param _contractOwner The address of the contract owner. - /// @param _blockRewardContract The address of the `BlockRewardHbbft` contract. - /// @param _randomContract The address of the `RandomHbbft` contract. - /// @param _stakingContract The address of the `StakingHbbft` contract. - /// @param _keyGenHistoryContract The address of the `KeyGenHistory` contract. - /// @param _validatorInactivityThreshold The time of inactivity in seconds to consider validator abandoned + /// @param _params ValidatorSetHbbft contract parameeters (introduced to avoid stack too deep issue): + /// blockRewardContract The address of the `BlockRewardHbbft` contract. + /// randomContract The address of the `RandomHbbft` contract. + /// stakingContract The address of the `StakingHbbft` contract. + /// keyGenHistoryContract The address of the `KeyGenHistory` contract. + /// bonusScoreContract The address of the `BonusScoreSystem` contract. + /// validatorInactivityThreshold The time of inactivity in seconds to consider validator abandoned /// @param _initialMiningAddresses The array of initial validators' mining addresses. /// @param _initialStakingAddresses The array of initial validators' staking addresses. function initialize( address _contractOwner, - address _blockRewardContract, - address _randomContract, - address _stakingContract, - address _keyGenHistoryContract, - uint256 _validatorInactivityThreshold, + ValidatorSetParams calldata _params, address[] calldata _initialMiningAddresses, address[] calldata _initialStakingAddresses ) external initializer { - if ( - _contractOwner == address(0) || - _blockRewardContract == address(0) || - _randomContract == address(0) || - _stakingContract == address(0) || - _keyGenHistoryContract == address(0) - ) { + _validateParams(_params); + + if (_contractOwner == address(0)) { revert ZeroAddress(); } @@ -223,11 +197,12 @@ contract ValidatorSetHbbft is Initializable, OwnableUpgradeable, IValidatorSetHb __Ownable_init(_contractOwner); - blockRewardContract = _blockRewardContract; - randomContract = _randomContract; - stakingContract = IStakingHbbft(_stakingContract); - keyGenHistoryContract = IKeyGenHistory(_keyGenHistoryContract); - validatorInactivityThreshold = _validatorInactivityThreshold; + blockRewardContract = _params.blockRewardContract; + randomContract = _params.randomContract; + stakingContract = IStakingHbbft(_params.stakingContract); + keyGenHistoryContract = IKeyGenHistory(_params.keyGenHistoryContract); + bonusScoreSystem = IBonusScoreSystem(_params.bonusScoreContract); + validatorInactivityThreshold = _params.validatorInactivityThreshold; // Add initial validators to the `_currentValidators` array for (uint256 i = 0; i < _initialMiningAddresses.length; i++) { @@ -240,7 +215,6 @@ contract ValidatorSetHbbft is Initializable, OwnableUpgradeable, IValidatorSetHb } maxValidators = 25; - banDuration = 12; } /// @dev Called by the system when a pending validator set is ready to be activated. @@ -254,6 +228,9 @@ contract ValidatorSetHbbft is Initializable, OwnableUpgradeable, IValidatorSetHb _finalizeNewValidators(); } + _rewardValidatorsStandBy(); + _penaliseValidatorsNoStandBy(); + // new epoch starts stakingContract.incrementStakingEpoch(); keyGenHistoryContract.notifyNewEpoch(); @@ -287,11 +264,14 @@ contract ValidatorSetHbbft is Initializable, OwnableUpgradeable, IValidatorSetHb emit SetValidatorInactivityThreshold(_seconds); } - /// @dev Removes malicious validators. - /// Called by the the Hbbft engine when a validator has been inactive for a long period. - /// @param _miningAddresses The mining addresses of the malicious validators. - function removeMaliciousValidators(address[] calldata _miningAddresses) external onlySystem { - _removeMaliciousValidators(_miningAddresses, "inactive"); + function setBonusScoreSystemAddress(address _address) external onlyOwner { + if (_address == address(0)) { + revert ZeroAddress(); + } + + bonusScoreSystem = IBonusScoreSystem(_address); + + emit SetBonusScoreContract(_address); } /// @dev called by validators when a validator comes online after @@ -320,11 +300,7 @@ contract ValidatorSetHbbft is Initializable, OwnableUpgradeable, IValidatorSetHb uint256 timestamp = block.timestamp; _writeValidatorAvailableSince(msg.sender, timestamp); - // as long the mining node is not banned as well, - // it can be picked up as regular active node again. - if (!isValidatorBanned(msg.sender)) { - stakingContract.notifyAvailability(stakingByMiningAddress[msg.sender]); - } + stakingContract.notifyAvailability(stakingByMiningAddress[msg.sender]); emit ValidatorAvailable(msg.sender, timestamp); } @@ -415,6 +391,10 @@ contract ValidatorSetHbbft is Initializable, OwnableUpgradeable, IValidatorSetHb // the pool does not get a Ban, // but is treated as "inactive" as long it does not `announceAvailability()` + // Decrease validator bonus score because of missed Part/ACK + // Should be called before `removePool` as it's changes pool likelihood + bonusScoreSystem.penaliseNoKeyWrite(miningAddress); + stakingContract.removePool(stakingByMiningAddress[miningAddress]); // mark the Node address as not available. @@ -459,60 +439,6 @@ contract ValidatorSetHbbft is Initializable, OwnableUpgradeable, IValidatorSetHb _writeValidatorAvailableSince(miningByStakingAddress[_stakingAddress], 0); } - /// @dev Reports that the malicious validator misbehaved at the specified block. - /// Called by the node of each honest validator after the specified validator misbehaved. - /// See https://openethereum.github.io/Validator-Set.html#reporting-contract - /// Can only be called when the `reportMaliciousCallable` getter returns `true`. - /// @param _maliciousMiningAddress The mining address of the malicious validator. - /// @param _blockNumber The block number where the misbehavior was observed. - function reportMalicious(address _maliciousMiningAddress, uint256 _blockNumber, bytes calldata) external { - address reportingMiningAddress = msg.sender; - - _incrementReportingCounter(reportingMiningAddress); - - (bool callable, bool removeReportingValidator) = reportMaliciousCallable( - reportingMiningAddress, - _maliciousMiningAddress, - _blockNumber - ); - - if (!callable) { - if (removeReportingValidator) { - // Reporting validator has been reporting too often, so - // treat them as a malicious as well (spam) - address[] memory miningAddresses = new address[](1); - miningAddresses[0] = reportingMiningAddress; - _removeMaliciousValidators(miningAddresses, "spam"); - } - return; - } - - address[] storage reportedValidators = _maliceReportedForBlock[_maliciousMiningAddress][_blockNumber]; - - reportedValidators.push(reportingMiningAddress); - - emit ReportedMalicious(reportingMiningAddress, _maliciousMiningAddress, _blockNumber); - - uint256 validatorsLength = _currentValidators.length; - bool remove; - - if (validatorsLength > 3) { - // If more than 2/3 of validators reported about malicious validator - // for the same `blockNumber` - remove = reportedValidators.length * 3 > validatorsLength * 2; - } else { - // If more than 1/2 of validators reported about malicious validator - // for the same `blockNumber` - remove = reportedValidators.length * 2 > validatorsLength; - } - - if (remove) { - address[] memory miningAddresses = new address[](1); - miningAddresses[0] = _maliciousMiningAddress; - _removeMaliciousValidators(miningAddresses, "malicious"); - } - } - /// @dev Binds a mining address to the specified staking address. Called by the `StakingHbbft.addPool` function /// when a user wants to become a candidate and creates a pool. /// See also the `miningByStakingAddress` and `stakingByMiningAddress` public mappings. @@ -530,12 +456,6 @@ contract ValidatorSetHbbft is Initializable, OwnableUpgradeable, IValidatorSetHb emit SetMaxValidators(_maxValidators); } - function setBanDuration(uint256 _banDuration) external onlyOwner { - banDuration = _banDuration; - - emit SetBanDuration(_banDuration); - } - /// @dev set's the validators ip address. /// this function can only be called by validators. /// @param _ip IPV4 address of a running Node Software or Proxy. @@ -568,13 +488,6 @@ contract ValidatorSetHbbft is Initializable, OwnableUpgradeable, IValidatorSetHb return _currentValidators.length; } - /// @dev Returns a boolean flag indicating whether delegators of the specified pool are currently banned. - /// A validator pool can be banned when they misbehave (see the `_removeMaliciousValidator` function). - /// @param _miningAddress The mining address of the pool. - function areDelegatorsBanned(address _miningAddress) external view returns (bool) { - return block.timestamp <= bannedDelegatorsUntil[_miningAddress]; - } - /// @dev Returns the previous validator set (validators' mining addresses array). /// The array is stored by the `finalizeChange` function /// when a new staking epoch's validator set is finalized. @@ -595,31 +508,6 @@ contract ValidatorSetHbbft is Initializable, OwnableUpgradeable, IValidatorSetHb return _currentValidators; } - /// @dev Returns a boolean flag indicating whether the specified validator (mining address) - /// is able to call the `reportMalicious` function or whether the specified validator (mining address) - /// can be reported as malicious. This function also allows a validator to call the `reportMalicious` - /// function several blocks after ceasing to be a validator. This is possible if a - /// validator did not have the opportunity to call the `reportMalicious` function prior to the - /// engine calling the `finalizeChange` function. - /// @param _miningAddress The validator's mining address. - function isReportValidatorValid(address _miningAddress) public view returns (bool) { - bool isValid = isValidator[_miningAddress] && !isValidatorBanned(_miningAddress); - if (stakingContract.stakingEpoch() == 0) { - return isValid; - } - // TO DO: arbitrarily chosen period stakingFixedEpochDuration/5. - if ( - block.timestamp - stakingContract.stakingEpochStartTime() <= stakingContract.stakingFixedEpochDuration() / 5 - ) { - // The current validator set was finalized by the engine, - // but we should let the previous validators finish - // reporting malicious validator within a few blocks - bool previousValidator = isValidatorPrevious[_miningAddress]; - return isValid || previousValidator; - } - return isValid; - } - function getPendingValidatorKeyGenerationMode(address _miningAddress) external view returns (KeyGenMode) { // enum KeyGenMode { NotAPendingValidator, WritePart, WaitForOtherParts, // WriteAck, WaitForOtherAcks, AllKeysDone } @@ -661,13 +549,6 @@ contract ValidatorSetHbbft is Initializable, OwnableUpgradeable, IValidatorSetHb } } - /// @dev Returns a boolean flag indicating whether the specified mining address is currently banned. - /// A validator can be banned when they misbehave (see the `_removeMaliciousValidator` internal function). - /// @param _miningAddress The mining address. - function isValidatorBanned(address _miningAddress) public view returns (bool) { - return block.timestamp <= bannedUntil[_miningAddress]; - } - /// @dev Returns a boolean flag indicating whether the specified mining address is a validator /// or is in the `_pendingValidators`. /// Used by the `StakingHbbft.maxWithdrawAllowed` and `StakingHbbft.maxWithdrawOrderAllowed` getters. @@ -690,17 +571,6 @@ contract ValidatorSetHbbft is Initializable, OwnableUpgradeable, IValidatorSetHb return false; } - /// @dev Returns an array of the validators (their mining addresses) which reported that the specified malicious - /// validator misbehaved at the specified block. - /// @param _miningAddress The mining address of malicious validator. - /// @param _blockNumber The block number. - function maliceReportedForBlock( - address _miningAddress, - uint256 _blockNumber - ) external view returns (address[] memory) { - return _maliceReportedForBlock[_miningAddress][_blockNumber]; - } - /// @dev Returns if the specified _miningAddress is able to announce availability. /// @param _miningAddress mining address that is allowed/disallowed. function canCallAnnounceAvailability(address _miningAddress) public view returns (bool) { @@ -717,67 +587,6 @@ contract ValidatorSetHbbft is Initializable, OwnableUpgradeable, IValidatorSetHb return true; } - /// @dev Returns whether the `reportMalicious` function can be called by the specified validator with the - /// given parameters. Used by the `reportMalicious` function and `TxPermission` contract. Also, returns - /// a boolean flag indicating whether the reporting validator should be removed as malicious due to - /// excessive reporting during the current staking epoch. - /// @param _reportingMiningAddress The mining address of the reporting validator which is calling - /// the `reportMalicious` function. - /// @param _maliciousMiningAddress The mining address of the malicious validator which is passed to - /// the `reportMalicious` function. - /// @param _blockNumber The block number which is passed to the `reportMalicious` function. - /// @return callable `bool callable` - The boolean flag indicating whether the `reportMalicious` function - /// can be called at the moment. - /// @return removeReportingValidator `bool removeReportingValidator` - The boolean flag indicating whether - /// the reporting validator should be removed as malicious due to excessive reporting. This flag is only used - /// by the `reportMalicious` function. - function reportMaliciousCallable( - address _reportingMiningAddress, - address _maliciousMiningAddress, - uint256 _blockNumber - ) public view returns (bool callable, bool removeReportingValidator) { - if (!isReportValidatorValid(_reportingMiningAddress)) return (false, false); - if (!isReportValidatorValid(_maliciousMiningAddress)) return (false, false); - - uint256 validatorsNumber = _currentValidators.length; - - if (validatorsNumber > 1) { - uint256 currentStakingEpoch = stakingContract.stakingEpoch(); - uint256 reportsNumber = reportingCounter[_reportingMiningAddress][currentStakingEpoch]; - uint256 reportsTotalNumber = reportingCounterTotal[currentStakingEpoch]; - uint256 averageReportsNumberX10 = 0; - - if (reportsTotalNumber >= reportsNumber) { - averageReportsNumberX10 = ((reportsTotalNumber - reportsNumber) * 10) / (validatorsNumber - 1); - } - - if (reportsNumber > validatorsNumber * 50 && reportsNumber > averageReportsNumberX10) { - return (false, true); - } - } - - uint256 currentBlock = block.number; // TODO: _getCurrentBlockNumber(); Make it time based here ? - - if (_blockNumber > currentBlock) return (false, false); // avoid reporting about future blocks - - uint256 ancientBlocksLimit = 100; //TODO: needs to be afjusted for HBBFT specifications i.e. time - if (currentBlock > ancientBlocksLimit && _blockNumber < currentBlock - ancientBlocksLimit) { - return (false, false); // avoid reporting about ancient blocks - } - - address[] storage reportedValidators = _maliceReportedForBlock[_maliciousMiningAddress][_blockNumber]; - - // Don't allow reporting validator to report about the same misbehavior more than once - uint256 length = reportedValidators.length; - for (uint256 m = 0; m < length; m++) { - if (reportedValidators[m] == _reportingMiningAddress) { - return (false, false); - } - } - - return (true, false); - } - /// @dev Returns the public key for the given stakingAddress /// @param _stakingAddress staking address of the wanted public key. /// @return public key of the _stakingAddress @@ -826,25 +635,6 @@ contract ValidatorSetHbbft is Initializable, OwnableUpgradeable, IValidatorSetHb // ============================================== Internal ======================================================== - /// @dev Updates the total reporting counter (see the `reportingCounterTotal` public mapping) for the current - /// staking epoch after the specified validator is removed as malicious. The `reportMaliciousCallable` getter - /// uses this counter for reporting checks so it must be up-to-date. Called by the `_removeMaliciousValidators` - /// internal function. - /// @param _miningAddress The mining address of the removed malicious validator. - function _clearReportingCounter(address _miningAddress) internal { - uint256 currentStakingEpoch = stakingContract.stakingEpoch(); - uint256 total = reportingCounterTotal[currentStakingEpoch]; - uint256 counter = reportingCounter[_miningAddress][currentStakingEpoch]; - - reportingCounter[_miningAddress][currentStakingEpoch] = 0; - - if (total >= counter) { - reportingCounterTotal[currentStakingEpoch] -= counter; - } else { - reportingCounterTotal[currentStakingEpoch] = 0; - } - } - function _newValidatorSet(address[] memory _forcedPools) internal { address[] memory poolsToBeElected = stakingContract.getPoolsToBeElected(); @@ -949,77 +739,6 @@ contract ValidatorSetHbbft is Initializable, OwnableUpgradeable, IValidatorSetHb } } - /// @dev Increments the reporting counter for the specified validator and the current staking epoch. - /// See the `reportingCounter` and `reportingCounterTotal` public mappings. Called by the `reportMalicious` - /// function when the validator reports a misbehavior. - /// @param _reportingMiningAddress The mining address of reporting validator. - function _incrementReportingCounter(address _reportingMiningAddress) internal { - if (!isReportValidatorValid(_reportingMiningAddress)) return; - uint256 currentStakingEpoch = stakingContract.stakingEpoch(); - reportingCounter[_reportingMiningAddress][currentStakingEpoch]++; - reportingCounterTotal[currentStakingEpoch]++; - } - - /// @dev Removes the specified validator as malicious. Used by the `_removeMaliciousValidators` internal function. - /// @param _miningAddress The removed validator mining address. - /// @param _reason A short string of the reason why the mining address is treated as malicious: - /// "inactive" - the validator has not been contributing to block creation for sigificant period of time. - /// "spam" - the validator made a lot of `reportMalicious` callings compared with other validators. - /// "malicious" - the validator was reported as malicious by other validators with the `reportMalicious` function. - /// @return Returns `true` if the specified validator has been removed from the pending validator set. - /// Otherwise returns `false` (if the specified validator has already been removed or cannot be removed). - function _removeMaliciousValidator(address _miningAddress, bytes32 _reason) internal returns (bool) { - bool isBanned = isValidatorBanned(_miningAddress); - // Ban the malicious validator for at least the next 12 staking epochs - uint256 banUntil = _banUntil(); - - banCounter[_miningAddress]++; - bannedUntil[_miningAddress] = banUntil; - banReason[_miningAddress] = _reason; - - if (isBanned) { - // The validator is already banned - return false; - } else { - bannedDelegatorsUntil[_miningAddress] = banUntil; - } - - // Remove malicious validator from the `pools` - address stakingAddress = stakingByMiningAddress[_miningAddress]; - stakingContract.removePool(stakingAddress); - - // If the validator set has only one validator, don't remove it. - uint256 length = _currentValidators.length; - if (length == 1) { - return false; - } - - for (uint256 i = 0; i < length; i++) { - if (_currentValidators[i] == _miningAddress) { - // Remove the malicious validator from `_pendingValidators` - _currentValidators[i] = _currentValidators[length - 1]; - _currentValidators.pop(); - return true; - } - } - - return false; - } - - /// @dev Removes the specified validators as malicious from the pending validator set. Does nothing if - /// the specified validators are already banned or don't exist in the pending validator set. - /// @param _miningAddresses The mining addresses of the malicious validators. - /// @param _reason A short string of the reason why the mining addresses are treated as malicious, - /// see the `_removeMaliciousValidator` internal function description for possible values. - function _removeMaliciousValidators(address[] memory _miningAddresses, bytes32 _reason) internal { - for (uint256 i = 0; i < _miningAddresses.length; i++) { - if (_removeMaliciousValidator(_miningAddresses[i], _reason)) { - // From this moment `getPendingValidators()` returns the new validator set - _clearReportingCounter(_miningAddresses[i]); - } - } - } - /// @dev Stores previous validators. Used by the `finalizeChange` function. function _savePreviousValidators() internal { uint256 length; @@ -1115,14 +834,35 @@ contract ValidatorSetHbbft is Initializable, OwnableUpgradeable, IValidatorSetHb validatorAvailableSinceLastWrite[_validator] = block.timestamp; } - /// @dev Returns the future timestamp until which a validator is banned. - /// Used by the `_removeMaliciousValidator` internal function. - function _banUntil() internal view returns (uint256) { - uint256 currentTimestamp = block.timestamp; - uint256 ticksUntilEnd = stakingContract.stakingFixedEpochEndTime() - currentTimestamp; - // Ban for at least 12 full staking epochs: - // currentTimestampt + stakingFixedEpochDuration + remainingEpochDuration. - return currentTimestamp + (banDuration * stakingContract.stakingFixedEpochDuration()) + (ticksUntilEnd); + function _rewardValidatorsStandBy() internal { + address[] memory poolsToBeElected = stakingContract.getPoolsToBeElected(); + uint256 poolsLength = poolsToBeElected.length; + + for (uint256 i = 0; i < poolsLength; ++i) { + address mining = miningByStakingAddress[poolsToBeElected[i]]; + + // slither-disable-next-line incorrect-equality + if (isValidator[mining] || validatorAvailableSince[mining] == 0) { + continue; + } + + bonusScoreSystem.rewardStandBy(mining, validatorAvailableSince[mining]); + } + } + + function _penaliseValidatorsNoStandBy() internal { + address[] memory poolsInactive = stakingContract.getPoolsInactive(); + uint256 poolsLength = poolsInactive.length; + + for (uint256 i = 0; i < poolsLength; ++i) { + address mining = miningByStakingAddress[poolsInactive[i]]; + + if (validatorAvailableSince[mining] != 0) { + continue; + } + + bonusScoreSystem.penaliseNoStandBy(mining, validatorAvailableSinceLastWrite[mining]); + } } /// @dev Returns an index of a pool in the `poolsToBeElected` array @@ -1147,4 +887,16 @@ contract ValidatorSetHbbft is Initializable, OwnableUpgradeable, IValidatorSetHb } return index - 1; } + + function _validateParams(ValidatorSetParams calldata _params) private pure { + if ( + _params.blockRewardContract == address(0) || + _params.randomContract == address(0) || + _params.stakingContract == address(0) || + _params.keyGenHistoryContract == address(0) || + _params.bonusScoreContract == address(0) + ) { + revert ZeroAddress(); + } + } } diff --git a/contracts/ValueGuards.sol b/contracts/ValueGuards.sol index 6acdf774..f22888ae 100644 --- a/contracts/ValueGuards.sol +++ b/contracts/ValueGuards.sol @@ -87,6 +87,10 @@ contract ValueGuards is OwnableUpgradeable { return allowedParameterRange[bytes4(keccak256(bytes(_selector)))]; } + function getAllowedParamsRangeWithSelector(bytes4 _selector) external view returns (ParameterRange memory) { + return allowedParameterRange[_selector]; + } + // =============================================== Setters ======================================================== /** diff --git a/contracts/interfaces/IBonusScoreSystem.sol b/contracts/interfaces/IBonusScoreSystem.sol new file mode 100644 index 00000000..8fbd671b --- /dev/null +++ b/contracts/interfaces/IBonusScoreSystem.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity =0.8.25; + +enum ScoringFactor { + StandByBonus, + NoStandByPenalty, + NoKeyWritePenalty, + BadPerformancePenalty +} + +interface IBonusScoreSystem { + function getValidatorScore(address mining) external view returns (uint256); + + function rewardStandBy(address mining, uint256 time) external; + + function penaliseNoStandBy(address mining, uint256 time) external; + + function penaliseNoKeyWrite(address mining) external; + + function penaliseBadPerformance(address mining, uint256 time) external; +} diff --git a/contracts/interfaces/IConnectivityTrackerHbbft.sol b/contracts/interfaces/IConnectivityTrackerHbbft.sol index 5592125a..20867205 100644 --- a/contracts/interfaces/IConnectivityTrackerHbbft.sol +++ b/contracts/interfaces/IConnectivityTrackerHbbft.sol @@ -29,4 +29,6 @@ interface IConnectivityTrackerHbbft { ) external view; function isEarlyEpochEnd(uint256 epoch) external view returns (bool); + + function penaliseFaultyValidators(uint256 epoch) external; } diff --git a/contracts/interfaces/IStakingHbbft.sol b/contracts/interfaces/IStakingHbbft.sol index ee66f52e..0280f8b6 100644 --- a/contracts/interfaces/IStakingHbbft.sol +++ b/contracts/interfaces/IStakingHbbft.sol @@ -4,6 +4,7 @@ pragma solidity =0.8.25; interface IStakingHbbft { struct StakingParams { address _validatorSetContract; + address _bonusScoreContract; address[] _initialStakingAddresses; uint256 _delegatorMinStake; uint256 _candidateMinStake; @@ -27,6 +28,8 @@ interface IStakingHbbft { function notifyNetworkOfftimeDetected(uint256) external; + function updatePoolLikelihood(address mining, uint256 validatorScore) external; + function getPoolPublicKey(address _poolAddress) external view @@ -41,8 +44,12 @@ interface IStakingHbbft { function getPoolsToBeRemoved() external view returns (address[] memory); + function getPoolsInactive() external view returns (address[] memory); + function isPoolActive(address) external view returns (bool); + function isPoolValid(address) external view returns (bool); + function MAX_CANDIDATES() external pure returns (uint256); // solhint-disable-line func-name-mixedcase function orderedWithdrawAmount(address, address) @@ -62,6 +69,8 @@ interface IStakingHbbft { function stakeAmountTotal(address) external view returns (uint256); + function totalStakedAmount() external view returns (uint256); + function stakingWithdrawDisallowPeriod() external view returns (uint256); function stakingEpoch() external view returns (uint256); diff --git a/contracts/interfaces/IValidatorSetHbbft.sol b/contracts/interfaces/IValidatorSetHbbft.sol index 31643567..99ca98dd 100644 --- a/contracts/interfaces/IValidatorSetHbbft.sol +++ b/contracts/interfaces/IValidatorSetHbbft.sol @@ -2,6 +2,15 @@ pragma solidity =0.8.25; interface IValidatorSetHbbft { + struct ValidatorSetParams { + address blockRewardContract; + address randomContract; + address stakingContract; + address keyGenHistoryContract; + address bonusScoreContract; + uint256 validatorInactivityThreshold; + } + // Key Generation states of validator. enum KeyGenMode { NotAPendingValidator, @@ -18,16 +27,12 @@ interface IValidatorSetHbbft { function newValidatorSet() external; - function removeMaliciousValidators(address[] calldata) external; - function setStakingAddress(address, address) external; function handleFailedKeyGeneration() external; function isFullHealth() external view returns (bool); - function areDelegatorsBanned(address) external view returns (bool); - function blockRewardContract() external view returns (address); function canCallAnnounceAvailability(address _miningAddress) @@ -41,12 +46,8 @@ interface IValidatorSetHbbft { function getValidators() external view returns (address[] memory); - function isReportValidatorValid(address) external view returns (bool); - function isValidator(address) external view returns (bool); - function isValidatorBanned(address) external view returns (bool); - function isValidatorOrPending(address) external view returns (bool); function isPendingValidator(address) external view returns (bool); @@ -64,12 +65,6 @@ interface IValidatorSetHbbft { function notifyUnavailability(address) external; - function reportMaliciousCallable( - address, - address, - uint256 - ) external view returns (bool, bool); - function stakingByMiningAddress(address) external view returns (address); function publicKeyByStakingAddress(address) diff --git a/contracts/lib/Constants.sol b/contracts/lib/Constants.sol index 0398c913..2469ffc5 100644 --- a/contracts/lib/Constants.sol +++ b/contracts/lib/Constants.sol @@ -4,5 +4,4 @@ pragma solidity =0.8.25; address constant SYSTEM_ADDRESS = 0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE; uint256 constant DEFAULT_GAS_PRICE = 1 gwei; uint256 constant DEFAULT_BLOCK_GAS_LIMIT = 100_000_000; // 100 MGas block -uint256 constant MIN_BLOCK_GAS_LIMIT = 100_000_000; // uint256 constant MIN_VALIDATOR_INACTIVITY_TIME = 1 weeks; \ No newline at end of file diff --git a/contracts/lib/Errors.sol b/contracts/lib/Errors.sol index 9486d427..05ea3162 100644 --- a/contracts/lib/Errors.sol +++ b/contracts/lib/Errors.sol @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: Apache 2.0 pragma solidity =0.8.25; error Unauthorized(); diff --git a/contracts/mockContracts/BonusScoreSystemMock.sol b/contracts/mockContracts/BonusScoreSystemMock.sol new file mode 100644 index 00000000..f5c56f54 --- /dev/null +++ b/contracts/mockContracts/BonusScoreSystemMock.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity =0.8.25; + +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; + +import { IBonusScoreSystem } from "../interfaces/IBonusScoreSystem.sol"; + +contract BonusScoreSystemMock is IBonusScoreSystem { + uint256 public constant DEFAULT_STAND_BY_FACTOR = 15; + uint256 public constant DEFAULT_NO_STAND_BY_FACTOR = 15; + uint256 public constant DEFAULT_NO_KEY_WRITE_FACTOR = 100; + uint256 public constant DEFAULT_BAD_PERF_FACTOR = 100; + + uint256 public constant MIN_SCORE = 1; + uint256 public constant MAX_SCORE = 1000; + + mapping(address => uint256) public validatorScore; + + receive() external payable {} + + function rewardStandBy(address mining, uint256) external { + uint256 currentScore = validatorScore[mining]; + + validatorScore[mining] = Math.min(currentScore + DEFAULT_STAND_BY_FACTOR, MAX_SCORE); + } + + function penaliseNoStandBy(address mining, uint256) external { + uint256 currentScore = validatorScore[mining]; + + if (currentScore <= DEFAULT_NO_STAND_BY_FACTOR) { + validatorScore[mining] = MIN_SCORE; + } else { + validatorScore[mining] = currentScore - DEFAULT_NO_STAND_BY_FACTOR; + } + } + + function penaliseNoKeyWrite(address mining) external { + uint256 currentScore = validatorScore[mining]; + + if (currentScore <= DEFAULT_NO_KEY_WRITE_FACTOR) { + validatorScore[mining] = MIN_SCORE; + } else { + validatorScore[mining] = currentScore - DEFAULT_NO_KEY_WRITE_FACTOR; + } + } + + function penaliseBadPerformance(address mining, uint256) external { + uint256 currentScore = validatorScore[mining]; + + if (currentScore <= DEFAULT_BAD_PERF_FACTOR) { + validatorScore[mining] = MIN_SCORE; + } else { + validatorScore[mining] = currentScore - DEFAULT_BAD_PERF_FACTOR; + } + } + + function setValidatorScore(address mining, uint256 value) external { + validatorScore[mining] = value; + } + + function getValidatorScore(address mining) external view returns (uint256) { + return Math.max(validatorScore[mining], MIN_SCORE); + } +} diff --git a/contracts/mockContracts/ConnectivityTrackerHbbftMock.sol b/contracts/mockContracts/ConnectivityTrackerHbbftMock.sol index 3f8f6985..2624c31b 100644 --- a/contracts/mockContracts/ConnectivityTrackerHbbftMock.sol +++ b/contracts/mockContracts/ConnectivityTrackerHbbftMock.sol @@ -3,6 +3,7 @@ pragma solidity =0.8.25; contract ConnectivityTrackerHbbftMock { mapping(uint256 => bool) public earlyEpochEnd; + mapping(uint256 => bool) public epochPenaltiesSent; receive() external payable {} @@ -10,7 +11,15 @@ contract ConnectivityTrackerHbbftMock { earlyEpochEnd[epoch] = set; } + function penaliseFaultyValidators(uint256 epoch) external { + epochPenaltiesSent[epoch] = true; + } + function isEarlyEpochEnd(uint256 epoch) external view returns (bool) { return earlyEpochEnd[epoch]; } + + function isEpochPenaltiesSent(uint256 epoch) external view returns (bool) { + return epochPenaltiesSent[epoch]; + } } diff --git a/contracts/mockContracts/DaoMock.sol b/contracts/mockContracts/DaoMock.sol index 6895a862..b68b0b1a 100644 --- a/contracts/mockContracts/DaoMock.sol +++ b/contracts/mockContracts/DaoMock.sol @@ -1,13 +1,11 @@ // SPDX-License-Identifier: Apache 2.0 pragma solidity =0.8.25; - -import { IGovernancePot } from "../interfaces/IGovernancePot.sol"; +import { IGovernancePot } from "../interfaces/IGovernancePot.sol"; contract DaoMock is IGovernancePot { - uint256 public phaseCounter; - + error SwitchPhaseReverted(); function switchPhase() external { @@ -21,7 +19,5 @@ contract DaoMock is IGovernancePot { } } - receive() external payable { - - } + receive() external payable {} } diff --git a/contracts/mockContracts/ReentrancyAttacker.sol b/contracts/mockContracts/ReentrancyAttacker.sol new file mode 100644 index 00000000..14a31bd3 --- /dev/null +++ b/contracts/mockContracts/ReentrancyAttacker.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.8.25; + +import { IBonusScoreSystem } from "../interfaces/IBonusScoreSystem.sol"; + +contract ReentrancyAttacker { + IBonusScoreSystem public bonusScoreSystem; + uint256 public timeArgValue; + bytes4 public funcId; + + constructor(address _bonusScoreSystem) { + bonusScoreSystem = IBonusScoreSystem(_bonusScoreSystem); + } + + function setFuncId(bytes4 id) external { + funcId = id; + } + + function attack(address mining, uint256 time) public { + timeArgValue = time; + + if (funcId == IBonusScoreSystem.rewardStandBy.selector) { + bonusScoreSystem.rewardStandBy(mining, timeArgValue); + } else if (funcId == IBonusScoreSystem.penaliseNoStandBy.selector) { + bonusScoreSystem.penaliseNoStandBy(mining, timeArgValue); + } else if (funcId == IBonusScoreSystem.penaliseBadPerformance.selector){ + bonusScoreSystem.penaliseBadPerformance(mining, timeArgValue); + } else { + bonusScoreSystem.penaliseNoKeyWrite(mining); + } + } + + function stakingFixedEpochDuration() external pure returns (uint256) { + return 43200; + } + + function updatePoolLikelihood(address mining, uint256) external { + if (funcId == IBonusScoreSystem.rewardStandBy.selector) { + bonusScoreSystem.rewardStandBy(mining, timeArgValue); + } else if (funcId == IBonusScoreSystem.penaliseNoStandBy.selector) { + bonusScoreSystem.penaliseNoStandBy(mining, timeArgValue); + } else if (funcId == IBonusScoreSystem.penaliseBadPerformance.selector){ + bonusScoreSystem.penaliseBadPerformance(mining, timeArgValue); + } else { + bonusScoreSystem.penaliseNoKeyWrite(mining); + } + } +} \ No newline at end of file diff --git a/contracts/mockContracts/TxPermissionHbbftMock.sol b/contracts/mockContracts/TxPermissionHbbftMock.sol index b61a2e26..cb3141be 100644 --- a/contracts/mockContracts/TxPermissionHbbftMock.sol +++ b/contracts/mockContracts/TxPermissionHbbftMock.sol @@ -17,6 +17,12 @@ contract MockValidatorSet { IValidatorSetHbbft.KeyGenMode public keyGenMode; address public stakingContract; + mapping(address => bool) public isValidator; + + function setValidator(address mining, bool val) external { + isValidator[mining] = val; + } + function setKeyGenMode(IValidatorSetHbbft.KeyGenMode _mode) external { keyGenMode = _mode; } diff --git a/contracts/mockContracts/ValidatorSetHbbftMock.sol b/contracts/mockContracts/ValidatorSetHbbftMock.sol index 02ff0a56..c18dc1f6 100644 --- a/contracts/mockContracts/ValidatorSetHbbftMock.sol +++ b/contracts/mockContracts/ValidatorSetHbbftMock.sol @@ -12,13 +12,6 @@ contract ValidatorSetHbbftMock is ValidatorSetHbbft { // =============================================== Setters ======================================================== - function setBannedUntil(address _miningAddress, uint256 _bannedUntil) - public - { - bannedUntil[_miningAddress] = _bannedUntil; - bannedDelegatorsUntil[_miningAddress] = _bannedUntil; - } - function setBlockRewardContract(address _address) public { blockRewardContract = _address; } @@ -47,6 +40,38 @@ contract ValidatorSetHbbftMock is ValidatorSetHbbft { _finalizeNewValidators(); } + function setValidatorsNum(uint256 num) external { + uint256 count = _currentValidators.length; + + if (count < num) { + address validator = _currentValidators[0]; + for (uint256 i = count; i <= num; ++i) { + _currentValidators.push(validator); + } + + } else if (count > num) { + for (uint256 i = count; i > num; --i) { + _currentValidators.pop(); + } + } else { + return; + } + } + + function kickValidator(address _mining) external { + uint256 len = _currentValidators.length; + + for (uint256 i = 0; i < len; i++) { + if (_currentValidators[i] == _mining) { + // Remove the malicious validator from `_pendingValidators` + _currentValidators[i] = _currentValidators[len - 1]; + _currentValidators.pop(); + + return; + } + } + } + // =============================================== Getters ======================================================== function getRandomIndex( diff --git a/hardhat.config.ts b/hardhat.config.ts index 4662d85c..fb6a01a6 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -69,8 +69,6 @@ const config: HardhatUserConfig = { browserURL: "http://185.187.170.209:4000/", }, }, - - ], }, contractSizer: { @@ -79,13 +77,10 @@ const config: HardhatUserConfig = { disambiguatePaths: false, only: [ "Hbbft", - "Registry" + "Registry", + ":BonusScoreSystem" ], - except: [ - "Mock", - "Sacrifice", - "Base" - ] + except: ["Mock"] }, gasReporter: { currency: "USD", @@ -178,9 +173,6 @@ const config: HardhatUserConfig = { mocha: { timeout: 100000000 }, - upgrades: { - - } }; export default config; diff --git a/initial-contracts.json b/initial-contracts.json index c68c7a9c..46410b67 100644 --- a/initial-contracts.json +++ b/initial-contracts.json @@ -39,6 +39,11 @@ "name": "ConnectivityTrackerHbbft", "proxyAddress": "0x1200000000000000000000000000000000000001", "implementationAddress": "0x1200000000000000000000000000000000000000" + }, + { + "name": "BonusScoreSystem", + "proxyAddress": "0x1300000000000000000000000000000000000001", + "implementationAddress": "0x1300000000000000000000000000000000000000" } ] } \ No newline at end of file diff --git a/package.json b/package.json index 33a11c7a..3240cd37 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,8 @@ "solhint:ci": "./node_modules/.bin/solhint --disc --max-warnings 0 -f compact 'contracts/**/*.sol' > /dev/null", "solhint:sarif-report": "./node_modules/.bin/solhint --disc -f sarif 'contracts/**/*.sol' > solhint_report.sarif", "getFunctionSignatures": "node scripts/getFunctionSignatures.js", + "oz-manifest": "npx hardhat run --network hardhat scripts/generateNetworkManifest.ts", + "spec": "npm run oz-manifest && npx hardhat make_spec_hbbft --init-contracts initial-contracts.json --use-upgrade-proxy init-data.json", "typechain": "npx typechain --target ethers-v5 --out-dir src/types 'artifacts/contracts/**/!(*.dbg).json'" }, "repository": { diff --git a/slither.config.json b/slither.config.json index ca76789c..68fe3a82 100644 --- a/slither.config.json +++ b/slither.config.json @@ -5,5 +5,5 @@ "exclude_low": false, "exclude_medium": false, "exclude_high": false, - "filter_paths": "mockContracts|openzeppelin" + "filter_paths": "mockContracts|openzeppelin|upgradeability" } diff --git a/tasks/make_spec.ts b/tasks/make_spec.ts index 1d2c0408..77f6cf96 100644 --- a/tasks/make_spec.ts +++ b/tasks/make_spec.ts @@ -58,13 +58,13 @@ task("make_spec_hbbft", "used to make a spec file") initializerArgs // bytes _data ); - // example: + // example: // npx hardhat verify --network alpha3 0x1000000000000000000000000000000000000000 let implementationAddress = initialContracts.core[i].implementationAddress let proxyAddress = initialContracts.core[i].proxyAddress; blocscoutVerificationScript += `### ${contractName} ###\n`; blocscoutVerificationScript += `echo "verifying ${contractName} on ${implementationAddress}"\n`; - blocscoutVerificationScript += `npx hardhat verify --network alpha3 ${implementationAddress}\n`; + blocscoutVerificationScript += `npx hardhat verify --network alpha3 ${implementationAddress}\n`; blocscoutVerificationScript += `echo "verifying proxy for ${contractName} on ${proxyAddress}"\n`; blocscoutVerificationScript += `npx hardhat verify --network alpha3 ${proxyAddress} ${implementationAddress} ${networkConfig.owner} ${initializerDataHex}\n`; diff --git a/tasks/types.ts b/tasks/types.ts index 4a89ca13..e1046487 100644 --- a/tasks/types.ts +++ b/tasks/types.ts @@ -85,29 +85,47 @@ export class NetworkConfiguration { publicKeys[i] = publicKeys[i].trim(); } + // ethers v6 working solution + // const newParts = new Array(); + // initData.parts.forEach((x: string) => { + // newParts.push(new Uint8Array(Buffer.from(x))); + // }); + + // const newAcks = new Array>(); + // for (const ack of initData.acks) { + // const ackResults = new Array(); + + // ack.forEach((x: string) => { + // ackResults.push(new Uint8Array(Buffer.from(x))); + // }) + + // newAcks.push(ackResults); + // } + + // not working with ethers v6 const newParts: string[] = []; initData.parts.forEach((x: string) => { newParts.push( '0x' + x); }); - instance.publicKeys = fp.flatMap((x: string) => [x.substring(0, 66), '0x' + x.substring(66, 130)])(publicKeys); - instance.initialMiningAddresses = initialValidators; - instance.initialStakingAddresses = stakingAddresses; - instance.internetAddresses = internetAddresses; - instance.permittedAddresses = [instance.owner]; - - instance.parts = newParts; - let newAcks : Array> = []; + let newAcks: Array> = []; // initData.acks initData.acks.forEach((acksValidator: Array) => { - let acks : Array = []; + let acks: Array = []; acksValidator.forEach((ack: string) => { - acks.push( '0x' + ack); + acks.push('0x' + ack); }); newAcks.push(acks); }); + instance.publicKeys = fp.flatMap((x: string) => [x.substring(0, 66), '0x' + x.substring(66, 130)])(publicKeys); + instance.initialMiningAddresses = initialValidators; + instance.initialStakingAddresses = stakingAddresses; + instance.internetAddresses = internetAddresses; + instance.permittedAddresses = [instance.owner]; + + instance.parts = newParts; instance.acks = newAcks; const stakingEpochDuration = process.env.STAKING_EPOCH_DURATION; @@ -117,7 +135,6 @@ export class NetworkConfiguration { const stakingMinStakeForDelegatorString = process.env.STAKING_MIN_STAKE_FOR_DELEGATOR; const validatorInactivityThresholdString = process.env.VALIDATOR_INACTIVITY_THRESHOLD; - let stakingMinStakeForValidator = ethers.parseEther('1'); if (stakingMinStakeForValidatorString) { stakingMinStakeForValidator = ethers.parseEther(stakingMinStakeForValidatorString); @@ -135,7 +152,6 @@ export class NetworkConfiguration { let stakingMaxStakeForValidator = ethers.parseEther('50000'); - instance.stakingParams = new StakingParams({ _initialStakingAddresses: instance.initialStakingAddresses, _delegatorMinStake: stakingMinStakeForDelegator, @@ -233,7 +249,7 @@ export class CoreContract { } toSpecAccount(useUpgradeProxy: boolean, initialBalance: number) { - let spec : { [id: string] : any; } = {}; + let spec: { [id: string]: any; } = {}; if (useUpgradeProxy) { spec[this.implementationAddress!] = { @@ -287,7 +303,7 @@ export class InitialContractsConfiguration { getAddress(name: string): string | undefined { const found = this.core.find(obj => obj.name === name); - + return found ? found.proxyAddress : ethers.ZeroAddress; } @@ -299,11 +315,14 @@ export class InitialContractsConfiguration { case 'ValidatorSetHbbft': return [ config.owner, - this.getAddress('BlockRewardHbbft'), - this.getAddress('RandomHbbft'), - this.getAddress('StakingHbbft'), - this.getAddress('KeyGenHistory'), - config.validatorInactivityThreshold, + { + blockRewardContract: this.getAddress('BlockRewardHbbft'), + randomContract: this.getAddress('RandomHbbft'), + stakingContract: this.getAddress('StakingHbbft'), + keyGenHistoryContract: this.getAddress('KeyGenHistory'), + bonusScoreContract: this.getAddress('BonusScoreSystem'), + validatorInactivityThreshold: config.validatorInactivityThreshold, + }, config.initialMiningAddresses, config.initialStakingAddresses ]; @@ -346,6 +365,7 @@ export class InitialContractsConfiguration { config.owner, { _validatorSetContract: this.getAddress('ValidatorSetHbbft'), + _bonusScoreContract: this.getAddress('BonusScoreSystem'), ...config.stakingParams }, config.publicKeys, @@ -357,8 +377,16 @@ export class InitialContractsConfiguration { this.getAddress('ValidatorSetHbbft'), this.getAddress('StakingHbbft'), this.getAddress('BlockRewardHbbft'), + this.getAddress('BonusScoreSystem'), config.minReportAgeBlocks ]; + case 'BonusScoreSystem': + return [ + config.owner, + this.getAddress('ValidatorSetHbbft'), + this.getAddress('ConnectivityTrackerHbbft'), + this.getAddress('StakingHbbft'), + ]; case 'Registry': return [ this.getAddress('CertifierHbbft'), diff --git a/test/BlockRewardHbbft.ts b/test/BlockRewardHbbft.ts index 5388d33e..65c3e980 100644 --- a/test/BlockRewardHbbft.ts +++ b/test/BlockRewardHbbft.ts @@ -9,6 +9,10 @@ import { BlockRewardHbbftMock, ValidatorSetHbbftMock, StakingHbbftMock, + RandomHbbft, + KeyGenHistory, + CertifierHbbft, + TxPermissionHbbft, } from "../src/types"; import { getNValidatorsPartNAcks } from "./testhelpers/data"; @@ -27,7 +31,6 @@ const MAX_STAKE = ethers.parseEther('100000'); const SystemAccountAddress = '0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE'; - const addToDeltaPotValue = ethers.parseEther('60'); const validatorInactivityThreshold = 365n * 86400n // 1 year @@ -85,100 +88,109 @@ describe('BlockRewardHbbft', () => { contractDeployCounter = contractDeployCounter + 1; - // every second deployment we add the DAOMock contract, + // every second deployment we add the DAOMock contract, // so we also cover the possibility that no contract was deployed. await deployDao(); - + + const bonusScoreContractMockFactory = await ethers.getContractFactory("BonusScoreSystemMock"); + const bonusScoreContractMock = await bonusScoreContractMockFactory.deploy(); + await bonusScoreContractMock.waitForDeployment(); + const ConnectivityTrackerFactory = await ethers.getContractFactory("ConnectivityTrackerHbbftMock"); const connectivityTrackerContract = await ConnectivityTrackerFactory.deploy(); await connectivityTrackerContract.waitForDeployment(); + const validatorSetParams = { + blockRewardContract: stubAddress, + randomContract: stubAddress, + stakingContract: stubAddress, + keyGenHistoryContract: stubAddress, + bonusScoreContract: await bonusScoreContractMock.getAddress(), + validatorInactivityThreshold: validatorInactivityThreshold, + } + const ValidatorSetFactory = await ethers.getContractFactory("ValidatorSetHbbftMock"); - const validatorSetHbbftProxy = await upgrades.deployProxy( + const validatorSetContract = await upgrades.deployProxy( ValidatorSetFactory, [ owner.address, - stubAddress, // _blockRewardContract - stubAddress, // _randomContract - stubAddress, // _stakingContract - stubAddress, // _keyGenHistoryContract - validatorInactivityThreshold, // _validatorInactivityThreshold - initialValidators, // _initialMiningAddresses - initialStakingAddresses, // _initialStakingAddresses + validatorSetParams, // _params + initialValidators, // _initialMiningAddresses + initialStakingAddresses, // _initialStakingAddresses ], { initializer: 'initialize' } - ); + ) as unknown as ValidatorSetHbbftMock; - await validatorSetHbbftProxy.waitForDeployment(); + await validatorSetContract.waitForDeployment(); const RandomHbbftFactory = await ethers.getContractFactory("RandomHbbft"); - const randomHbbftProxy = await upgrades.deployProxy( + const randomHbbftContract = await upgrades.deployProxy( RandomHbbftFactory, [ owner.address, - await validatorSetHbbftProxy.getAddress() + await validatorSetContract.getAddress() ], { initializer: 'initialize' }, - ); + ) as unknown as RandomHbbft; - await randomHbbftProxy.waitForDeployment(); + await randomHbbftContract.waitForDeployment(); const KeyGenFactory = await ethers.getContractFactory("KeyGenHistory"); - const keyGenHistoryProxy = await upgrades.deployProxy( + const keyGenHistoryContract = await upgrades.deployProxy( KeyGenFactory, [ owner.address, - await validatorSetHbbftProxy.getAddress(), + await validatorSetContract.getAddress(), initialValidators, parts, acks ], { initializer: 'initialize' } - ); + ) as unknown as KeyGenHistory; - await keyGenHistoryProxy.waitForDeployment(); + await keyGenHistoryContract.waitForDeployment(); const CertifierFactory = await ethers.getContractFactory("CertifierHbbft"); - const certifierProxy = await upgrades.deployProxy( + const certifierContract = await upgrades.deployProxy( CertifierFactory, [ [owner.address], - await validatorSetHbbftProxy.getAddress(), + await validatorSetContract.getAddress(), owner.address ], { initializer: 'initialize' } - ); + ) as unknown as CertifierHbbft; - await certifierProxy.waitForDeployment(); + await certifierContract.waitForDeployment(); const TxPermissionFactory = await ethers.getContractFactory("TxPermissionHbbft"); - const txPermissionProxy = await upgrades.deployProxy( + const txPermissionContract = await upgrades.deployProxy( TxPermissionFactory, [ [owner.address], - await certifierProxy.getAddress(), - await validatorSetHbbftProxy.getAddress(), - await keyGenHistoryProxy.getAddress(), - stubAddress, + await certifierContract.getAddress(), + await validatorSetContract.getAddress(), + await keyGenHistoryContract.getAddress(), + await connectivityTrackerContract.getAddress(), owner.address ], { initializer: 'initialize' } - ); + ) as unknown as TxPermissionHbbft; - await txPermissionProxy.waitForDeployment(); + await txPermissionContract.waitForDeployment(); const BlockRewardHbbftFactory = await ethers.getContractFactory("BlockRewardHbbftMock"); - const blockRewardHbbftProxy = await upgrades.deployProxy( + const blockRewardContract = await upgrades.deployProxy( BlockRewardHbbftFactory, [ owner.address, - await validatorSetHbbftProxy.getAddress(), + await validatorSetContract.getAddress(), await connectivityTrackerContract.getAddress(), ], { initializer: 'initialize' } - ); + ) as unknown as BlockRewardHbbftMock; - await blockRewardHbbftProxy.waitForDeployment(); + await blockRewardContract.waitForDeployment(); // The following private keys belong to the accounts 1-3, fixed by using the "--mnemonic" option when starting ganache. // const initialValidatorsPrivKeys = ["0x272b8400a202c08e23641b53368d603e5fec5c13ea2f438bce291f7be63a02a7", "0xa8ea110ffc8fe68a069c8a460ad6b9698b09e21ad5503285f633b3ad79076cf7", "0x5da461ff1378256f69cb9a9d0a8b370c97c460acbe88f5d897cb17209f891ffc"]; @@ -194,7 +206,8 @@ describe('BlockRewardHbbft', () => { initialValidatorsIpAddresses = Array(initialValidators.length).fill(ethers.zeroPadBytes("0x00", 16)); let structure = { - _validatorSetContract: await validatorSetHbbftProxy.getAddress(), + _validatorSetContract: await validatorSetContract.getAddress(), + _bonusScoreContract: await bonusScoreContractMock.getAddress(), _initialStakingAddresses: initialStakingAddresses, _delegatorMinStake: DELEGATOR_MIN_STAKE, _candidateMinStake: MIN_STAKE, @@ -205,7 +218,7 @@ describe('BlockRewardHbbft', () => { }; const StakingHbbftFactory = await ethers.getContractFactory("StakingHbbftMock"); - const stakingHbbftProxy = await upgrades.deployProxy( + const stakingContract = await upgrades.deployProxy( StakingHbbftFactory, [ owner.address, @@ -214,26 +227,14 @@ describe('BlockRewardHbbft', () => { initialValidatorsIpAddresses // _internetAddresses ], { initializer: 'initialize' } - ); - - await stakingHbbftProxy.waitForDeployment(); + ) as unknown as StakingHbbftMock; - const validatorSetContract = ValidatorSetFactory.attach( - await validatorSetHbbftProxy.getAddress() - ) as ValidatorSetHbbftMock; - - const stakingContract = StakingHbbftFactory.attach( - await stakingHbbftProxy.getAddress() - ) as StakingHbbftMock; - - const blockRewardContract = BlockRewardHbbftFactory.attach( - await blockRewardHbbftProxy.getAddress() - ) as BlockRewardHbbftMock + await stakingContract.waitForDeployment(); await validatorSetContract.setBlockRewardContract(await blockRewardContract.getAddress()); - await validatorSetContract.setRandomContract(await randomHbbftProxy.getAddress()); + await validatorSetContract.setRandomContract(await randomHbbftContract.getAddress()); await validatorSetContract.setStakingContract(await stakingContract.getAddress()); - await validatorSetContract.setKeyGenHistoryContract(await keyGenHistoryProxy.getAddress()); + await validatorSetContract.setKeyGenHistoryContract(await keyGenHistoryContract.getAddress()); return { blockRewardContract, validatorSetContract, stakingContract, connectivityTrackerContract }; } @@ -555,45 +556,6 @@ describe('BlockRewardHbbft', () => { await helpers.stopImpersonatingAccount(SystemAccountAddress); }); - it('should not reward banned validators', async () => { - const { - blockRewardContract, - validatorSetContract, - stakingContract, - } = await helpers.loadFixture(deployContractsFixture); - - for (const _staking of initialStakingAddresses) { - const pool = await ethers.getSigner(_staking); - - await stakingContract.connect(pool).stake(pool.address, { value: candidateMinStake }); - expect(await stakingContract.stakeAmountTotal(pool.address)).to.be.eq(candidateMinStake); - } - - await callReward(blockRewardContract, true); - await callReward(blockRewardContract, true); - - const fixedEpochEndTime = await stakingHbbft.stakingFixedEpochEndTime(); - await helpers.time.increaseTo(fixedEpochEndTime + 1n); - await helpers.mine(1); - - const deltaPotValue = ethers.parseEther('10'); - await blockRewardContract.addToDeltaPot({ value: deltaPotValue }); - expect(await blockRewardContract.deltaPot()).to.be.eq(deltaPotValue); - - const now = (await ethers.provider.getBlock('latest'))!.timestamp; - - for (const validator of initialValidators) { - await validatorSetContract.setBannedUntil(validator, now + 3600); - expect(await validatorSetContract.isValidatorBanned(validator)).to.be.true; - } - - const systemSigner = await impersonateAcc(SystemAccountAddress); - await expect(blockRewardContract.connect(systemSigner).reward(true)) - .to.emit(blockRewardContract, "CoinsRewarded") - .withArgs(0n); - await helpers.stopImpersonatingAccount(SystemAccountAddress); - }); - it('should save epochs in which validator was awarded', async () => { const { blockRewardContract, @@ -926,12 +888,14 @@ describe('BlockRewardHbbft', () => { }); it("should end epoch earlier if notified", async () => { - expect(await blockRewardHbbft.setConnectivityTracker(owner.address)).to.not.be.reverted; + const connectivityTracker = await impersonateAcc(await blockRewardHbbft.connectivityTracker()); expect(await blockRewardHbbft.earlyEpochEnd()).to.be.false; - expect(await blockRewardHbbft.connect(owner).notifyEarlyEpochEnd()).to.not.be.reverted; + expect(await blockRewardHbbft.connect(connectivityTracker).notifyEarlyEpochEnd()).to.not.be.reverted; expect(await blockRewardHbbft.earlyEpochEnd()).to.be.true; + await helpers.stopImpersonatingAccount(connectivityTracker.address); + const systemSigner = await impersonateAcc(SystemAccountAddress); expect(await blockRewardHbbft.connect(systemSigner).reward(false)).to.emit( @@ -956,14 +920,10 @@ describe('BlockRewardHbbft', () => { }); it("should restrict calling notifyEarlyEpochEnd to connectivity tracker contract only", async () => { - const allowedCaller = accounts[10]; const caller = accounts[11]; - expect(await blockRewardHbbft.setConnectivityTracker(allowedCaller.address)).to.not.be.reverted; - expect(await blockRewardHbbft.connectivityTracker()).to.be.equal(allowedCaller.address); - - await expect(blockRewardHbbft.connect(caller).notifyEarlyEpochEnd()).to.be.reverted; - expect(await blockRewardHbbft.connect(allowedCaller).notifyEarlyEpochEnd()).to.not.be.reverted; + await expect(blockRewardHbbft.connect(caller).notifyEarlyEpochEnd()) + .to.be.revertedWithCustomError(blockRewardHbbft, "Unauthorized"); }); it("upscaling: add multiple validator pools and upscale if needed.", async () => { @@ -1005,14 +965,14 @@ describe('BlockRewardHbbft', () => { expect(await stakingHbbft.getPoolsToBeElected()).to.be.lengthOf(49); }) - it("upscaling: banning validator up to 16", async () => { + it("upscaling: removing validators up to 16", async () => { while ((await validatorSetHbbft.getValidators()).length > 16) { await mine(); const validators = await validatorSetHbbft.getValidators(); const systemSigner = await impersonateAcc(SystemAccountAddress); - await validatorSetHbbft.connect(systemSigner).removeMaliciousValidators([validators[13]]); + await validatorSetHbbft.connect(systemSigner).kickValidator(validators[13]); await helpers.stopImpersonatingAccount(systemSigner.address); } diff --git a/test/BonusScoreSystem.ts b/test/BonusScoreSystem.ts new file mode 100644 index 00000000..92416bd8 --- /dev/null +++ b/test/BonusScoreSystem.ts @@ -0,0 +1,990 @@ +import { ethers, upgrades } from "hardhat"; +import { expect } from "chai"; +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; +import * as helpers from "@nomicfoundation/hardhat-network-helpers"; +import fp from "lodash/fp"; + +import { BonusScoreSystem, StakingHbbft, ValidatorSetHbbftMock } from "../src/types"; + +// one epoch in 12 hours. +const STAKING_FIXED_EPOCH_DURATION = 43200n; + +// the transition time window is 30 minutes. +const STAKING_TRANSITION_WINDOW_LENGTH = 1800n; + +const MIN_SCORE = 1n; +const MAX_SCORE = 1000n; +const STAND_BY_BONUS = 15n; +const STAND_BY_PENALTY = 15n; +const NO_KEY_WRITE_PENALTY = 100n; +const BAD_PERFORMANCE_PENALTY = 100n; + +enum ScoringFactor { + StandByBonus, + NoStandByPenalty, + NoKeyWritePenalty, + BadPerformancePenalty +} + +const ScoringFactors = [ + { factor: ScoringFactor.StandByBonus, value: STAND_BY_BONUS }, + { factor: ScoringFactor.NoStandByPenalty, value: STAND_BY_PENALTY }, + { factor: ScoringFactor.NoKeyWritePenalty, value: NO_KEY_WRITE_PENALTY }, + { factor: ScoringFactor.BadPerformancePenalty, value: BAD_PERFORMANCE_PENALTY }, +]; + +describe("BonusScoreSystem", function () { + let users: HardhatEthersSigner[]; + let owner: HardhatEthersSigner; + let initialValidators: string[]; + let initialStakingAddresses: string[]; + let initialValidatorsPubKeys; + let initialValidatorsIpAddresses; + + let randomWallet = () => ethers.Wallet.createRandom().address; + + before(async function () { + users = await ethers.getSigners(); + owner = users[0]; + }); + + async function deployContracts() { + const stubAddress = users[5].address; + + initialValidators = users.slice(10, 12 + 1).map(x => x.address); // accounts[10...12] + initialStakingAddresses = users.slice(13, 15 + 1).map(x => x.address); // accounts[10...12] + + initialValidatorsPubKeys = fp.flatMap((x: string) => [x.substring(0, 66), '0x' + x.substring(66, 130)]) + ([ + '0x52be8f332b0404dff35dd0b2ba44993a9d3dc8e770b9ce19a849dff948f1e14c57e7c8219d522c1a4cce775adbee5330f222520f0afdabfdb4a4501ceeb8dcee', + '0x99edf3f524a6f73e7f5d561d0030fc6bcc3e4bd33971715617de7791e12d9bdf6258fa65b74e7161bbbf7ab36161260f56f68336a6f65599dc37e7f2e397f845', + '0xa255fd7ad199f0ee814ee00cce44ef2b1fa1b52eead5d8013ed85eade03034ae4c246658946c2e1d7ded96394a1247fb4d093c32474317ae388e8d25692a0f56' + ]); + + // The IP addresses are irrelevant for these unit test, just initialize them to 0. + initialValidatorsIpAddresses = Array(initialValidators.length).fill(ethers.zeroPadBytes("0x00", 16)); + + const validatorSetParams = { + blockRewardContract: stubAddress, + randomContract: stubAddress, + stakingContract: stubAddress, + keyGenHistoryContract: stubAddress, + bonusScoreContract: stubAddress, + validatorInactivityThreshold: 86400, + } + + const validatorSetFactory = await ethers.getContractFactory("ValidatorSetHbbftMock"); + const validatorSetHbbft = await upgrades.deployProxy( + validatorSetFactory, + [ + owner.address, + validatorSetParams, // _params + initialValidators, // _initialMiningAddresses + initialStakingAddresses, // _initialStakingAddresses + ], + { initializer: 'initialize' } + ) as unknown as ValidatorSetHbbftMock; + + await validatorSetHbbft.waitForDeployment(); + + let stakingParams = { + _validatorSetContract: await validatorSetHbbft.getAddress(), + _bonusScoreContract: stubAddress, + _initialStakingAddresses: initialStakingAddresses, + _delegatorMinStake: ethers.parseEther('100'), + _candidateMinStake: ethers.parseEther('1'), + _maxStake: ethers.parseEther('100000'), + _stakingFixedEpochDuration: STAKING_FIXED_EPOCH_DURATION, + _stakingTransitionTimeframeLength: STAKING_TRANSITION_WINDOW_LENGTH, + _stakingWithdrawDisallowPeriod: 2n + }; + + const StakingHbbftFactory = await ethers.getContractFactory("StakingHbbftMock"); + const stakingHbbft = await upgrades.deployProxy( + StakingHbbftFactory, + [ + owner.address, + stakingParams, // initializer structure + initialValidatorsPubKeys, // _publicKeys + initialValidatorsIpAddresses // _internetAddresses + ], + { initializer: 'initialize' } + ) as unknown as StakingHbbft; + + await stakingHbbft.waitForDeployment(); + + const bonusScoreSystemFactory = await ethers.getContractFactory("BonusScoreSystem"); + + const bonusScoreSystem = await upgrades.deployProxy( + bonusScoreSystemFactory, + [ + owner.address, + await validatorSetHbbft.getAddress(), // _validatorSetHbbft + randomWallet(), // _connectivityTracker + await stakingHbbft.getAddress(), // _stakingContract + ], + { initializer: 'initialize' } + ) as unknown as BonusScoreSystem; + + await bonusScoreSystem.waitForDeployment(); + + await stakingHbbft.setBonusScoreContract(await bonusScoreSystem.getAddress()); + await validatorSetHbbft.setBonusScoreSystemAddress(await bonusScoreSystem.getAddress()); + + const reentrancyAttackerFactory = await ethers.getContractFactory("ReentrancyAttacker"); + const reentrancyAttacker = await reentrancyAttackerFactory.deploy(await bonusScoreSystem.getAddress()); + await reentrancyAttacker.waitForDeployment(); + + return { bonusScoreSystem, stakingHbbft, validatorSetHbbft, reentrancyAttacker }; + } + + async function impersonateAcc(accAddress: string) { + await helpers.impersonateAccount(accAddress); + + await owner.sendTransaction({ + to: accAddress, + value: ethers.parseEther('10'), + }); + + return await ethers.getSigner(accAddress); + } + + async function getPoolLikelihood( + stakingHbbft: StakingHbbft, + stakingAddress: string + ): Promise { + const poolsToBeElected = await stakingHbbft.getPoolsToBeElected(); + const poolsLikelihood = (await stakingHbbft.getPoolsLikelihood()).likelihoods; + + const index = Number(await stakingHbbft.poolToBeElectedIndex(stakingAddress)); + if (poolsToBeElected.length <= index || poolsToBeElected[index] != stakingAddress) { + throw new Error("pool not found"); + } + + return poolsLikelihood[index]; + } + + async function increaseScore( + bonusScoreContract: BonusScoreSystem, + validator: string, + score: bigint + ) { + const timeToGetScorePoint = await bonusScoreContract.getTimePerScorePoint(ScoringFactor.StandByBonus); + const timeToGetFullBonus = timeToGetScorePoint * STAND_BY_BONUS; + + const validatorSetAddress = await bonusScoreContract.validatorSetHbbft(); + const validatorSet = await impersonateAcc(validatorSetAddress); + + let currentScore = await bonusScoreContract.getValidatorScore(validator); + + while (currentScore < score) { + let scoreDiff = score - currentScore; + let timeInterval = scoreDiff < STAND_BY_BONUS + ? scoreDiff * timeToGetScorePoint + : timeToGetFullBonus; + + const block = await ethers.provider.getBlock("latest"); + + await helpers.time.increase(timeInterval + 1n); + await bonusScoreContract.connect(validatorSet).rewardStandBy(validator, block?.timestamp!); + + currentScore = await bonusScoreContract.getValidatorScore(validator); + + if (currentScore == MAX_SCORE) { + break; + } + } + + await helpers.stopImpersonatingAccount(validatorSet.address); + } + + describe('Initializer', async () => { + let InitializeCases = [ + [ethers.ZeroAddress, randomWallet(), randomWallet(), randomWallet()], + [randomWallet(), ethers.ZeroAddress, randomWallet(), randomWallet()], + [randomWallet(), randomWallet(), ethers.ZeroAddress, randomWallet()], + [randomWallet(), randomWallet(), randomWallet(), ethers.ZeroAddress], + ]; + + InitializeCases.forEach((args, index) => { + it(`should revert initialization with zero address argument, test #${index + 1}`, async function () { + const bonusScoreSystemFactory = await ethers.getContractFactory("BonusScoreSystem"); + + await expect(upgrades.deployProxy( + bonusScoreSystemFactory, + args, + { initializer: 'initialize' } + )).to.be.revertedWithCustomError(bonusScoreSystemFactory, "ZeroAddress"); + }); + }); + + it("should not allow re-initialization", async () => { + const args = [randomWallet(), randomWallet(), randomWallet(), randomWallet()]; + + const bonusScoreSystemFactory = await ethers.getContractFactory("BonusScoreSystem"); + const bonusScoreSystem = await upgrades.deployProxy( + bonusScoreSystemFactory, + args, + { initializer: 'initialize' } + ); + + await bonusScoreSystem.waitForDeployment(); + + await expect( + bonusScoreSystem.initialize(...args) + ).to.be.revertedWithCustomError(bonusScoreSystem, "InvalidInitialization"); + }); + + ScoringFactors.forEach((args) => { + it(`should set initial scoring factor ${ScoringFactor[args.factor]}`, async () => { + const bonusScoreSystemFactory = await ethers.getContractFactory("BonusScoreSystem"); + const bonusScoreSystem = await upgrades.deployProxy( + bonusScoreSystemFactory, + [randomWallet(), randomWallet(), randomWallet(), randomWallet()], + { initializer: 'initialize' } + ); + + await bonusScoreSystem.waitForDeployment(); + + expect(await bonusScoreSystem.getScoringFactorValue(args.factor)).to.equal(args.value); + }); + }); + }); + + describe('updateScoringFactor', async () => { + const TestCases = [ + { factor: ScoringFactor.StandByBonus, value: 20 }, + { factor: ScoringFactor.NoStandByPenalty, value: 50 }, + { factor: ScoringFactor.NoKeyWritePenalty, value: 200 }, + { factor: ScoringFactor.BadPerformancePenalty, value: 199 }, + ]; + + it('should restrict calling to contract owner', async function () { + const { bonusScoreSystem } = await helpers.loadFixture(deployContracts); + const caller = users[2]; + + await expect(bonusScoreSystem.connect(caller).updateScoringFactor(ScoringFactor.StandByBonus, 1)) + .to.be.revertedWithCustomError(bonusScoreSystem, "OwnableUnauthorizedAccount") + .withArgs(caller.address); + }); + + it('should not allow zero factor value', async function () { + const { bonusScoreSystem } = await helpers.loadFixture(deployContracts); + + await expect(bonusScoreSystem.updateScoringFactor(ScoringFactor.StandByBonus, 0)) + .to.be.revertedWithCustomError(bonusScoreSystem, "ZeroFactorValue"); + }); + + TestCases.forEach((args) => { + it(`should set scoring factor ${ScoringFactor[args.factor]} and emit event`, async function () { + const { bonusScoreSystem } = await helpers.loadFixture(deployContracts); + + await expect( + bonusScoreSystem.updateScoringFactor(args.factor, args.value) + ).to.emit(bonusScoreSystem, "UpdateScoringFactor") + .withArgs(args.factor, args.value); + + expect(await bonusScoreSystem.getScoringFactorValue(args.factor)).to.equal(args.value); + }); + }); + }); + + describe('setStakingContract', async () => { + it('should restrict calling to contract owner', async function () { + const { bonusScoreSystem } = await helpers.loadFixture(deployContracts); + const caller = users[2]; + + await expect(bonusScoreSystem.connect(caller).setStakingContract(randomWallet())) + .to.be.revertedWithCustomError(bonusScoreSystem, "OwnableUnauthorizedAccount") + .withArgs(caller.address); + }); + + it('should not set zero contract address', async function () { + const { bonusScoreSystem } = await helpers.loadFixture(deployContracts); + + await expect( + bonusScoreSystem.setStakingContract(ethers.ZeroAddress) + ).to.be.revertedWithCustomError(bonusScoreSystem, "ZeroAddress"); + }); + + it('should set Staking contract address and emit event', async function () { + const { bonusScoreSystem } = await helpers.loadFixture(deployContracts); + const _staking = randomWallet(); + + await expect( + bonusScoreSystem.setStakingContract(_staking) + ).to.emit(bonusScoreSystem, "SetStakingContract").withArgs(_staking); + + expect(await bonusScoreSystem.stakingHbbft()).to.equal(_staking); + }); + }); + + describe('setValidatorSetContract', async () => { + it('should restrict calling to contract owner', async function () { + const { bonusScoreSystem } = await helpers.loadFixture(deployContracts); + const caller = users[2]; + + await expect(bonusScoreSystem.connect(caller).setValidatorSetContract(randomWallet())) + .to.be.revertedWithCustomError(bonusScoreSystem, "OwnableUnauthorizedAccount") + .withArgs(caller.address); + }); + + it('should not set zero contract address', async function () { + const { bonusScoreSystem } = await helpers.loadFixture(deployContracts); + + await expect( + bonusScoreSystem.setValidatorSetContract(ethers.ZeroAddress) + ).to.be.revertedWithCustomError(bonusScoreSystem, "ZeroAddress"); + }); + + it('should set ValidatorSet contract address and emit event', async function () { + const { bonusScoreSystem } = await helpers.loadFixture(deployContracts); + const _validatorSet = randomWallet(); + + await expect( + bonusScoreSystem.setValidatorSetContract(_validatorSet) + ).to.emit(bonusScoreSystem, "SetValidatorSetContract").withArgs(_validatorSet); + + expect(await bonusScoreSystem.validatorSetHbbft()).to.equal(_validatorSet); + }); + }); + + describe('setConnectivityTrackerContract', async () => { + it('should restrict calling to contract owner', async function () { + const { bonusScoreSystem } = await helpers.loadFixture(deployContracts); + const caller = users[2]; + + await expect(bonusScoreSystem.connect(caller).setConnectivityTrackerContract(randomWallet())) + .to.be.revertedWithCustomError(bonusScoreSystem, "OwnableUnauthorizedAccount") + .withArgs(caller.address); + }); + + it('should not set zero contract address', async function () { + const { bonusScoreSystem } = await helpers.loadFixture(deployContracts); + + await expect( + bonusScoreSystem.setConnectivityTrackerContract(ethers.ZeroAddress) + ).to.be.revertedWithCustomError(bonusScoreSystem, "ZeroAddress"); + }); + + it('should set ConnectivityTracker contract address and emit event', async function () { + const { bonusScoreSystem } = await helpers.loadFixture(deployContracts); + const _connectivityTracker = randomWallet(); + + await expect( + bonusScoreSystem.setConnectivityTrackerContract(_connectivityTracker) + ).to.emit(bonusScoreSystem, "SetConnectivityTrackerContract").withArgs(_connectivityTracker); + + expect(await bonusScoreSystem.connectivityTracker()).to.equal(_connectivityTracker); + }); + }); + + describe('getScoringFactorValue', async () => { + it('should revert for unknown scoring factor', async function () { + const { bonusScoreSystem } = await helpers.loadFixture(deployContracts); + + const unknownFactor = ScoringFactor.BadPerformancePenalty + 1; + + await expect( + bonusScoreSystem.getScoringFactorValue(unknownFactor) + ).to.be.reverted; + }); + + it('should get scoring factor value', async function () { + const { bonusScoreSystem } = await helpers.loadFixture(deployContracts); + + expect(await bonusScoreSystem.getScoringFactorValue(ScoringFactor.BadPerformancePenalty)) + .to.equal(await bonusScoreSystem.DEFAULT_BAD_PERF_FACTOR()); + }); + }); + + describe('getTimePerScorePoint', async () => { + ScoringFactors.forEach((args) => { + it(`should get time per ${ScoringFactor[args.factor]} factor point`, async () => { + const { bonusScoreSystem, stakingHbbft } = await helpers.loadFixture(deployContracts); + const fixedEpochDuration = await stakingHbbft.stakingFixedEpochDuration(); + + const expected = fixedEpochDuration / args.value; + + expect(await bonusScoreSystem.getTimePerScorePoint(args.factor)).to.equal(expected); + }); + }); + }); + + describe('getValidatorScore', async () => { + it('should return MIN_SCORE if not previously recorded', async function () { + const { bonusScoreSystem } = await helpers.loadFixture(deployContracts); + + const validator = ethers.Wallet.createRandom().address; + + expect(await bonusScoreSystem.getValidatorScore(validator)).to.equal(MIN_SCORE); + }); + }); + + describe('rewardStandBy', async () => { + it('should restrict calling to ValidatorSet contract', async function () { + const { bonusScoreSystem } = await helpers.loadFixture(deployContracts); + const caller = users[2]; + + await expect(bonusScoreSystem.connect(caller).rewardStandBy(randomWallet(), 100)) + .to.be.revertedWithCustomError(bonusScoreSystem, "Unauthorized"); + }); + + it('should revert for availability timestamp in the future', async function () { + const { bonusScoreSystem } = await helpers.loadFixture(deployContracts); + + const validator = initialValidators[0]; + const availableSince = (await ethers.provider.getBlock('latest'))?.timestamp!; + + const validatorSet = await impersonateAcc(await bonusScoreSystem.validatorSetHbbft()); + await expect(bonusScoreSystem.connect(validatorSet).rewardStandBy(validator, availableSince + 5)) + .to.be.revertedWithCustomError(bonusScoreSystem, "InvalidIntervalStartTimestamp"); + + await helpers.stopImpersonatingAccount(validatorSet.address); + }); + + it('should increase validator score depending on stand by interval', async function () { + const { bonusScoreSystem } = await helpers.loadFixture(deployContracts); + + const validator = initialValidators[0]; + const availableSince = (await ethers.provider.getBlock('latest'))?.timestamp!; + + expect(await bonusScoreSystem.getValidatorScore(validator)).to.equal(MIN_SCORE); + + const standByTime = 6n * 60n * 60n // 6 hours + + const timePerPoint = await bonusScoreSystem.getTimePerScorePoint(ScoringFactor.StandByBonus); + const expectedScore = standByTime / timePerPoint + MIN_SCORE; + + await helpers.time.increase(standByTime); + + const validatorSet = await impersonateAcc(await bonusScoreSystem.validatorSetHbbft()); + expect(await bonusScoreSystem.connect(validatorSet).rewardStandBy(validator, availableSince)); + expect(await bonusScoreSystem.getValidatorScore(validator)).to.equal(expectedScore); + + await helpers.stopImpersonatingAccount(validatorSet.address); + }); + + it('should emit event', async function () { + const { bonusScoreSystem } = await helpers.loadFixture(deployContracts); + + const validator = initialValidators[0]; + const availableSince = (await ethers.provider.getBlock('latest'))?.timestamp!; + + expect(await bonusScoreSystem.getValidatorScore(validator)).to.equal(MIN_SCORE); + + const standByTime = 1n * 60n * 60n // 1 hours + + const timePerPoint = await bonusScoreSystem.getTimePerScorePoint(ScoringFactor.StandByBonus); + const expectedScore = standByTime / timePerPoint + MIN_SCORE; + + await helpers.time.increase(standByTime); + + const validatorSet = await impersonateAcc(await bonusScoreSystem.validatorSetHbbft()); + await expect(bonusScoreSystem.connect(validatorSet).rewardStandBy(validator, availableSince)) + .to.emit(bonusScoreSystem, "ValidatorScoreChanged") + .withArgs(validator, ScoringFactor.StandByBonus, expectedScore); + + await helpers.stopImpersonatingAccount(validatorSet.address); + }); + + it('should not exceed MAX_SCORE', async function () { + const { bonusScoreSystem, stakingHbbft } = await helpers.loadFixture(deployContracts); + + const validator = initialValidators[0]; + const availableSince = (await ethers.provider.getBlock('latest'))?.timestamp!; + + const initialScore = MAX_SCORE - 2n; + await increaseScore(bonusScoreSystem, validator, initialScore); + expect(await bonusScoreSystem.getValidatorScore(validator)).to.equal(initialScore); + + const standByTime = await stakingHbbft.stakingFixedEpochDuration(); + + const validatorSet = await impersonateAcc(await bonusScoreSystem.validatorSetHbbft()); + + await helpers.time.increase(standByTime + 1n); + + expect(await bonusScoreSystem.connect(validatorSet).rewardStandBy(validator, availableSince)); + expect(await bonusScoreSystem.getValidatorScore(validator)).to.equal(MAX_SCORE); + + await helpers.stopImpersonatingAccount(validatorSet.address); + }); + + it('should use last score change timestamp if its higher than availability timestamp', async function () { + const { bonusScoreSystem } = await helpers.loadFixture(deployContracts); + + const validator = initialValidators[0]; + const availableSince = (await ethers.provider.getBlock('latest'))?.timestamp!; + + expect(await bonusScoreSystem.getValidatorScore(validator)).to.equal(MIN_SCORE); + + let standByTime = 6n * 60n * 60n // 6 hours + + const timePerPoint = await bonusScoreSystem.getTimePerScorePoint(ScoringFactor.StandByBonus); + const expectedScore = standByTime / timePerPoint + MIN_SCORE; + + await helpers.time.increase(standByTime); + + const validatorSet = await impersonateAcc(await bonusScoreSystem.validatorSetHbbft()); + expect(await bonusScoreSystem.connect(validatorSet).rewardStandBy(validator, availableSince)); + expect(await bonusScoreSystem.getValidatorScore(validator)).to.equal(expectedScore); + + const additionalPoints = 5n; + standByTime = timePerPoint * additionalPoints; // time to accumulate 5 stand by points + + await helpers.time.increase(standByTime); + expect(await bonusScoreSystem.connect(validatorSet).rewardStandBy(validator, availableSince)); + expect(await bonusScoreSystem.getValidatorScore(validator)).to.equal(expectedScore + additionalPoints); + + await helpers.stopImpersonatingAccount(validatorSet.address); + }); + + it('should increase pool likelihood', async function () { + const { bonusScoreSystem, stakingHbbft } = await helpers.loadFixture(deployContracts); + + const mining = await ethers.getSigner(initialValidators[0]); + const staking = await ethers.getSigner(initialStakingAddresses[0]); + const canidateStake = await stakingHbbft.candidateMinStake(); + + const availableSince = (await ethers.provider.getBlock('latest'))?.timestamp!; + + expect(await bonusScoreSystem.getValidatorScore(mining.address)).to.equal(MIN_SCORE); + + await stakingHbbft.connect(staking).stake(staking.address, { + value: canidateStake + }); + + expect(await getPoolLikelihood(stakingHbbft, staking.address)).to.equal(canidateStake); + + const standByTime = 1n * 60n * 60n // 1 hours + + const timePerPoint = await bonusScoreSystem.getTimePerScorePoint(ScoringFactor.StandByBonus); + const expectedScore = standByTime / timePerPoint + MIN_SCORE; + + await helpers.time.increase(standByTime); + + const validatorSet = await impersonateAcc(await bonusScoreSystem.validatorSetHbbft()); + expect(await bonusScoreSystem.connect(validatorSet).rewardStandBy(mining.address, availableSince)); + expect(await bonusScoreSystem.getValidatorScore(mining.address)).to.equal(expectedScore); + + expect(await getPoolLikelihood(stakingHbbft, staking.address)).to.equal(canidateStake * expectedScore); + + await helpers.stopImpersonatingAccount(validatorSet.address); + }); + + it('should be non-reentrant', async () => { + const { bonusScoreSystem, reentrancyAttacker } = await helpers.loadFixture(deployContracts); + + const selector = bonusScoreSystem.interface.getFunction('rewardStandBy').selector; + + await bonusScoreSystem.setStakingContract(await reentrancyAttacker.getAddress()); + await bonusScoreSystem.setValidatorSetContract(await reentrancyAttacker.getAddress()); + + await reentrancyAttacker.setFuncId(selector); + + const mining = await ethers.getSigner(initialValidators[0]); + const timestamp = await helpers.time.latest(); + + await helpers.time.increase(1000); + + await expect(reentrancyAttacker.attack(mining, timestamp)) + .to.be.revertedWithCustomError(bonusScoreSystem, "ReentrancyGuardReentrantCall"); + }); + }); + + describe('penaliseNoStandBy', async () => { + it('should restrict calling to ValidatorSet contract', async function () { + const { bonusScoreSystem } = await helpers.loadFixture(deployContracts); + const caller = users[3]; + + await expect(bonusScoreSystem.connect(caller).penaliseNoStandBy(randomWallet(), 100)) + .to.be.revertedWithCustomError(bonusScoreSystem, "Unauthorized"); + }); + + it('should revert for availability timestamp in the future', async function () { + const { bonusScoreSystem } = await helpers.loadFixture(deployContracts); + + const validator = initialValidators[0]; + const availableSince = (await ethers.provider.getBlock('latest'))?.timestamp!; + + const validatorSet = await impersonateAcc(await bonusScoreSystem.validatorSetHbbft()); + await expect(bonusScoreSystem.connect(validatorSet).penaliseNoStandBy(validator, availableSince + 5)) + .to.be.revertedWithCustomError(bonusScoreSystem, "InvalidIntervalStartTimestamp"); + + await helpers.stopImpersonatingAccount(validatorSet.address); + }); + + it('should decrease validator score depending on no stand by interval', async function () { + const { bonusScoreSystem } = await helpers.loadFixture(deployContracts); + + const validator = initialValidators[0]; + + const scoreBefore = 110n; + await increaseScore(bonusScoreSystem, validator, scoreBefore); + expect(await bonusScoreSystem.getValidatorScore(validator)).to.equal(scoreBefore); + + const unavailableSince = (await ethers.provider.getBlock('latest'))?.timestamp!; + const noStandByTime = 6n * 60n * 60n // 6 hours + + const timePerPoint = await bonusScoreSystem.getTimePerScorePoint(ScoringFactor.NoStandByPenalty); + const scorePenalty = noStandByTime / timePerPoint; + + await helpers.time.increase(noStandByTime); + + const validatorSet = await impersonateAcc(await bonusScoreSystem.validatorSetHbbft()); + expect(await bonusScoreSystem.connect(validatorSet).penaliseNoStandBy(validator, unavailableSince)); + expect(await bonusScoreSystem.getValidatorScore(validator)).to.equal(scoreBefore - scorePenalty); + + await helpers.stopImpersonatingAccount(validatorSet.address); + }); + + it('should emit event', async function () { + const { bonusScoreSystem } = await helpers.loadFixture(deployContracts); + + const validator = initialValidators[0]; + const scoreBefore = 110n; + await increaseScore(bonusScoreSystem, validator, scoreBefore); + expect(await bonusScoreSystem.getValidatorScore(validator)).to.equal(scoreBefore); + + const unavailableSince = (await ethers.provider.getBlock('latest'))?.timestamp!; + const noStandByTime = 1n * 60n * 60n // 1 hours + + const timePerPoint = await bonusScoreSystem.getTimePerScorePoint(ScoringFactor.NoStandByPenalty); + const scoreAfter = scoreBefore - noStandByTime / timePerPoint; + + await helpers.time.increase(noStandByTime); + + const validatorSet = await impersonateAcc(await bonusScoreSystem.validatorSetHbbft()); + await expect(bonusScoreSystem.connect(validatorSet).penaliseNoStandBy(validator, unavailableSince)) + .to.emit(bonusScoreSystem, "ValidatorScoreChanged") + .withArgs(validator, ScoringFactor.NoStandByPenalty, scoreAfter); + + await helpers.stopImpersonatingAccount(validatorSet.address); + }); + + it('should not decrease below MIN_SCORE', async function () { + const { bonusScoreSystem } = await helpers.loadFixture(deployContracts); + + const validator = initialValidators[0]; + + const initialScore = MIN_SCORE + 1n; + await increaseScore(bonusScoreSystem, validator, initialScore); + expect(await bonusScoreSystem.getValidatorScore(validator)).to.equal(initialScore); + + const unavailableSince = (await ethers.provider.getBlock('latest'))?.timestamp!; + const noStandByTime = 12n * 60n * 60n // 12 hours + + await helpers.time.increase(noStandByTime); + + const validatorSet = await impersonateAcc(await bonusScoreSystem.validatorSetHbbft()); + expect(await bonusScoreSystem.connect(validatorSet).penaliseNoStandBy(validator, unavailableSince)); + expect(await bonusScoreSystem.getValidatorScore(validator)).to.equal(MIN_SCORE); + + await helpers.stopImpersonatingAccount(validatorSet.address); + }); + + it('should use last score change timestamp if its higher than availability timestamp', async function () { + const { bonusScoreSystem } = await helpers.loadFixture(deployContracts); + + const validator = initialValidators[0]; + const initialScore = 250n; + await increaseScore(bonusScoreSystem, validator, initialScore); + expect(await bonusScoreSystem.getValidatorScore(validator)).to.equal(initialScore); + + const unavailableSince = (await ethers.provider.getBlock('latest'))?.timestamp!; + let noStandByTime = 10n * 60n * 60n // 10 hours + + const timePerPoint = await bonusScoreSystem.getTimePerScorePoint(ScoringFactor.NoStandByPenalty); + const expectedScore = initialScore - noStandByTime / timePerPoint; + + await helpers.time.increase(noStandByTime); + + const validatorSet = await impersonateAcc(await bonusScoreSystem.validatorSetHbbft()); + expect(await bonusScoreSystem.connect(validatorSet).penaliseNoStandBy(validator, unavailableSince)); + expect(await bonusScoreSystem.getValidatorScore(validator)).to.equal(expectedScore); + + const additionalPenatlies = 5n; + noStandByTime = timePerPoint * additionalPenatlies; // time to accumulate 5 no stand by points + + await helpers.time.increase(noStandByTime); + expect(await bonusScoreSystem.connect(validatorSet).penaliseNoStandBy(validator, unavailableSince)); + expect(await bonusScoreSystem.getValidatorScore(validator)).to.equal(expectedScore - additionalPenatlies); + + await helpers.stopImpersonatingAccount(validatorSet.address); + }); + + it('should decrease pool likelihood', async function () { + const { bonusScoreSystem, stakingHbbft } = await helpers.loadFixture(deployContracts); + + const mining = await ethers.getSigner(initialValidators[0]); + const staking = await ethers.getSigner(initialStakingAddresses[0]); + const canidateStake = await stakingHbbft.candidateMinStake(); + + await stakingHbbft.connect(staking).stake(staking.address, { + value: canidateStake + }); + + const initialScore = 250n; + await increaseScore(bonusScoreSystem, mining.address, initialScore); + expect(await bonusScoreSystem.getValidatorScore(mining.address)).to.equal(initialScore); + expect(await getPoolLikelihood(stakingHbbft, staking.address)).to.equal(canidateStake * initialScore); + + const unavailableSince = (await ethers.provider.getBlock('latest'))?.timestamp!; + const noStandByTime = 10n * 60n * 60n // 10 hours + + const timePerPoint = await bonusScoreSystem.getTimePerScorePoint(ScoringFactor.NoStandByPenalty); + const expectedScore = initialScore - noStandByTime / timePerPoint; + + await helpers.time.increase(noStandByTime); + + const validatorSet = await impersonateAcc(await bonusScoreSystem.validatorSetHbbft()); + + expect(await bonusScoreSystem.connect(validatorSet).penaliseNoStandBy(mining.address, unavailableSince)); + expect(await bonusScoreSystem.getValidatorScore(mining.address)).to.equal(expectedScore); + expect(await getPoolLikelihood(stakingHbbft, staking.address)).to.equal(canidateStake * expectedScore); + + await helpers.stopImpersonatingAccount(validatorSet.address); + }); + + it('should be non-reentrant', async () => { + const { bonusScoreSystem, reentrancyAttacker } = await helpers.loadFixture(deployContracts); + + const selector = bonusScoreSystem.interface.getFunction('penaliseNoStandBy').selector; + + await bonusScoreSystem.setStakingContract(await reentrancyAttacker.getAddress()); + await bonusScoreSystem.setValidatorSetContract(await reentrancyAttacker.getAddress()); + + await reentrancyAttacker.setFuncId(selector); + + const mining = await ethers.getSigner(initialValidators[0]); + const timestamp = await helpers.time.latest(); + + await helpers.time.increase(1000); + + await expect(reentrancyAttacker.attack(mining, timestamp)) + .to.be.revertedWithCustomError(bonusScoreSystem, "ReentrancyGuardReentrantCall"); + }); + }); + + describe('penaliseNoKeyWrite', async () => { + it('should restrict calling to ValidatorSet contract', async function () { + const { bonusScoreSystem } = await helpers.loadFixture(deployContracts); + const caller = users[4]; + + await expect(bonusScoreSystem.connect(caller).penaliseNoKeyWrite(randomWallet())) + .to.be.revertedWithCustomError(bonusScoreSystem, "Unauthorized"); + }); + + it('should decrease validator score', async function () { + const { bonusScoreSystem } = await helpers.loadFixture(deployContracts); + + const validator = initialValidators[0]; + + const scoreBefore = 110n; + await increaseScore(bonusScoreSystem, validator, scoreBefore); + expect(await bonusScoreSystem.getValidatorScore(validator)).to.equal(scoreBefore); + + const expectedScore = scoreBefore - NO_KEY_WRITE_PENALTY; + + const validatorSet = await impersonateAcc(await bonusScoreSystem.validatorSetHbbft()); + expect(await bonusScoreSystem.connect(validatorSet).penaliseNoKeyWrite(validator)); + expect(await bonusScoreSystem.getValidatorScore(validator)).to.equal(expectedScore); + + await helpers.stopImpersonatingAccount(validatorSet.address); + }); + + it('should not decrease below MIN_SCORE', async function () { + const { bonusScoreSystem } = await helpers.loadFixture(deployContracts); + + const validator = initialValidators[0]; + + const scoreBefore = 100n; + await increaseScore(bonusScoreSystem, validator, scoreBefore); + expect(await bonusScoreSystem.getValidatorScore(validator)).to.equal(scoreBefore); + + const validatorSet = await impersonateAcc(await bonusScoreSystem.validatorSetHbbft()); + expect(await bonusScoreSystem.connect(validatorSet).penaliseNoKeyWrite(validator)); + expect(await bonusScoreSystem.getValidatorScore(validator)).to.equal(MIN_SCORE); + + await helpers.stopImpersonatingAccount(validatorSet.address); + }); + + it('should emit event', async function () { + const { bonusScoreSystem } = await helpers.loadFixture(deployContracts); + + const validator = initialValidators[0]; + + const scoreBefore = 110n; + await increaseScore(bonusScoreSystem, validator, scoreBefore); + expect(await bonusScoreSystem.getValidatorScore(validator)).to.equal(scoreBefore); + + const expectedScore = scoreBefore - NO_KEY_WRITE_PENALTY; + + const validatorSet = await impersonateAcc(await bonusScoreSystem.validatorSetHbbft()); + + await expect(bonusScoreSystem.connect(validatorSet).penaliseNoKeyWrite(validator)) + .to.emit(bonusScoreSystem, "ValidatorScoreChanged") + .withArgs(validator, ScoringFactor.NoKeyWritePenalty, expectedScore); + + await helpers.stopImpersonatingAccount(validatorSet.address); + }); + + it('should decrease pool likelihood', async function () { + const { bonusScoreSystem, stakingHbbft } = await helpers.loadFixture(deployContracts); + + const mining = await ethers.getSigner(initialValidators[0]); + const staking = await ethers.getSigner(initialStakingAddresses[0]); + const canidateStake = await stakingHbbft.candidateMinStake(); + + await stakingHbbft.connect(staking).stake(staking.address, { + value: canidateStake + }); + + const bonusScoreBefore = 110n; + const bonusScoreAfter = bonusScoreBefore - NO_KEY_WRITE_PENALTY; + await increaseScore(bonusScoreSystem, mining.address, bonusScoreBefore); + + expect(await bonusScoreSystem.getValidatorScore(mining.address)).to.equal(bonusScoreBefore); + expect(await getPoolLikelihood(stakingHbbft, staking.address)).to.equal(canidateStake * bonusScoreBefore); + + const validatorSet = await impersonateAcc(await bonusScoreSystem.validatorSetHbbft()); + expect(await bonusScoreSystem.connect(validatorSet).penaliseNoKeyWrite(mining.address)); + + const stakeAmount = await stakingHbbft.stakeAmountTotal(staking.address); + expect(await getPoolLikelihood(stakingHbbft, staking.address)).to.equal(stakeAmount * bonusScoreAfter); + + await helpers.stopImpersonatingAccount(validatorSet.address); + }); + + it('should be non-reentrant', async () => { + const { bonusScoreSystem, reentrancyAttacker } = await helpers.loadFixture(deployContracts); + + const selector = bonusScoreSystem.interface.getFunction('penaliseNoKeyWrite').selector; + + await bonusScoreSystem.setStakingContract(await reentrancyAttacker.getAddress()); + await bonusScoreSystem.setValidatorSetContract(await reentrancyAttacker.getAddress()); + + await reentrancyAttacker.setFuncId(selector); + + const mining = await ethers.getSigner(initialValidators[0]); + const timestamp = await helpers.time.latest(); + + await helpers.time.increase(1000); + + await expect(reentrancyAttacker.attack(mining, timestamp)) + .to.be.revertedWithCustomError(bonusScoreSystem, "ReentrancyGuardReentrantCall"); + }); + }); + + describe('penaliseBadPerformance', async () => { + it('should restrict calling to ConnectivityTracker contract', async function () { + const { bonusScoreSystem } = await helpers.loadFixture(deployContracts); + const caller = users[5]; + + await expect(bonusScoreSystem.connect(caller).penaliseBadPerformance(randomWallet(), 100)) + .to.be.revertedWithCustomError(bonusScoreSystem, "Unauthorized"); + }); + + it('should decrease validator score depending on disconnect interval', async function () { + const { bonusScoreSystem } = await helpers.loadFixture(deployContracts); + + const validator = initialValidators[0]; + + const scoreBefore = 150n; + await increaseScore(bonusScoreSystem, validator, scoreBefore); + expect(await bonusScoreSystem.getValidatorScore(validator)).to.equal(scoreBefore); + + const lostPoints = 60n; + const timePerPoint = await bonusScoreSystem.getTimePerScorePoint(ScoringFactor.BadPerformancePenalty); + const disconnectInterval = lostPoints * timePerPoint; + + const connectivityTracker = await impersonateAcc(await bonusScoreSystem.connectivityTracker()); + expect(await bonusScoreSystem.connect(connectivityTracker).penaliseBadPerformance(validator, disconnectInterval)); + expect(await bonusScoreSystem.getValidatorScore(validator)).to.equal(scoreBefore - lostPoints); + + await helpers.stopImpersonatingAccount(connectivityTracker.address); + }); + + it('should fully penalise for bad performance', async function () { + const { bonusScoreSystem } = await helpers.loadFixture(deployContracts); + + const validator = initialValidators[0]; + + const scoreBefore = 150n; + const scoreAfter = scoreBefore - BAD_PERFORMANCE_PENALTY; + await increaseScore(bonusScoreSystem, validator, scoreBefore); + expect(await bonusScoreSystem.getValidatorScore(validator)).to.equal(scoreBefore); + + const connectivityTracker = await impersonateAcc(await bonusScoreSystem.connectivityTracker()); + expect(await bonusScoreSystem.connect(connectivityTracker).penaliseBadPerformance(validator, 0)); + expect(await bonusScoreSystem.getValidatorScore(validator)).to.equal(scoreAfter); + + await helpers.stopImpersonatingAccount(connectivityTracker.address); + }); + + it('should emit event', async function () { + const { bonusScoreSystem } = await helpers.loadFixture(deployContracts); + + const validator = initialValidators[0]; + + const scoreBefore = 150n; + const scoreAfter = scoreBefore - BAD_PERFORMANCE_PENALTY; + await increaseScore(bonusScoreSystem, validator, scoreBefore); + expect(await bonusScoreSystem.getValidatorScore(validator)).to.equal(scoreBefore); + + const connectivityTracker = await impersonateAcc(await bonusScoreSystem.connectivityTracker()); + await expect(bonusScoreSystem.connect(connectivityTracker).penaliseBadPerformance(validator, 0)) + .to.emit(bonusScoreSystem, "ValidatorScoreChanged") + .withArgs(validator, ScoringFactor.BadPerformancePenalty, scoreAfter); + + + await helpers.stopImpersonatingAccount(connectivityTracker.address); + }); + + it('should decrease pool likelihood', async function () { + const { bonusScoreSystem, stakingHbbft } = await helpers.loadFixture(deployContracts); + + const mining = await ethers.getSigner(initialValidators[0]); + const staking = await ethers.getSigner(initialStakingAddresses[0]); + const canidateStake = await stakingHbbft.candidateMinStake(); + + await stakingHbbft.connect(staking).stake(staking.address, { + value: canidateStake + }); + + const bonusScoreBefore = 210n; + const bonusScoreAfter = bonusScoreBefore - BAD_PERFORMANCE_PENALTY; + await increaseScore(bonusScoreSystem, mining.address, bonusScoreBefore); + + expect(await bonusScoreSystem.getValidatorScore(mining.address)).to.equal(bonusScoreBefore); + expect(await getPoolLikelihood(stakingHbbft, staking.address)).to.equal(canidateStake * bonusScoreBefore); + + const connectivityTracker = await impersonateAcc(await bonusScoreSystem.connectivityTracker()); + expect(await bonusScoreSystem.connect(connectivityTracker).penaliseBadPerformance(mining.address, 0)); + + const stakeAmount = await stakingHbbft.stakeAmountTotal(staking.address); + expect(await getPoolLikelihood(stakingHbbft, staking.address)).to.equal(stakeAmount * bonusScoreAfter); + + await helpers.stopImpersonatingAccount(connectivityTracker.address); + }); + + it('should be non-reentrant', async () => { + const { bonusScoreSystem, reentrancyAttacker } = await helpers.loadFixture(deployContracts); + + const selector = bonusScoreSystem.interface.getFunction('penaliseBadPerformance').selector; + + await bonusScoreSystem.setStakingContract(await reentrancyAttacker.getAddress()); + await bonusScoreSystem.setConnectivityTrackerContract(await reentrancyAttacker.getAddress()); + + await reentrancyAttacker.setFuncId(selector); + + const mining = await ethers.getSigner(initialValidators[0]); + const timestamp = await helpers.time.latest(); + + await helpers.time.increase(1000); + + await expect(reentrancyAttacker.attack(mining, timestamp)) + .to.be.revertedWithCustomError(bonusScoreSystem, "ReentrancyGuardReentrantCall"); + }); + }); +}); diff --git a/test/CertifierHbbft.ts b/test/CertifierHbbft.ts index d061cbe0..29f2e37d 100644 --- a/test/CertifierHbbft.ts +++ b/test/CertifierHbbft.ts @@ -19,16 +19,21 @@ describe('CertifierHbbft contract', () => { async function deployContracts() { const stubAddress = accounts[1].address; + const validatorSetParams = { + blockRewardContract: stubAddress, + randomContract: stubAddress, + stakingContract: stubAddress, + keyGenHistoryContract: stubAddress, + bonusScoreContract: stubAddress, + validatorInactivityThreshold: validatorInactivityThreshold, + } + const validatorSetFactory = await ethers.getContractFactory("ValidatorSetHbbftMock"); const validatorSetHbbftProxy = await upgrades.deployProxy( validatorSetFactory, [ owner.address, - stubAddress, // _blockRewardContract - stubAddress, // _randomContract - stubAddress, // _stakingContract - stubAddress, // _keyGenHistoryContract - validatorInactivityThreshold, // _validatorInactivityThreshold + validatorSetParams, // _params initialValidators, // _initialMiningAddresses initialStakingAddresses, // _initialStakingAddresses ], diff --git a/test/ConnectivityTrackerHbbft.ts b/test/ConnectivityTrackerHbbft.ts index 4a4707b4..56b56215 100644 --- a/test/ConnectivityTrackerHbbft.ts +++ b/test/ConnectivityTrackerHbbft.ts @@ -9,6 +9,7 @@ import { ValidatorSetHbbftMock, StakingHbbftMock, BlockRewardHbbftMock, + KeyGenHistory, } from "../src/types"; import { getNValidatorsPartNAcks } from "./testhelpers/data"; @@ -43,39 +44,49 @@ describe('ConnectivityTrackerHbbft', () => { const { parts, acks } = getNValidatorsPartNAcks(initialValidators.length); + const bonusScoreContractMockFactory = await ethers.getContractFactory("BonusScoreSystemMock"); + const bonusScoreContractMock = await bonusScoreContractMockFactory.deploy(); + await bonusScoreContractMock.waitForDeployment(); + + const validatorSetParams = { + blockRewardContract: stubAddress, + randomContract: stubAddress, + stakingContract: stubAddress, + keyGenHistoryContract: stubAddress, + bonusScoreContract: await bonusScoreContractMock.getAddress(), + validatorInactivityThreshold: validatorInactivityThreshold, + } + const validatorSetFactory = await ethers.getContractFactory("ValidatorSetHbbftMock"); - const validatorSetHbbftProxy = await upgrades.deployProxy( + const validatorSetHbbft = await upgrades.deployProxy( validatorSetFactory, [ owner.address, - stubAddress, // _blockRewardContract - stubAddress, // _randomContract - stubAddress, // _stakingContract - stubAddress, // _keyGenHistoryContract - validatorInactivityThreshold, // _validatorInactivityThreshold + validatorSetParams, // _params validatorAddresses, // _initialMiningAddresses stakingAddresses, // _initialStakingAddresses ], { initializer: 'initialize' } - ); + ) as unknown as ValidatorSetHbbftMock; - await validatorSetHbbftProxy.waitForDeployment(); + await validatorSetHbbft.waitForDeployment(); const keyGenFactoryFactory = await ethers.getContractFactory("KeyGenHistory"); const keyGenHistory = await upgrades.deployProxy( keyGenFactoryFactory, [ owner.address, - await validatorSetHbbftProxy.getAddress(), + await validatorSetHbbft.getAddress(), validatorAddresses, parts, acks ], { initializer: 'initialize' } - ); + ) as unknown as KeyGenHistory; let stakingParams = { - _validatorSetContract: await validatorSetHbbftProxy.getAddress(), + _validatorSetContract: await validatorSetHbbft.getAddress(), + _bonusScoreContract: await bonusScoreContractMock.getAddress(), _initialStakingAddresses: stakingAddresses, _delegatorMinStake: ethers.parseEther('1'), _candidateMinStake: ethers.parseEther('10'), @@ -97,7 +108,7 @@ describe('ConnectivityTrackerHbbft', () => { (initialValidatorsPubKeys); const stakingHbbftFactory = await ethers.getContractFactory("StakingHbbftMock"); - const stakingHbbftProxy = await upgrades.deployProxy( + const stakingHbbft = await upgrades.deployProxy( stakingHbbftFactory, [ owner.address, @@ -107,73 +118,62 @@ describe('ConnectivityTrackerHbbft', () => { ], { initializer: 'initialize' } - ); + ) as unknown as StakingHbbftMock; - await stakingHbbftProxy.waitForDeployment(); + await stakingHbbft.waitForDeployment(); const blockRewardHbbftFactory = await ethers.getContractFactory("BlockRewardHbbftMock"); - const blockRewardHbbftProxy = await upgrades.deployProxy( + const blockRewardHbbft = await upgrades.deployProxy( blockRewardHbbftFactory, [ owner.address, - await validatorSetHbbftProxy.getAddress(), + await validatorSetHbbft.getAddress(), stubAddress ], { initializer: 'initialize' } - ); + ) as unknown as BlockRewardHbbftMock; - await blockRewardHbbftProxy.waitForDeployment(); + await blockRewardHbbft.waitForDeployment(); const connectivityTrackerFactory = await ethers.getContractFactory("ConnectivityTrackerHbbft"); - const connectivityTrackerProxy = await upgrades.deployProxy( + const connectivityTracker = await upgrades.deployProxy( connectivityTrackerFactory, [ owner.address, - await validatorSetHbbftProxy.getAddress(), - await stakingHbbftProxy.getAddress(), - await blockRewardHbbftProxy.getAddress(), + await validatorSetHbbft.getAddress(), + await stakingHbbft.getAddress(), + await blockRewardHbbft.getAddress(), + await bonusScoreContractMock.getAddress(), minReportAgeBlocks, ], { initializer: 'initialize' } - ); - - await connectivityTrackerProxy.waitForDeployment(); - - const validatorSetHbbft = validatorSetFactory.attach( - await validatorSetHbbftProxy.getAddress() - ) as ValidatorSetHbbftMock; - - const stakingHbbft = stakingHbbftFactory.attach( - await stakingHbbftProxy.getAddress() - ) as StakingHbbftMock; + ) as unknown as ConnectivityTrackerHbbft; - const blockRewardHbbft = blockRewardHbbftFactory.attach( - await blockRewardHbbftProxy.getAddress() - ) as BlockRewardHbbftMock; - - const connectivityTracker = connectivityTrackerFactory.attach( - await connectivityTrackerProxy.getAddress() - ) as ConnectivityTrackerHbbft; + await connectivityTracker.waitForDeployment(); await blockRewardHbbft.setConnectivityTracker(await connectivityTracker.getAddress()); await validatorSetHbbft.setStakingContract(await stakingHbbft.getAddress()); await validatorSetHbbft.setBlockRewardContract(await blockRewardHbbft.getAddress()); await validatorSetHbbft.setKeyGenHistoryContract(await keyGenHistory.getAddress()); - return { connectivityTracker, validatorSetHbbft, stakingHbbft, blockRewardHbbft }; + return { connectivityTracker, validatorSetHbbft, stakingHbbft, blockRewardHbbft, bonusScoreContractMock }; } - async function setStakingEpochStartTime(caller: string, stakingHbbft: StakingHbbftMock) { - await helpers.impersonateAccount(caller); + async function impersonateAcc(accAddress: string) { + await helpers.impersonateAccount(accAddress); await owner.sendTransaction({ - to: caller, + to: accAddress, value: ethers.parseEther('10'), }); - const latest = await helpers.time.latest(); - const signer = await ethers.getSigner(caller); + return await ethers.getSigner(accAddress); + } + async function setStakingEpochStartTime(caller: string, stakingHbbft: StakingHbbftMock) { + const signer = await impersonateAcc(caller); + + const latest = await helpers.time.latest(); expect(await stakingHbbft.connect(signer).setStakingEpochStartTime(latest)); await helpers.stopImpersonatingAccount(caller); @@ -191,10 +191,11 @@ describe('ConnectivityTrackerHbbft', () => { stubAddress, stubAddress, stubAddress, + stubAddress, minReportAgeBlocks ], { initializer: 'initialize' } - )).to.be.revertedWithCustomError(ConnectivityTrackerFactory, "InvalidAddress"); + )).to.be.revertedWithCustomError(ConnectivityTrackerFactory, "ZeroAddress"); }); it("should revert if validator set contract = address(0)", async () => { @@ -208,10 +209,11 @@ describe('ConnectivityTrackerHbbft', () => { ethers.ZeroAddress, stubAddress, stubAddress, + stubAddress, minReportAgeBlocks ], { initializer: 'initialize' } - )).to.be.revertedWithCustomError(ConnectivityTrackerFactory, "InvalidAddress"); + )).to.be.revertedWithCustomError(ConnectivityTrackerFactory, "ZeroAddress"); }); it("should revert if staking contract = address(0)", async () => { @@ -225,10 +227,11 @@ describe('ConnectivityTrackerHbbft', () => { stubAddress, ethers.ZeroAddress, stubAddress, + stubAddress, minReportAgeBlocks ], { initializer: 'initialize' } - )).to.be.revertedWithCustomError(ConnectivityTrackerFactory, "InvalidAddress"); + )).to.be.revertedWithCustomError(ConnectivityTrackerFactory, "ZeroAddress"); }); it("should revert if block reward contract = address(0)", async () => { @@ -242,10 +245,29 @@ describe('ConnectivityTrackerHbbft', () => { stubAddress, stubAddress, ethers.ZeroAddress, + stubAddress, minReportAgeBlocks ], { initializer: 'initialize' } - )).to.be.revertedWithCustomError(ConnectivityTrackerFactory, "InvalidAddress"); + )).to.be.revertedWithCustomError(ConnectivityTrackerFactory, "ZeroAddress"); + }); + + it("should revert if block bonus score contract = address(0)", async () => { + const stubAddress = addresses[1]; + + const ConnectivityTrackerFactory = await ethers.getContractFactory("ConnectivityTrackerHbbft"); + await expect(upgrades.deployProxy( + ConnectivityTrackerFactory, + [ + owner.address, + stubAddress, + stubAddress, + stubAddress, + ethers.ZeroAddress, + minReportAgeBlocks + ], + { initializer: 'initialize' } + )).to.be.revertedWithCustomError(ConnectivityTrackerFactory, "ZeroAddress"); }); it("should revert double initialization", async () => { @@ -259,6 +281,7 @@ describe('ConnectivityTrackerHbbft', () => { stubAddress, stubAddress, stubAddress, + stubAddress, minReportAgeBlocks ], { initializer: 'initialize' } @@ -271,6 +294,7 @@ describe('ConnectivityTrackerHbbft', () => { stubAddress, stubAddress, stubAddress, + stubAddress, minReportAgeBlocks )).to.be.revertedWithCustomError(contract, "InvalidInitialization"); }); @@ -470,7 +494,7 @@ describe('ConnectivityTrackerHbbft', () => { .to.equal(previousScore + 1n); }); - it("should increase validator score with each report", async () => { + it("should increase validator connectivity score with each report", async () => { const { connectivityTracker, stakingHbbft } = await helpers.loadFixture(deployContracts); const validator = initialValidators[0]; @@ -494,44 +518,6 @@ describe('ConnectivityTrackerHbbft', () => { expect(await connectivityTracker.getValidatorConnectivityScore(epoch, validator.address)) .to.equal(initialValidators.length - 1); }); - - it("should early epoch end = true with sufficient reports", async () => { - const { connectivityTracker, stakingHbbft, blockRewardHbbft } = await helpers.loadFixture(deployContracts); - - const goodValidatorsCount = Math.floor(initialValidators.length * 2 / 3 + 1); - - const goodValidators = initialValidators.slice(0, goodValidatorsCount); - const badValidators = initialValidators.slice(goodValidatorsCount); - - const epoch = 5; - await stakingHbbft.setStakingEpoch(epoch); - const latestBlock = await ethers.provider.getBlock("latest"); - - expect(await connectivityTracker.isEarlyEpochEnd(epoch)).to.equal(false); - - for (let i = 0; i < goodValidators.length; ++i) { - for (let j = 0; j < badValidators.length - 1; ++j) { - expect(await connectivityTracker.connect(goodValidators[i]).reportMissingConnectivity( - badValidators[j].address, - latestBlock!.number, - latestBlock!.hash! - )); - } - } - - const lastBlock = await helpers.time.latestBlock(); - - const lastValidatorToReport = goodValidators[goodValidators.length - 1]; - await expect(connectivityTracker.connect(lastValidatorToReport).reportMissingConnectivity( - badValidators[badValidators.length - 1].address, - latestBlock!.number, - latestBlock!.hash! - )).to.emit(connectivityTracker, "NotifyEarlyEpochEnd") - .withArgs(epoch, lastBlock + 1); - - expect(await connectivityTracker.isEarlyEpochEnd(epoch)).to.equal(true); - expect(await blockRewardHbbft.earlyEpochEnd()).to.equal(true); - }); }); describe('reportReconnect', async () => { @@ -689,7 +675,7 @@ describe('ConnectivityTrackerHbbft', () => { expect(await connectivityTracker.getFlaggedValidators()).to.not.include(validator.address); }); - it("should decrease validator connectivity score if report reconnected", async () => { + it("should decrease validator connectivity score if reported reconnect", async () => { const { connectivityTracker, stakingHbbft } = await helpers.loadFixture(deployContracts); const latestBlock = await ethers.provider.getBlock("latest"); const validator = initialValidators[2]; @@ -716,5 +702,377 @@ describe('ConnectivityTrackerHbbft', () => { expect(currentScore).to.equal(previousScore - 1n); }); + + it("should send bad performance penalty after faulty validator full reconnect", async () => { + const { connectivityTracker, stakingHbbft, bonusScoreContractMock } = await helpers.loadFixture(deployContracts); + + const [badValidator, ...goodValidators] = initialValidators; + const reportsThreshold = Math.floor(goodValidators.length * 2 / 3 + 1); + + const epoch = 5; + await stakingHbbft.setStakingEpoch(epoch); + let latestBlock = await ethers.provider.getBlock("latest"); + + expect(await connectivityTracker.isFaultyValidator(badValidator.address, epoch)).to.be.false; + + for (let i = 0; i < reportsThreshold; ++i) { + await connectivityTracker.connect(goodValidators[i]).reportMissingConnectivity( + badValidator.address, + latestBlock!.number, + latestBlock!.hash! + ); + } + + expect(await connectivityTracker.isFaultyValidator(badValidator.address, epoch)).to.be.true; + + const initialScore = 250n; + await bonusScoreContractMock.setValidatorScore(badValidator.address, initialScore); + + const expectedScore = initialScore - await bonusScoreContractMock.DEFAULT_BAD_PERF_FACTOR(); + + latestBlock = await ethers.provider.getBlock("latest"); + + for (let i = 0; i < reportsThreshold; ++i) { + await connectivityTracker.connect(goodValidators[i]).reportReconnect( + badValidator.address, + latestBlock!.number, + latestBlock!.hash! + ); + } + + expect(await connectivityTracker.isFaultyValidator(badValidator.address, epoch)).to.be.false; + expect(await connectivityTracker.getValidatorConnectivityScore(epoch, badValidator.address)).to.equal(0n); + expect(await bonusScoreContractMock.getValidatorScore(badValidator.address)).to.equal(expectedScore); + }); + }); + + describe('penaliseFaultyValidators', async () => { + it("should restrict calling to BlockReward contract", async () => { + const { connectivityTracker } = await helpers.loadFixture(deployContracts); + + const caller = accounts[0]; + + await expect( + connectivityTracker.connect(caller).penaliseFaultyValidators(0) + ).to.be.revertedWithCustomError(connectivityTracker, "Unauthorized"); + }); + + it("should not send penalties twice for same epoch", async () => { + const { connectivityTracker, stakingHbbft, blockRewardHbbft } = await helpers.loadFixture(deployContracts); + + const epoch = 5; + await stakingHbbft.setStakingEpoch(epoch); + + const signer = await impersonateAcc(await blockRewardHbbft.getAddress()); + + expect(await connectivityTracker.connect(signer).penaliseFaultyValidators(epoch)); + + await expect(connectivityTracker.connect(signer).penaliseFaultyValidators(epoch)) + .to.be.revertedWithCustomError(connectivityTracker, "EpochPenaltiesAlreadySent") + .withArgs(epoch); + + await helpers.stopImpersonatingAccount(signer.address); + }); + + it("should penalise faulty validators", async () => { + const { + connectivityTracker, + stakingHbbft, + blockRewardHbbft, + bonusScoreContractMock + } = await helpers.loadFixture(deployContracts); + + const badValidatorsCount = Math.floor(initialValidators.length / 4); + + const badValidators = initialValidators.slice(0, badValidatorsCount); + const goodValidators = initialValidators.slice(badValidatorsCount); + + const reportsThreshold = Math.floor(goodValidators.length * 2 / 3 + 1); + + const epoch = 5; + await stakingHbbft.setStakingEpoch(epoch); + const latestBlock = await ethers.provider.getBlock("latest"); + + for (const badValidator of badValidators) { + for (let j = 0; j < reportsThreshold; ++j) { + await connectivityTracker.connect(goodValidators[j]).reportMissingConnectivity( + badValidator.address, + latestBlock!.number, + latestBlock!.hash! + ); + } + } + + const initialScore = 205n; + const scoreAfter = initialScore - 100n; + + for (const badValidator of badValidators) { + await bonusScoreContractMock.setValidatorScore(badValidator.address, initialScore); + } + + const signer = await impersonateAcc(await blockRewardHbbft.getAddress()); + + expect(await connectivityTracker.connect(signer).penaliseFaultyValidators(epoch)); + + for (const badValidator of badValidators) { + expect(await bonusScoreContractMock.getValidatorScore(badValidator.address)) + .to.equal(scoreAfter); + } + + await helpers.stopImpersonatingAccount(signer.address); + }); + + it("should not penalise flagged but non faulty validators", async () => { + const { + connectivityTracker, + stakingHbbft, + blockRewardHbbft, + bonusScoreContractMock + } = await helpers.loadFixture(deployContracts); + + const badValidatorsCount = Math.floor(initialValidators.length / 4); + + const badValidators = initialValidators.slice(0, badValidatorsCount); + const goodValidators = initialValidators.slice(badValidatorsCount); + + const reportsThreshold = Math.floor(goodValidators.length * 2 / 3 + 1); + + const epoch = 5; + await stakingHbbft.setStakingEpoch(epoch); + const latestBlock = await ethers.provider.getBlock("latest"); + + for (const badValidator of badValidators) { + for (let j = 0; j < reportsThreshold - 2; ++j) { + await connectivityTracker.connect(goodValidators[j]).reportMissingConnectivity( + badValidator.address, + latestBlock!.number, + latestBlock!.hash! + ); + } + } + + const initialScore = 205n; + + for (const badValidator of badValidators) { + await bonusScoreContractMock.setValidatorScore(badValidator.address, initialScore); + } + + const signer = await impersonateAcc(await blockRewardHbbft.getAddress()); + expect(await connectivityTracker.connect(signer).penaliseFaultyValidators(epoch)); + + for (const badValidator of badValidators) { + expect(await bonusScoreContractMock.getValidatorScore(badValidator.address)) + .to.equal(initialScore); + } + + await helpers.stopImpersonatingAccount(signer.address); + }); + }); + + describe('countFaultyValidators', async () => { + it("should count faulty validators", async function () { + const { connectivityTracker, stakingHbbft } = await helpers.loadFixture(deployContracts); + + const badValidatorsCount = Math.floor(initialValidators.length / 4); + + const badValidators = initialValidators.slice(0, badValidatorsCount); + const goodValidators = initialValidators.slice(badValidatorsCount); + + const reportsThreshold = Math.floor(goodValidators.length * 2 / 3 + 1); + + const epoch = 5; + await stakingHbbft.setStakingEpoch(epoch); + const latestBlock = await ethers.provider.getBlock("latest"); + + for (const badValidator of badValidators) { + for (let j = 0; j < reportsThreshold; ++j) { + await connectivityTracker.connect(goodValidators[j]).reportMissingConnectivity( + badValidator.address, + latestBlock!.number, + latestBlock!.hash! + ); + } + } + + expect(await connectivityTracker.countFaultyValidators(epoch)).to.equal(badValidatorsCount); + }); + + it("should return 0 if validators reported but not faulty", async function () { + const { connectivityTracker, stakingHbbft } = await helpers.loadFixture(deployContracts); + + const badValidatorsCount = Math.floor(initialValidators.length / 4); + + const badValidators = initialValidators.slice(0, badValidatorsCount); + const goodValidators = initialValidators.slice(badValidatorsCount); + + const reportsThreshold = Math.floor(goodValidators.length * 2 / 3 + 1); + + const epoch = 5; + await stakingHbbft.setStakingEpoch(epoch); + const latestBlock = await ethers.provider.getBlock("latest"); + + for (const badValidator of badValidators) { + for (let j = 0; j < reportsThreshold - 1; ++j) { + await connectivityTracker.connect(goodValidators[j]).reportMissingConnectivity( + badValidator.address, + latestBlock!.number, + latestBlock!.hash! + ); + } + } + + expect(await connectivityTracker.countFaultyValidators(epoch)).to.equal(0n); + }); + }); + + describe('isReported', async () => { + it("should check if validator reported", async function () { + const { connectivityTracker, stakingHbbft } = await helpers.loadFixture(deployContracts); + + const [badValidator, ...goodValidators] = initialValidators; + const reportsThreshold = Math.floor(goodValidators.length * 2 / 3 + 1); + + const epoch = 5; + await stakingHbbft.setStakingEpoch(epoch); + let latestBlock = await ethers.provider.getBlock("latest"); + + expect(await connectivityTracker.isFaultyValidator(badValidator.address, epoch)).to.be.false; + + for (let i = 0; i < reportsThreshold - 1; ++i) { + await connectivityTracker.connect(goodValidators[i]).reportMissingConnectivity( + badValidator.address, + latestBlock!.number, + latestBlock!.hash! + ); + + expect( + await connectivityTracker.isReported(epoch, badValidator.address, goodValidators[i].address) + ).to.be.true; + } + }); + }); + + describe('earlyEpochEndThreshold', async () => { + let EpochEndTriggers = [ + { hbbftFaultTolerance: 0, networkSize: 1, threshold: 0 }, + { hbbftFaultTolerance: 0, networkSize: 2, threshold: 0 }, + { hbbftFaultTolerance: 0, networkSize: 3, threshold: 0 }, + { hbbftFaultTolerance: 1, networkSize: 4, threshold: 0 }, + { hbbftFaultTolerance: 2, networkSize: 7, threshold: 0 }, + { hbbftFaultTolerance: 3, networkSize: 10, threshold: 1 }, + { hbbftFaultTolerance: 4, networkSize: 13, threshold: 2 }, + { hbbftFaultTolerance: 5, networkSize: 16, threshold: 3 }, + { hbbftFaultTolerance: 6, networkSize: 19, threshold: 4 }, + { hbbftFaultTolerance: 7, networkSize: 22, threshold: 5 }, + { hbbftFaultTolerance: 8, networkSize: 25, threshold: 6 }, + ]; + + EpochEndTriggers.forEach((args) => { + it(`should get epoch end threshold for hbbft fault tolerance: ${args.hbbftFaultTolerance}, network size: ${args.networkSize}`, async () => { + const { connectivityTracker, validatorSetHbbft } = await helpers.loadFixture(deployContracts); + + await validatorSetHbbft.setValidatorsNum(args.networkSize); + expect(await validatorSetHbbft.getCurrentValidatorsCount()).to.equal(args.networkSize); + + expect(await connectivityTracker.earlyEpochEndThreshold()).to.equal(args.threshold); + }); + }); + }); + + describe('early epoch end', async () => { + it("should set early epoch end = true with sufficient reports", async () => { + const { connectivityTracker, stakingHbbft, blockRewardHbbft } = await helpers.loadFixture(deployContracts); + + const badValidatorsCount = Math.floor(initialValidators.length / 4); + + const badValidators = initialValidators.slice(0, badValidatorsCount); + const goodValidators = initialValidators.slice(badValidatorsCount); + + const reportsThreshold = Math.floor(goodValidators.length * 2 / 3 + 1); + + const epoch = 5; + await stakingHbbft.setStakingEpoch(epoch); + const latestBlock = await ethers.provider.getBlock("latest"); + + expect(await connectivityTracker.isEarlyEpochEnd(epoch)).to.equal(false); + + for (let i = 0; i < badValidators.length; ++i) { + for (let j = 0; j < reportsThreshold; ++j) { + if (i == badValidators.length - 1 && j == reportsThreshold - 1) { + break; + } + + await connectivityTracker.connect(goodValidators[j]).reportMissingConnectivity( + badValidators[i].address, + latestBlock!.number, + latestBlock!.hash! + ); + } + } + + const lastBlock = await helpers.time.latestBlock(); + + const lastReporter = goodValidators[reportsThreshold - 1]; + await expect(connectivityTracker.connect(lastReporter).reportMissingConnectivity( + badValidators[badValidators.length - 1].address, + latestBlock!.number, + latestBlock!.hash! + )).to.emit(connectivityTracker, "NotifyEarlyEpochEnd") + .withArgs(epoch, lastBlock + 1); + + expect(await connectivityTracker.isEarlyEpochEnd(epoch)).to.equal(true); + expect(await blockRewardHbbft.earlyEpochEnd()).to.equal(true); + }); + + it("should skip check for current epoch if early end already set", async () => { + const { connectivityTracker, stakingHbbft, blockRewardHbbft } = await helpers.loadFixture(deployContracts); + + const badValidatorsCount = Math.floor(initialValidators.length / 4); + + const badValidators = initialValidators.slice(0, badValidatorsCount); + const goodValidators = initialValidators.slice(badValidatorsCount); + + const reportsThreshold = Math.floor(goodValidators.length * 2 / 3 + 1); + + const epoch = 5; + await stakingHbbft.setStakingEpoch(epoch); + const latestBlock = await ethers.provider.getBlock("latest"); + + expect(await connectivityTracker.isEarlyEpochEnd(epoch)).to.equal(false); + + for (let i = 0; i < badValidators.length; ++i) { + for (let j = 0; j < reportsThreshold; ++j) { + if (i == badValidators.length - 1 && j == reportsThreshold - 1) { + break; + } + + await connectivityTracker.connect(goodValidators[j]).reportMissingConnectivity( + badValidators[i].address, + latestBlock!.number, + latestBlock!.hash! + ); + } + } + + const lastBlock = await helpers.time.latestBlock(); + + let reporter = goodValidators[reportsThreshold - 1]; + await expect(connectivityTracker.connect(reporter).reportMissingConnectivity( + badValidators[badValidators.length - 1].address, + latestBlock!.number, + latestBlock!.hash! + )).to.emit(connectivityTracker, "NotifyEarlyEpochEnd") + .withArgs(epoch, lastBlock + 1); + + expect(await connectivityTracker.isEarlyEpochEnd(epoch)).to.equal(true); + expect(await blockRewardHbbft.earlyEpochEnd()).to.equal(true); + + reporter = goodValidators[reportsThreshold]; + await expect(connectivityTracker.connect(reporter).reportMissingConnectivity( + badValidators[badValidators.length - 1].address, + latestBlock!.number, + latestBlock!.hash! + )).to.not.emit(connectivityTracker, "NotifyEarlyEpochEnd"); + }); }); }); diff --git a/test/KeyGenHistory.ts b/test/KeyGenHistory.ts index ba798ec5..d7f5b7a3 100644 --- a/test/KeyGenHistory.ts +++ b/test/KeyGenHistory.ts @@ -16,6 +16,7 @@ import { import { getTestPartNAcks } from './testhelpers/data'; import { Permission } from "./testhelpers/Permission"; +import { deployDao } from "./testhelpers/daoDeployment"; const logOutput = false; @@ -214,56 +215,68 @@ describe('KeyGenHistory', () => { console.log('initial Staking Addresses', initializingStakingAddresses); } + await deployDao(); + + const bonusScoreContractMockFactory = await ethers.getContractFactory("BonusScoreSystemMock"); + const bonusScoreContractMock = await bonusScoreContractMockFactory.deploy(); + await bonusScoreContractMock.waitForDeployment(); + const ConnectivityTrackerFactory = await ethers.getContractFactory("ConnectivityTrackerHbbftMock"); const connectivityTracker = await ConnectivityTrackerFactory.deploy(); await connectivityTracker.waitForDeployment(); + const validatorSetParams = { + blockRewardContract: stubAddress, + randomContract: stubAddress, + stakingContract: stubAddress, + keyGenHistoryContract: stubAddress, + bonusScoreContract: await bonusScoreContractMock.getAddress(), + validatorInactivityThreshold: validatorInactivityThreshold, + } + // Deploy ValidatorSet contract const ValidatorSetFactory = await ethers.getContractFactory("ValidatorSetHbbftMock"); - const validatorSetHbbftProxy = await upgrades.deployProxy( + validatorSetHbbft = await upgrades.deployProxy( ValidatorSetFactory, [ owner.address, - stubAddress, // _blockRewardContract - stubAddress, // _randomContract - stubAddress, // _stakingContract - stubAddress, // _keyGenHistoryContract - validatorInactivityThreshold, // _validatorInactivityThreshold + validatorSetParams, // _params initializingMiningAddresses, // _initialMiningAddresses initializingStakingAddresses, // _initialStakingAddresses ], { initializer: 'initialize' } - ); + ) as unknown as ValidatorSetHbbftMock; - await validatorSetHbbftProxy.waitForDeployment(); + await validatorSetHbbft.waitForDeployment(); const BlockRewardHbbftFactory = await ethers.getContractFactory("BlockRewardHbbftMock"); - const blockRewardHbbftProxy = await upgrades.deployProxy( + blockRewardHbbft = await upgrades.deployProxy( BlockRewardHbbftFactory, [ owner.address, - await validatorSetHbbftProxy.getAddress(), + await validatorSetHbbft.getAddress(), await connectivityTracker.getAddress() ], { initializer: 'initialize' } - ); + ) as unknown as BlockRewardHbbftMock; - await blockRewardHbbftProxy.waitForDeployment(); + await blockRewardHbbft.waitForDeployment(); const RandomHbbftFactory = await ethers.getContractFactory("RandomHbbft"); - const randomHbbftProxy = await upgrades.deployProxy( + randomHbbft = await upgrades.deployProxy( RandomHbbftFactory, [ owner.address, - await validatorSetHbbftProxy.getAddress() + await validatorSetHbbft.getAddress() ], { initializer: 'initialize' } - ); + ) as unknown as RandomHbbft; - await randomHbbftProxy.waitForDeployment(); + await randomHbbft.waitForDeployment(); const stakingParams = { - _validatorSetContract: await validatorSetHbbftProxy.getAddress(), + _validatorSetContract: await validatorSetHbbft.getAddress(), + _bonusScoreContract: await bonusScoreContractMock.getAddress(), _initialStakingAddresses: initializingStakingAddresses, _delegatorMinStake: delegatorMinStake, _candidateMinStake: candidateMinStake, @@ -274,7 +287,7 @@ describe('KeyGenHistory', () => { }; const StakingHbbftFactory = await ethers.getContractFactory("StakingHbbftMock"); - const stakingHbbftProxy = await upgrades.deployProxy( + stakingHbbft = await upgrades.deployProxy( StakingHbbftFactory, [ owner.address, @@ -283,79 +296,53 @@ describe('KeyGenHistory', () => { initialValidatorsIpAddresses ], { initializer: 'initialize' } - ); + ) as unknown as StakingHbbftMock; - await stakingHbbftProxy.waitForDeployment(); + await stakingHbbft.waitForDeployment(); const KeyGenFactory = await ethers.getContractFactory("KeyGenHistory"); - const keyGenHistoryProxy = await upgrades.deployProxy( + keyGenHistory = await upgrades.deployProxy( KeyGenFactory, [ owner.address, - await validatorSetHbbftProxy.getAddress(), + await validatorSetHbbft.getAddress(), initializingMiningAddresses, parts, acks ], { initializer: 'initialize' } - ); + ) as unknown as KeyGenHistory; - await keyGenHistoryProxy.waitForDeployment(); + await keyGenHistory.waitForDeployment(); const CertifierFactory = await ethers.getContractFactory("CertifierHbbft"); - const certifierProxy = await upgrades.deployProxy( + certifier = await upgrades.deployProxy( CertifierFactory, [ [owner.address], - await validatorSetHbbftProxy.getAddress(), + await validatorSetHbbft.getAddress(), owner.address ], { initializer: 'initialize' } - ); + ) as unknown as CertifierHbbft; - await certifierProxy.waitForDeployment() + await certifier.waitForDeployment() const TxPermissionFactory = await ethers.getContractFactory("TxPermissionHbbft"); - const txPermissionProxy = await upgrades.deployProxy( + txPermission = await upgrades.deployProxy( TxPermissionFactory, [ [owner.address], - await certifierProxy.getAddress(), - await validatorSetHbbftProxy.getAddress(), - await keyGenHistoryProxy.getAddress(), + await certifier.getAddress(), + await validatorSetHbbft.getAddress(), + await keyGenHistory.getAddress(), stubAddress, owner.address ], { initializer: 'initialize' } - ); - - await txPermissionProxy.waitForDeployment(); - - validatorSetHbbft = ValidatorSetFactory.attach( - await validatorSetHbbftProxy.getAddress() - ) as ValidatorSetHbbftMock; - - blockRewardHbbft = BlockRewardHbbftFactory.attach( - await blockRewardHbbftProxy.getAddress() - ) as BlockRewardHbbftMock; - - randomHbbft = RandomHbbftFactory.attach(await randomHbbftProxy.getAddress()) as RandomHbbft; - - stakingHbbft = StakingHbbftFactory.attach( - await stakingHbbftProxy.getAddress() - ) as StakingHbbftMock; - - keyGenHistory = KeyGenFactory.attach( - await keyGenHistoryProxy.getAddress() - ) as KeyGenHistory; - - certifier = CertifierFactory.attach( - await certifierProxy.getAddress() - ) as CertifierHbbft; + ) as unknown as TxPermissionHbbft; - txPermission = TxPermissionFactory.attach( - await txPermissionProxy.getAddress() - ) as TxPermissionHbbft; + await txPermission.waitForDeployment(); keyGenHistoryPermission = new Permission(txPermission, keyGenHistory, logOutput); @@ -563,15 +550,11 @@ describe('KeyGenHistory', () => { }); it('failed KeyGeneration, availability.', async () => { - const stakingBanned = await validatorSetHbbft.bannedUntil(stakingAddresses[0]); - const miningBanned = await validatorSetHbbft.bannedUntil(miningAddresses[0]); const currentTS = await helpers.time.latest(); const newPoolStakingAddress = stakingAddresses[4]; const newPoolMiningAddress = miningAddresses[4]; if (logOutput) { - console.log('stakingBanned?', stakingBanned); - console.log('miningBanned?', miningBanned); console.log('currentTS:', currentTS); console.log('newPoolStakingAddress:', newPoolStakingAddress); console.log('newPoolMiningAddress:', newPoolMiningAddress); diff --git a/test/RandomHbbft.ts b/test/RandomHbbft.ts index 1022856e..2e9b707a 100644 --- a/test/RandomHbbft.ts +++ b/test/RandomHbbft.ts @@ -23,6 +23,7 @@ const stakingWithdrawDisallowPeriod = 1n; const validatorInactivityThreshold = 365 * 86400 // 1 year const SystemAccountAddress = "0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE"; +const ZeroIpAddress = ethers.zeroPadBytes("0x00", 16); describe('RandomHbbft', () => { let owner: HardhatEthersSigner; @@ -32,38 +33,44 @@ describe('RandomHbbft', () => { let stubAddress: string async function deployContracts() { + const validatorSetParams = { + blockRewardContract: stubAddress, + randomContract: stubAddress, + stakingContract: stubAddress, + keyGenHistoryContract: stubAddress, + bonusScoreContract: stubAddress, + validatorInactivityThreshold: validatorInactivityThreshold, + } + const ValidatorSetFactory = await ethers.getContractFactory("ValidatorSetHbbftMock"); - const validatorSetHbbftProxy = await upgrades.deployProxy( + const validatorSetHbbft = await upgrades.deployProxy( ValidatorSetFactory, [ owner.address, - '0x1000000000000000000000000000000000000001', // _blockRewardContract - stubAddress, // _randomContract - stubAddress, // _stakingContract - '0x4000000000000000000000000000000000000001', // _keyGenHistoryContract - validatorInactivityThreshold, // _validatorInactivityThreshold - initialValidators, // _initialMiningAddresses - initialStakingAddresses, // _initialStakingAddresses + validatorSetParams, // _params + initialValidators, // _initialMiningAddresses + initialStakingAddresses, // _initialStakingAddresses ], { initializer: 'initialize' } - ); + ) as unknown as ValidatorSetHbbftMock; - await validatorSetHbbftProxy.waitForDeployment(); + await validatorSetHbbft.waitForDeployment(); const RandomHbbftFactory = await ethers.getContractFactory("RandomHbbft"); - const randomHbbftProxy = await upgrades.deployProxy( + const randomHbbft = await upgrades.deployProxy( RandomHbbftFactory, [ owner.address, - await validatorSetHbbftProxy.getAddress() + await validatorSetHbbft.getAddress() ], { initializer: 'initialize' } - ); + ) as unknown as RandomHbbft; - await randomHbbftProxy.waitForDeployment(); + await randomHbbft.waitForDeployment(); let stakingParams = { - _validatorSetContract: await validatorSetHbbftProxy.getAddress(), + _validatorSetContract: await validatorSetHbbft.getAddress(), + _bonusScoreContract: stubAddress, _initialStakingAddresses: initialStakingAddresses, _delegatorMinStake: minStake, _candidateMinStake: minStake, @@ -78,7 +85,7 @@ describe('RandomHbbft', () => { for (let i = 0; i < initialStakingAddresses.length; i++) { initialValidatorsPubKeys.push(ethers.Wallet.createRandom().signingKey.publicKey); - initialValidatorsIpAddresses.push('0x00000000000000000000000000000000'); + initialValidatorsIpAddresses.push(ZeroIpAddress); } let initialValidatorsPubKeysSplit = fp.flatMap( @@ -88,7 +95,7 @@ describe('RandomHbbft', () => { ])(initialValidatorsPubKeys); const StakingHbbftFactory = await ethers.getContractFactory("StakingHbbftMock"); - const stakingHbbftProxy = await upgrades.deployProxy( + const stakingHbbft = await upgrades.deployProxy( StakingHbbftFactory, [ owner.address, @@ -97,21 +104,9 @@ describe('RandomHbbft', () => { initialValidatorsIpAddresses // _internetAddresses ], { initializer: 'initialize' } - ); - - await stakingHbbftProxy.waitForDeployment(); - - const validatorSetHbbft = ValidatorSetFactory.attach( - await validatorSetHbbftProxy.getAddress() - ) as ValidatorSetHbbftMock; - - const randomHbbft = RandomHbbftFactory.attach( - await randomHbbftProxy.getAddress() - ) as RandomHbbft; + ) as unknown as StakingHbbftMock; - const stakingHbbft = StakingHbbftFactory.attach( - await stakingHbbftProxy.getAddress() - ) as StakingHbbftMock; + await stakingHbbft.waitForDeployment(); await validatorSetHbbft.setRandomContract(await randomHbbft.getAddress()); await validatorSetHbbft.setStakingContract(await stakingHbbft.getAddress()); @@ -237,31 +232,40 @@ describe('RandomHbbft', () => { }); describe("FullHealth()", async function () { - // it('should display health correctly', async () => { - // ((await validatorSetHbbft.getValidators()).length).should.be.equal(25); - // (await randomHbbft.isFullHealth()).should.be.equal(true); - // await validatorSetHbbft.connect(owner).removeMaliciousValidators([accounts[15].address]); - // ((await validatorSetHbbft.getValidators()).length).should.be.equal(24); - // (await randomHbbft.isFullHealth()).should.be.equal(false); - // }); - - // it('should set historical FullHealth() value as true when the block is healthy', async () => { - // let randomSeed = BigNumber.from(random(0, Number.MAX_SAFE_INTEGER)); - // // storing current seed and the health state of the network, network is healthy with 25 validators - // await randomHbbft.connect(owner).setCurrentSeed(randomSeed); - // ((await validatorSetHbbft.getValidators()).length).should.be.equal(25); - - // // removing a validator so the network is not healthy - // await validatorSetHbbft.connect(owner).removeMaliciousValidators([accounts[15].address]); - - // randomSeed = BigNumber.from(random(0, Number.MAX_SAFE_INTEGER)); - // // storing current seed and the health state of the network, network is NOT healthy with 24 validators - // await randomHbbft.connect(owner).setCurrentSeed(randomSeed); - // ((await validatorSetHbbft.getValidators()).length).should.be.equal(24); - // // getting historical health values for both previous and current block - // let blockNumber = await ethers.provider.getBlockNumber(); - // (await randomHbbft.isFullHealthsHistoric([blockNumber, blockNumber - 1])).should.be.deep.equal([false, true]); - // }); + it.skip('should display health correctly', async () => { + const { randomHbbft, validatorSetHbbft } = await helpers.loadFixture(deployContracts); + + expect(await validatorSetHbbft.getValidators()).to.be.lengthOf(25); + expect(await randomHbbft.isFullHealth()).to.be.true; + + await validatorSetHbbft.connect(owner).removeMaliciousValidators([accounts[15].address]); + + expect(await validatorSetHbbft.getValidators()).to.be.lengthOf(24); + expect(await randomHbbft.isFullHealth()).to.be.false; + }); + + it.skip('should set historical FullHealth() value as true when the block is healthy', async () => { + const { randomHbbft, validatorSetHbbft } = await helpers.loadFixture(deployContracts); + + let randomSeed = random(0, Number.MAX_SAFE_INTEGER); + + // storing current seed and the health state of the network, network is healthy with 25 validators + await randomHbbft.connect(owner).setCurrentSeed(randomSeed); + expect(await validatorSetHbbft.getValidators()).to.be.lengthOf(25); + + // removing a validator so the network is not healthy + await validatorSetHbbft.connect(owner).removeMaliciousValidators([accounts[15].address]); + + randomSeed = random(0, Number.MAX_SAFE_INTEGER); + + // storing current seed and the health state of the network, network is NOT healthy with 24 validators + await randomHbbft.connect(owner).setCurrentSeed(randomSeed); + expect(await validatorSetHbbft.getValidators()).to.be.lengthOf(24); + + // getting historical health values for both previous and current block + let blockNumber = await ethers.provider.getBlockNumber(); + expect(await randomHbbft.isFullHealthsHistoric([blockNumber, blockNumber - 1])).to.be.deep.equal([false, true]); + }); it('should display health correctly', async () => { const { randomHbbft, validatorSetHbbft } = await helpers.loadFixture(deployContracts); diff --git a/test/StakingHbbft.ts b/test/StakingHbbft.ts index 2d8d9819..d4c2c0e4 100644 --- a/test/StakingHbbft.ts +++ b/test/StakingHbbft.ts @@ -14,6 +14,7 @@ import { } from "../src/types"; import { getNValidatorsPartNAcks } from "./testhelpers/data"; +import { deployDao } from "./testhelpers/daoDeployment"; //consts const SystemAccountAddress = '0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE'; @@ -67,56 +68,67 @@ describe('StakingHbbft', () => { async function deployContractsFixture() { const stubAddress = owner.address; + await deployDao(); + + const bonusScoreContractMockFactory = await ethers.getContractFactory("BonusScoreSystemMock"); + const bonusScoreContractMock = await bonusScoreContractMockFactory.deploy(); + await bonusScoreContractMock.waitForDeployment(); + const ConnectivityTrackerFactory = await ethers.getContractFactory("ConnectivityTrackerHbbftMock"); const connectivityTracker = await ConnectivityTrackerFactory.deploy(); await connectivityTracker.waitForDeployment(); + const validatorSetParams = { + blockRewardContract: stubAddress, + randomContract: stubAddress, + stakingContract: stubAddress, + keyGenHistoryContract: stubAddress, + bonusScoreContract: await bonusScoreContractMock.getAddress(), + validatorInactivityThreshold: validatorInactivityThreshold, + } + // Deploy ValidatorSet contract const ValidatorSetFactory = await ethers.getContractFactory("ValidatorSetHbbftMock"); - const validatorSetHbbftProxy = await upgrades.deployProxy( + const validatorSetHbbft = await upgrades.deployProxy( ValidatorSetFactory, [ owner.address, - stubAddress, // _blockRewardContract - stubAddress, // _randomContract - stubAddress, // _stakingContract - stubAddress, // _keyGenHistoryContract - validatorInactivityThreshold, // _validatorInactivityThreshold + validatorSetParams, initialValidators, // _initialMiningAddresses initialStakingAddresses, // _initialStakingAddresses ], { initializer: 'initialize' } - ); + ) as unknown as ValidatorSetHbbftMock; - await validatorSetHbbftProxy.waitForDeployment(); + await validatorSetHbbft.waitForDeployment(); // Deploy BlockRewardHbbft contract const BlockRewardHbbftFactory = await ethers.getContractFactory("BlockRewardHbbftMock"); - const blockRewardHbbftProxy = await upgrades.deployProxy( + const blockRewardHbbft = await upgrades.deployProxy( BlockRewardHbbftFactory, [ owner.address, - await validatorSetHbbftProxy.getAddress(), + await validatorSetHbbft.getAddress(), await connectivityTracker.getAddress(), ], { initializer: 'initialize' } - ); + ) as unknown as BlockRewardHbbftMock; - await blockRewardHbbftProxy.waitForDeployment(); + await blockRewardHbbft.waitForDeployment(); - await validatorSetHbbftProxy.setBlockRewardContract(await blockRewardHbbftProxy.getAddress()); + await validatorSetHbbft.setBlockRewardContract(await blockRewardHbbft.getAddress()); const RandomHbbftFactory = await ethers.getContractFactory("RandomHbbft"); - const randomHbbftProxy = await upgrades.deployProxy( + const randomHbbft = await upgrades.deployProxy( RandomHbbftFactory, [ owner.address, - await validatorSetHbbftProxy.getAddress() + await validatorSetHbbft.getAddress() ], { initializer: 'initialize' } - ); + ) as unknown as RandomHbbft; - await randomHbbftProxy.waitForDeployment(); + await randomHbbft.waitForDeployment(); //without that, the Time is 0, //meaning a lot of checks that expect time to have some value deliver incorrect results. @@ -125,22 +137,23 @@ describe('StakingHbbft', () => { const { parts, acks } = getNValidatorsPartNAcks(initialValidators.length); const KeyGenFactory = await ethers.getContractFactory("KeyGenHistory"); - const keyGenHistoryProxy = await upgrades.deployProxy( + const keyGenHistory = await upgrades.deployProxy( KeyGenFactory, [ owner.address, - await validatorSetHbbftProxy.getAddress(), + await validatorSetHbbft.getAddress(), initialValidators, parts, acks ], { initializer: 'initialize' } - ); + ) as unknown as KeyGenHistory; - await keyGenHistoryProxy.waitForDeployment(); + await keyGenHistory.waitForDeployment(); let stakingParams = { - _validatorSetContract: await validatorSetHbbftProxy.getAddress(), + _validatorSetContract: await validatorSetHbbft.getAddress(), + _bonusScoreContract: await bonusScoreContractMock.getAddress(), _initialStakingAddresses: initialStakingAddresses, _delegatorMinStake: minStakeDelegators, _candidateMinStake: minStake, @@ -166,7 +179,7 @@ describe('StakingHbbft', () => { initialValidatorsIpAddresses = Array(initialValidators.length).fill(ZeroIpAddress); const StakingHbbftFactory = await ethers.getContractFactory("StakingHbbftMock"); - const stakingHbbftProxy = await upgrades.deployProxy( + const stakingHbbft = await upgrades.deployProxy( StakingHbbftFactory, [ owner.address, @@ -175,32 +188,17 @@ describe('StakingHbbft', () => { initialValidatorsIpAddresses // _internetAddresses ], { initializer: 'initialize' } - ); - - await stakingHbbftProxy.waitForDeployment(); - - await validatorSetHbbftProxy.setRandomContract(await randomHbbftProxy.getAddress()); - await validatorSetHbbftProxy.setStakingContract(await stakingHbbftProxy.getAddress()); - await validatorSetHbbftProxy.setKeyGenHistoryContract(await keyGenHistoryProxy.getAddress()); + ) as unknown as StakingHbbftMock; - const validatorSetHbbft = ValidatorSetFactory.attach( - await validatorSetHbbftProxy.getAddress() - ) as ValidatorSetHbbftMock; + await stakingHbbft.waitForDeployment(); - const stakingHbbft = StakingHbbftFactory.attach( - await stakingHbbftProxy.getAddress() - ) as StakingHbbftMock; - - const blockRewardHbbft = BlockRewardHbbftFactory.attach( - await blockRewardHbbftProxy.getAddress() - ) as BlockRewardHbbftMock; + await validatorSetHbbft.setRandomContract(await randomHbbft.getAddress()); + await validatorSetHbbft.setStakingContract(await stakingHbbft.getAddress()); + await validatorSetHbbft.setKeyGenHistoryContract(await keyGenHistory.getAddress()); const delegatorMinStake = await stakingHbbft.delegatorMinStake(); const candidateMinStake = await stakingHbbft.candidateMinStake(); - const randomHbbft = RandomHbbftFactory.attach(await randomHbbftProxy.getAddress()) as RandomHbbft; - const keyGenHistory = KeyGenFactory.attach(await keyGenHistoryProxy.getAddress()) as KeyGenHistory; - return { validatorSetHbbft, stakingHbbft, @@ -602,7 +600,8 @@ describe('StakingHbbft', () => { }); describe('initialize()', async () => { - const validatorSetContract = '0x1000000000000000000000000000000000000001'; + const validatorSetContract = ethers.Wallet.createRandom().address; + const bonusScoreContract = ethers.Wallet.createRandom().address; beforeEach(async () => { // The following private keys belong to the accounts 1-3, fixed by using the "--mnemonic" option when starting ganache. @@ -628,6 +627,7 @@ describe('StakingHbbft', () => { it('should initialize successfully', async () => { let stakingParams = { _validatorSetContract: validatorSetContract, + _bonusScoreContract: bonusScoreContract, _initialStakingAddresses: initialStakingAddresses, _delegatorMinStake: minStakeDelegators, _candidateMinStake: minStake, @@ -647,7 +647,7 @@ describe('StakingHbbft', () => { initialValidatorsIpAddresses // _internetAddresses ], { initializer: 'initialize' } - ); + ) as unknown as StakingHbbftMock; await stakingHbbft.waitForDeployment(); @@ -669,6 +669,7 @@ describe('StakingHbbft', () => { it('should fail if owner = address(0)', async () => { let stakingParams = { _validatorSetContract: validatorSetContract, + _bonusScoreContract: bonusScoreContract, _initialStakingAddresses: initialStakingAddresses, _delegatorMinStake: minStake, _candidateMinStake: minStake, @@ -694,6 +695,7 @@ describe('StakingHbbft', () => { it('should fail if ValidatorSet contract address is zero', async () => { let stakingParams = { _validatorSetContract: ethers.ZeroAddress, + _bonusScoreContract: bonusScoreContract, _initialStakingAddresses: initialStakingAddresses, _delegatorMinStake: minStake, _candidateMinStake: minStake, @@ -719,6 +721,7 @@ describe('StakingHbbft', () => { it('should fail if delegatorMinStake is zero', async () => { let stakingParams = { _validatorSetContract: validatorSetContract, + _bonusScoreContract: bonusScoreContract, _initialStakingAddresses: initialStakingAddresses, _delegatorMinStake: 0, _candidateMinStake: minStake, @@ -745,6 +748,7 @@ describe('StakingHbbft', () => { it('should fail if candidateMinStake is zero', async () => { let stakingParams = { _validatorSetContract: validatorSetContract, + _bonusScoreContract: bonusScoreContract, _initialStakingAddresses: initialStakingAddresses, _delegatorMinStake: minStake, _candidateMinStake: 0, @@ -771,6 +775,7 @@ describe('StakingHbbft', () => { it('should fail if already initialized', async () => { let stakingParams = { _validatorSetContract: validatorSetContract, + _bonusScoreContract: bonusScoreContract, _initialStakingAddresses: initialStakingAddresses, _delegatorMinStake: minStake, _candidateMinStake: minStake, @@ -805,6 +810,7 @@ describe('StakingHbbft', () => { it('should fail if stakingEpochDuration is 0', async () => { let stakingParams = { _validatorSetContract: validatorSetContract, + _bonusScoreContract: bonusScoreContract, _initialStakingAddresses: initialStakingAddresses, _delegatorMinStake: minStake, _candidateMinStake: minStake, @@ -830,6 +836,7 @@ describe('StakingHbbft', () => { it('should fail if stakingWithdrawDisallowPeriod is 0', async () => { let stakingParams = { _validatorSetContract: validatorSetContract, + _bonusScoreContract: bonusScoreContract, _initialStakingAddresses: initialStakingAddresses, _delegatorMinStake: minStake, _candidateMinStake: minStake, @@ -855,6 +862,7 @@ describe('StakingHbbft', () => { it('should fail if stakingWithdrawDisallowPeriod >= stakingEpochDuration', async () => { let stakingParams = { _validatorSetContract: validatorSetContract, + _bonusScoreContract: bonusScoreContract, _initialStakingAddresses: initialStakingAddresses, _delegatorMinStake: minStake, _candidateMinStake: minStake, @@ -882,6 +890,7 @@ describe('StakingHbbft', () => { let stakingParams = { _validatorSetContract: validatorSetContract, + _bonusScoreContract: bonusScoreContract, _initialStakingAddresses: initialStakingAddresses, _delegatorMinStake: minStake, _candidateMinStake: minStake, @@ -907,6 +916,7 @@ describe('StakingHbbft', () => { it('should fail if timewindow is 0', async () => { let stakingParams = { _validatorSetContract: validatorSetContract, + _bonusScoreContract: bonusScoreContract, _initialStakingAddresses: initialStakingAddresses, _delegatorMinStake: minStake, _candidateMinStake: minStake, @@ -932,6 +942,7 @@ describe('StakingHbbft', () => { it('should fail if transition timewindow is smaller than the staking time window', async () => { let stakingParams = { _validatorSetContract: validatorSetContract, + _bonusScoreContract: bonusScoreContract, _initialStakingAddresses: initialStakingAddresses, _delegatorMinStake: minStake, _candidateMinStake: minStake, @@ -1132,30 +1143,6 @@ describe('StakingHbbft', () => { .withArgs(pool, delegatorAddress.address); }); - it('should fail for a banned validator', async () => { - const { - stakingHbbft, - validatorSetHbbft, - candidateMinStake, - delegatorMinStake - } = await helpers.loadFixture(deployContractsFixture); - - const pool = await ethers.getSigner(initialStakingAddresses[1]); - - await stakingHbbft.connect(pool).stake(pool.address, { value: candidateMinStake }); - - const systemSigner = await impersonateAcc(SystemAccountAddress); - await validatorSetHbbft.connect(systemSigner).removeMaliciousValidators([initialValidators[1]]); - - await expect(stakingHbbft.connect(delegatorAddress).stake( - pool.address, - { value: delegatorMinStake } - )).to.be.revertedWithCustomError(stakingHbbft, "PoolMiningBanned") - .withArgs(pool.address); - - await helpers.stopImpersonatingAccount(systemSigner.address); - }); - it.skip('should only success in the allowed staking window', async () => { const { stakingHbbft, candidateMinStake } = await helpers.loadFixture(deployContractsFixture); @@ -1577,31 +1564,6 @@ describe('StakingHbbft', () => { await stakingHbbft.connect(staker).withdraw(staker.address, stakeAmount); }); - it("shouldn't allow withdrawing from a banned pool", async () => { - const { stakingHbbft, validatorSetHbbft } = await helpers.loadFixture(deployContractsFixture); - - const pool = await ethers.getSigner(initialStakingAddresses[1]); - - await stakingHbbft.connect(pool).stake(pool.address, { value: stakeAmount }); - await stakingHbbft.connect(delegatorAddress).stake(pool.address, { value: stakeAmount }); - - await validatorSetHbbft.setBannedUntil(initialValidators[1], '0xffffffffffffffff'); - - const maxAllowedForPool = await stakingHbbft.maxWithdrawOrderAllowed(pool.address, pool.address); - await expect(stakingHbbft.connect(pool).withdraw(pool.address, stakeAmount)) - .to.be.revertedWithCustomError(stakingHbbft, "MaxAllowedWithdrawExceeded") - .withArgs(maxAllowedForPool, stakeAmount); - - const maxAllowedForDelegator = await stakingHbbft.maxWithdrawOrderAllowed(pool.address, delegatorAddress.address); - await expect(stakingHbbft.connect(delegatorAddress).withdraw(pool.address, stakeAmount)) - .to.be.revertedWithCustomError(stakingHbbft, "MaxAllowedWithdrawExceeded") - .withArgs(maxAllowedForDelegator, stakeAmount); - - await validatorSetHbbft.setBannedUntil(initialValidators[1], 0n); - await stakingHbbft.connect(pool).withdraw(pool.address, stakeAmount); - await stakingHbbft.connect(delegatorAddress).withdraw(pool.address, stakeAmount); - }); - it.skip("shouldn't allow withdrawing during the stakingWithdrawDisallowPeriod", async () => { const { stakingHbbft } = await helpers.loadFixture(deployContractsFixture); @@ -2272,8 +2234,6 @@ describe('StakingHbbft', () => { it('should set delegator min stake', async () => { const { stakingHbbft } = await helpers.loadFixture(deployContractsFixture); - - console.log("mim stake: ", await stakingHbbft.delegatorMinStake()); const minStakeValue = ethers.parseEther('150') await stakingHbbft.setDelegatorMinStake(minStakeValue); expect(await stakingHbbft.delegatorMinStake()).to.be.equal(minStakeValue); diff --git a/test/TxPermissionHbbft.ts b/test/TxPermissionHbbft.ts index 13f6cbe8..379c5b8c 100644 --- a/test/TxPermissionHbbft.ts +++ b/test/TxPermissionHbbft.ts @@ -10,7 +10,8 @@ import { StakingHbbftMock, TxPermissionHbbftMock, ValidatorSetHbbftMock, - ConnectivityTrackerHbbft + ConnectivityTrackerHbbft, + BlockRewardHbbftMock } from "../src/types"; import { getTestPartNAcks } from './testhelpers/data'; @@ -56,26 +57,36 @@ describe('TxPermissionHbbft', () => { const { parts, acks } = getTestPartNAcks(); + const bonusScoreContractMockFactory = await ethers.getContractFactory("BonusScoreSystemMock"); + const bonusScoreContractMock = await bonusScoreContractMockFactory.deploy(); + await bonusScoreContractMock.waitForDeployment(); + + const validatorSetParams = { + blockRewardContract: stubAddress, + randomContract: stubAddress, + stakingContract: stubAddress, + keyGenHistoryContract: stubAddress, + bonusScoreContract: await bonusScoreContractMock.getAddress(), + validatorInactivityThreshold: validatorInactivityThreshold, + } + const ValidatorSetFactory = await ethers.getContractFactory("ValidatorSetHbbftMock"); - const validatorSetHbbftProxy = await upgrades.deployProxy( + const validatorSetHbbft = await upgrades.deployProxy( ValidatorSetFactory, [ owner.address, - stubAddress, // _blockRewardContract - stubAddress, // _randomContract - stubAddress, // _stakingContract - stubAddress, // _keyGenHistoryContract - validatorInactivityThreshold, // _validatorInactivityThreshold - initialValidators, // _initialMiningAddresses - initialStakingAddresses, // _initialStakingAddresses + validatorSetParams, // _params + initialValidators, // _initialMiningAddresses + initialStakingAddresses, // _initialStakingAddresses ], { initializer: 'initialize' } - ); + ) as unknown as ValidatorSetHbbftMock; - await validatorSetHbbftProxy.waitForDeployment(); + await validatorSetHbbft.waitForDeployment(); let stakingParams = { - _validatorSetContract: await validatorSetHbbftProxy.getAddress(), + _validatorSetContract: await validatorSetHbbft.getAddress(), + _bonusScoreContract: await bonusScoreContractMock.getAddress(), _initialStakingAddresses: initialStakingAddresses, _delegatorMinStake: ethers.parseEther('1'), _candidateMinStake: ethers.parseEther('1'), @@ -98,7 +109,7 @@ describe('TxPermissionHbbft', () => { const StakingHbbftFactory = await ethers.getContractFactory("StakingHbbftMock"); //Deploy StakingHbbft contract - const stakingHbbftProxy = await upgrades.deployProxy( + const stakingHbbft = await upgrades.deployProxy( StakingHbbftFactory, [ owner.address, @@ -107,101 +118,86 @@ describe('TxPermissionHbbft', () => { initialValidatorsIpAddresses // _internetAddresses ], { initializer: 'initialize' } - ); + ) as unknown as StakingHbbftMock; - await stakingHbbftProxy.waitForDeployment(); + await stakingHbbft.waitForDeployment(); const KeyGenFactory = await ethers.getContractFactory("KeyGenHistory"); - const keyGenHistoryProxy = await upgrades.deployProxy( + const keyGenHistory = await upgrades.deployProxy( KeyGenFactory, [ owner.address, - await validatorSetHbbftProxy.getAddress(), + await validatorSetHbbft.getAddress(), initialValidators, parts, acks ], { initializer: 'initialize' } - ); + ) as unknown as KeyGenHistory; - await keyGenHistoryProxy.waitForDeployment(); + await keyGenHistory.waitForDeployment(); const CertifierFactory = await ethers.getContractFactory("CertifierHbbft"); - const certifierProxy = await upgrades.deployProxy( + const certifier = await upgrades.deployProxy( CertifierFactory, [ [owner.address], - await validatorSetHbbftProxy.getAddress(), + await validatorSetHbbft.getAddress(), owner.address ], { initializer: 'initialize' } - ); + ) as unknown as CertifierHbbft; - await certifierProxy.waitForDeployment(); + await certifier.waitForDeployment(); const BlockRewardHbbftFactory = await ethers.getContractFactory("BlockRewardHbbftMock"); - const blockRewardHbbftProxy = await upgrades.deployProxy( + const blockRewardHbbft = await upgrades.deployProxy( BlockRewardHbbftFactory, [ owner.address, - await validatorSetHbbftProxy.getAddress(), + await validatorSetHbbft.getAddress(), stubAddress ], { initializer: 'initialize' } - ); + ) as unknown as BlockRewardHbbftMock; - await blockRewardHbbftProxy.waitForDeployment(); + await blockRewardHbbft.waitForDeployment(); const ConnectivityTrackerFactory = await ethers.getContractFactory("ConnectivityTrackerHbbft"); - const connectivityTrackerProxy = await upgrades.deployProxy( + const connectivityTracker = await upgrades.deployProxy( ConnectivityTrackerFactory, [ owner.address, - await validatorSetHbbftProxy.getAddress(), - await stakingHbbftProxy.getAddress(), - await blockRewardHbbftProxy.getAddress(), + await validatorSetHbbft.getAddress(), + await stakingHbbft.getAddress(), + await blockRewardHbbft.getAddress(), + await bonusScoreContractMock.getAddress(), minReportAgeBlocks, ], { initializer: 'initialize' } - ); + ) as unknown as ConnectivityTrackerHbbft; - await connectivityTrackerProxy.waitForDeployment(); + await connectivityTracker.waitForDeployment(); const TxPermissionFactory = await ethers.getContractFactory("TxPermissionHbbftMock"); - const txPermissionProxy = await upgrades.deployProxy( + const txPermission = await upgrades.deployProxy( TxPermissionFactory, [ allowedSenders, - await certifierProxy.getAddress(), - await validatorSetHbbftProxy.getAddress(), - await keyGenHistoryProxy.getAddress(), - await connectivityTrackerProxy.getAddress(), + await certifier.getAddress(), + await validatorSetHbbft.getAddress(), + await keyGenHistory.getAddress(), + await connectivityTracker.getAddress(), owner.address ], { initializer: 'initialize' } - ); - - await txPermissionProxy.waitForDeployment(); - - const txPermission = TxPermissionFactory.attach(await txPermissionProxy.getAddress()) as TxPermissionHbbftMock; - const keyGenHistory = KeyGenFactory.attach(await keyGenHistoryProxy.getAddress()) as KeyGenHistory; - const certifier = CertifierFactory.attach(await certifierProxy.getAddress()) as CertifierHbbft; - - const validatorSetHbbft = ValidatorSetFactory.attach( - await validatorSetHbbftProxy.getAddress() - ) as ValidatorSetHbbftMock; - - const stakingHbbft = StakingHbbftFactory.attach( - await stakingHbbftProxy.getAddress() - ) as StakingHbbftMock; + ) as unknown as TxPermissionHbbftMock; - const connectivityTracker = ConnectivityTrackerFactory.attach( - await connectivityTrackerProxy.getAddress() - ) as ConnectivityTrackerHbbft; + await txPermission.waitForDeployment(); - await blockRewardHbbftProxy.setConnectivityTracker(await connectivityTrackerProxy.getAddress()); - await validatorSetHbbftProxy.setKeyGenHistoryContract(await keyGenHistoryProxy.getAddress()); - await validatorSetHbbftProxy.setStakingContract(await stakingHbbftProxy.getAddress()); + await blockRewardHbbft.setConnectivityTracker(await connectivityTracker.getAddress()); + await validatorSetHbbft.setKeyGenHistoryContract(await keyGenHistory.getAddress()); + await validatorSetHbbft.setStakingContract(await stakingHbbft.getAddress()); return { txPermission, validatorSetHbbft, certifier, keyGenHistory, stakingHbbft, connectivityTracker }; } @@ -539,7 +535,53 @@ describe('TxPermissionHbbft', () => { }); }); + describe('setConnectivityTracker()', async function () { + it("should restrict calling setConnectivityTracker to contract owner", async function () { + const { txPermission } = await helpers.loadFixture(deployContractsFixture); + + const caller = accounts[1]; + + await expect(txPermission.connect(caller).setConnectivityTracker(caller.address)) + .to.be.revertedWithCustomError(txPermission, "OwnableUnauthorizedAccount") + .withArgs(caller.address); + }); + + it("should not allow to set connectivity tracker address to zero", async function () { + const { txPermission } = await helpers.loadFixture(deployContractsFixture); + + await expect( + txPermission.connect(owner).setConnectivityTracker(ethers.ZeroAddress) + ).to.be.revertedWithCustomError(txPermission, "ZeroAddress"); + }); + + it("should set connectivity tracker contract and emit event", async function () { + const { txPermission } = await helpers.loadFixture(deployContractsFixture); + + const newAddress = accounts[10].address; + + await expect(txPermission.connect(owner).setConnectivityTracker(newAddress)) + .to.emit(txPermission, "SetConnectivityTracker") + .withArgs(newAddress) + + expect(await txPermission.connectivityTracker()).to.equal(newAddress); + }); + }); + describe('allowedTxTypes()', async function () { + async function deployMocks() { + const mockStakingFactory = await ethers.getContractFactory("MockStaking"); + const mockStaking = await mockStakingFactory.deploy(); + await mockStaking.waitForDeployment(); + + const mockValidatorSetFactory = await ethers.getContractFactory("MockValidatorSet"); + const mockValidatorSet = await mockValidatorSetFactory.deploy(); + await mockValidatorSet.waitForDeployment(); + + await mockValidatorSet.setStakingContract(await mockStaking.getAddress()); + + return { mockValidatorSet, mockStaking }; + } + it("should allow all transaction types for allowed sender", async function () { const { txPermission } = await helpers.loadFixture(deployContractsFixture); @@ -727,100 +769,6 @@ describe('TxPermissionHbbft', () => { const ipAddress = '0x11111111111111111111111111111111'; const port = '0xbeef'; - it("should allow reportMalicious if callable", async function () { - const { txPermission, validatorSetHbbft } = await helpers.loadFixture(deployContractsFixture); - - const gasPrice = await txPermission.minimumGasPrice(); - const reporter = await ethers.getSigner(initialValidators[0]); - const malicious = initialValidators[1]; - - const latestBlock = await helpers.time.latestBlock(); - - const calldata = validatorSetHbbft.interface.encodeFunctionData( - "reportMalicious", - [ - malicious, - latestBlock - 1, - EmptyBytes, - ] - ); - - const result = await txPermission.allowedTxTypes( - reporter.address, - await validatorSetHbbft.getAddress(), - 0n, - gasPrice, - ethers.hexlify(calldata), - ); - - expect(result.typesMask).to.equal(AllowedTxTypeMask.Call); - expect(result.cache).to.be.false; - }); - - it("should allow reportMalicious if callable with data length <= 64", async function () { - const { txPermission, validatorSetHbbft } = await helpers.loadFixture(deployContractsFixture); - - const gasPrice = await txPermission.minimumGasPrice(); - const reporter = await ethers.getSigner(initialValidators[0]); - const malicious = initialValidators[1]; - - const latestBlock = await helpers.time.latestBlock(); - - const calldata = validatorSetHbbft.interface.encodeFunctionData( - 'reportMalicious', - [ - malicious, - latestBlock - 1, - EmptyBytes - ] - ); - - const slicedCalldata = calldata.slice(0, calldata.length - 128); - - const result = await txPermission.allowedTxTypes( - reporter.address, - await validatorSetHbbft.getAddress(), - 0n, - gasPrice, - ethers.hexlify(slicedCalldata), - ); - - expect(result.typesMask).to.equal(AllowedTxTypeMask.Call); - expect(result.cache).to.be.false; - }); - - it("should not allow reportMalicious if not callable", async function () { - const { txPermission, validatorSetHbbft } = await helpers.loadFixture(deployContractsFixture); - - const gasPrice = await txPermission.minimumGasPrice(); - - // If reporter is not validator, reportMalicious is not callable, that means tx is not allowed - const reporter = await ethers.getSigner(initialStakingAddresses[0]); - const malicious = initialValidators[1]; - - const latestBlock = await helpers.time.latestBlock(); - - const calldata = validatorSetHbbft.interface.encodeFunctionData( - 'reportMalicious', - [ - malicious, - latestBlock - 1, - EmptyBytes - ] - ); - - const result = await txPermission.allowedTxTypes( - reporter.address, - await validatorSetHbbft.getAddress(), - 0, - gasPrice, - ethers.hexlify(calldata), - ); - - expect(result.typesMask).to.equal(AllowedTxTypeMask.None); - expect(result.cache).to.be.false; - }); - it("should allow announce availability by known unvailable validator", async function () { const { txPermission, validatorSetHbbft } = await helpers.loadFixture(deployContractsFixture); @@ -982,36 +930,23 @@ describe('TxPermissionHbbft', () => { it("should use default validation for other methods calls with zero gas price", async function () { const { txPermission, validatorSetHbbft } = await helpers.loadFixture(deployContractsFixture); + const sender = accounts[11]; const calldata = validatorSetHbbft.interface.encodeFunctionData('newValidatorSet'); const result = await txPermission.allowedTxTypes( - owner.address, + sender.address, await validatorSetHbbft.getAddress(), 0, 0, ethers.hexlify(calldata), ); - expect(result.typesMask).to.equal(AllowedTxTypeMask.All); + expect(result.typesMask).to.equal(AllowedTxTypeMask.None); expect(result.cache).to.be.false; }); }); describe('calls to KeyGenHistory contract', async function () { - async function deployMocks() { - const mockStakingFactory = await ethers.getContractFactory("MockStaking"); - const mockStaking = await mockStakingFactory.deploy(); - await mockStaking.waitForDeployment(); - - const mockValidatorSetFactory = await ethers.getContractFactory("MockValidatorSet"); - const mockValidatorSet = await mockValidatorSetFactory.deploy(); - await mockValidatorSet.waitForDeployment(); - - await mockValidatorSet.setStakingContract(await mockStaking.getAddress()); - - return { mockValidatorSet, mockStaking }; - } - it("should not allow writePart transactions outside of write part time", async function () { const { txPermission, keyGenHistory } = await helpers.loadFixture(deployContractsFixture); @@ -1255,6 +1190,32 @@ describe('TxPermissionHbbft', () => { expect(result.typesMask).to.equal(AllowedTxTypeMask.Call); expect(result.cache).to.be.false; }); + + it("should use default validation for other methods calls", async function () { + const { txPermission, keyGenHistory } = await helpers.loadFixture(deployContractsFixture); + const { mockValidatorSet, mockStaking } = await deployMocks(); + + const epoch = 10; + + await txPermission.setValidatorSetContract(await mockValidatorSet.getAddress()); + await mockValidatorSet.setKeyGenMode(KeyGenMode.WriteAck); + await mockStaking.setStakingEpoch(epoch); + + const calldata = keyGenHistory.interface.encodeFunctionData('clearPrevKeyGenState', [[]]); + const caller = accounts[8]; + const gasPrice = await txPermission.minimumGasPrice(); + + const result = await txPermission.allowedTxTypes( + caller.address, + await keyGenHistory.getAddress(), + 0, + gasPrice, + ethers.hexlify(calldata), + ); + + expect(result.typesMask).to.equal(AllowedTxTypeMask.All); + expect(result.cache).to.be.false; + }); }); describe('calls to ConnectivityTracker contract', async function () { @@ -1395,6 +1356,68 @@ describe('TxPermissionHbbft', () => { expect(result.typesMask).to.equal(AllowedTxTypeMask.None); expect(result.cache).to.be.false; }); + + it("should use default validation for other methods calls", async function () { + const { txPermission, connectivityTracker } = await helpers.loadFixture(deployContractsFixture); + const { mockValidatorSet, mockStaking } = await deployMocks(); + + const epoch = 10; + + const gasPrice = await txPermission.minimumGasPrice(); + const caller = accounts[10]; + + await txPermission.setValidatorSetContract(await mockValidatorSet.getAddress()); + await mockValidatorSet.setKeyGenMode(KeyGenMode.WriteAck); + await mockStaking.setStakingEpoch(epoch); + + const calldata = connectivityTracker.interface.encodeFunctionData('penaliseFaultyValidators', [epoch]); + + const result = await txPermission.allowedTxTypes( + caller.address, + await connectivityTracker.getAddress(), + 0, + gasPrice, + ethers.hexlify(calldata), + ); + + expect(result.typesMask).to.equal(AllowedTxTypeMask.All); + expect(result.cache).to.be.false; + }); + + it("should skip unknown params in calldata", async function () { + const { txPermission, connectivityTracker } = await helpers.loadFixture(deployContractsFixture); + + const gasPrice = await txPermission.minimumGasPrice(); + const reporter = await ethers.getSigner(initialValidators[0]); + + await helpers.mine(minReportAgeBlocks + 1n); + + const latestBlockNum = await helpers.time.latestBlock(); + const block = await ethers.provider.getBlock(latestBlockNum - 1); + + const calldata = connectivityTracker.interface.encodeFunctionData( + 'reportReconnect', + [ + initialValidators[1], + block!.number, + ethers.ZeroHash, + ] + ); + + const abiCoder = new ethers.AbiCoder(); + const additionalArg = abiCoder.encode(["address"], [reporter.address]); + + const result = await txPermission.allowedTxTypes( + reporter.address, + await connectivityTracker.getAddress(), + 0, + gasPrice, + ethers.hexlify(calldata + additionalArg.slice(2)), + ); + + expect(result.typesMask).to.equal(AllowedTxTypeMask.None); + expect(result.cache).to.be.false; + }); }); }); }); diff --git a/test/ValidatorSetHbbft.ts b/test/ValidatorSetHbbft.ts index d5c7fa98..4c7bb72e 100644 --- a/test/ValidatorSetHbbft.ts +++ b/test/ValidatorSetHbbft.ts @@ -11,7 +11,6 @@ import { ValidatorSetHbbftMock, StakingHbbftMock, KeyGenHistory, - TxPermissionHbbftMock, CertifierHbbft, TxPermissionHbbft, } from "../src/types"; @@ -49,102 +48,123 @@ describe('ValidatorSetHbbft', () => { let stubAddress: string; + let getValidatorSetParams = () => { + return { + blockRewardContract: ethers.Wallet.createRandom().address, + randomContract: ethers.Wallet.createRandom().address, + stakingContract: ethers.Wallet.createRandom().address, + keyGenHistoryContract: ethers.Wallet.createRandom().address, + bonusScoreContract: ethers.Wallet.createRandom().address, + validatorInactivityThreshold: validatorInactivityThreshold, + } + } + async function deployContractsFixture() { const { parts, acks } = getNValidatorsPartNAcks(initialValidators.length); + const bonusScoreContractMockFactory = await ethers.getContractFactory("BonusScoreSystemMock"); + const bonusScoreContractMock = await bonusScoreContractMockFactory.deploy(); + await bonusScoreContractMock.waitForDeployment(); + const ConnectivityTrackerFactory = await ethers.getContractFactory("ConnectivityTrackerHbbftMock"); const connectivityTracker = await ConnectivityTrackerFactory.deploy(); await connectivityTracker.waitForDeployment(); + const validatorSetParams = { + blockRewardContract: stubAddress, + randomContract: stubAddress, + stakingContract: stubAddress, + keyGenHistoryContract: stubAddress, + bonusScoreContract: await bonusScoreContractMock.getAddress(), + validatorInactivityThreshold: validatorInactivityThreshold, + } + const ValidatorSetFactory = await ethers.getContractFactory("ValidatorSetHbbftMock"); - const validatorSetHbbftProxy = await upgrades.deployProxy( + const validatorSetHbbft = await upgrades.deployProxy( ValidatorSetFactory, [ owner.address, - stubAddress, // _blockRewardContract - stubAddress, // _randomContract - stubAddress, // _stakingContract - stubAddress, // _keyGenHistoryContract - validatorInactivityThreshold, // _validatorInactivityThreshold - initialValidators, // _initialMiningAddresses - initialStakingAddresses, // _initialStakingAddresses + validatorSetParams, // _params + initialValidators, // _initialMiningAddresses + initialStakingAddresses, // _initialStakingAddresses ], { initializer: 'initialize' } - ); + ) as unknown as ValidatorSetHbbftMock; - await validatorSetHbbftProxy.waitForDeployment(); + await validatorSetHbbft.waitForDeployment(); const RandomHbbftFactory = await ethers.getContractFactory("RandomHbbft"); - const randomHbbftProxy = await upgrades.deployProxy( + const randomHbbft = await upgrades.deployProxy( RandomHbbftFactory, [ owner.address, - await validatorSetHbbftProxy.getAddress() + await validatorSetHbbft.getAddress() ], { initializer: 'initialize' }, - ); + ) as unknown as RandomHbbft; - await randomHbbftProxy.waitForDeployment(); + await randomHbbft.waitForDeployment(); const KeyGenFactory = await ethers.getContractFactory("KeyGenHistory"); - const keyGenHistoryProxy = await upgrades.deployProxy( + const keyGenHistory = await upgrades.deployProxy( KeyGenFactory, [ owner.address, - await validatorSetHbbftProxy.getAddress(), + await validatorSetHbbft.getAddress(), initialValidators, parts, acks ], { initializer: 'initialize' } - ); + ) as unknown as KeyGenHistory; - await keyGenHistoryProxy.waitForDeployment(); + await keyGenHistory.waitForDeployment(); const CertifierFactory = await ethers.getContractFactory("CertifierHbbft"); - const certifierProxy = await upgrades.deployProxy( + const certifier = await upgrades.deployProxy( CertifierFactory, [ [owner.address], - await validatorSetHbbftProxy.getAddress(), + await validatorSetHbbft.getAddress(), owner.address ], { initializer: 'initialize' } - ); + ) as unknown as CertifierHbbft; - await certifierProxy.waitForDeployment(); + await certifier.waitForDeployment(); const TxPermissionFactory = await ethers.getContractFactory("TxPermissionHbbft"); - const txPermissionProxy = await upgrades.deployProxy( + const txPermission = await upgrades.deployProxy( TxPermissionFactory, [ [owner.address], - await certifierProxy.getAddress(), - await validatorSetHbbftProxy.getAddress(), - await keyGenHistoryProxy.getAddress(), + await certifier.getAddress(), + await validatorSetHbbft.getAddress(), + await keyGenHistory.getAddress(), stubAddress, owner.address ], { initializer: 'initialize' } - ); + ) as unknown as TxPermissionHbbft; - await txPermissionProxy.waitForDeployment(); + await txPermission.waitForDeployment(); const BlockRewardHbbftFactory = await ethers.getContractFactory("BlockRewardHbbftMock"); - const blockRewardHbbftProxy = await upgrades.deployProxy( + const blockRewardHbbft = await upgrades.deployProxy( BlockRewardHbbftFactory, [ owner.address, - await validatorSetHbbftProxy.getAddress(), + await validatorSetHbbft.getAddress(), await connectivityTracker.getAddress(), ], { initializer: 'initialize' } - ); + ) as unknown as BlockRewardHbbftMock; - await blockRewardHbbftProxy.waitForDeployment(); + await blockRewardHbbft.waitForDeployment(); let stakingParams = { - _validatorSetContract: await validatorSetHbbftProxy.getAddress(), + _validatorSetContract: await validatorSetHbbft.getAddress(), + _bonusScoreContract: await bonusScoreContractMock.getAddress(), _initialStakingAddresses: initialStakingAddresses, _delegatorMinStake: ethers.parseEther('1'), _candidateMinStake: ethers.parseEther('1'), @@ -155,7 +175,7 @@ describe('ValidatorSetHbbft', () => { }; const StakingHbbftFactory = await ethers.getContractFactory("StakingHbbftMock"); - const stakingHbbftProxy = await upgrades.deployProxy( + const stakingHbbft = await upgrades.deployProxy( StakingHbbftFactory, [ owner.address, @@ -164,26 +184,9 @@ describe('ValidatorSetHbbft', () => { initialValidatorsIpAddresses // _internetAddresses ], { initializer: 'initialize' } - ); - - await stakingHbbftProxy.waitForDeployment(); + ) as unknown as StakingHbbftMock; - const txPermission = TxPermissionFactory.attach(await txPermissionProxy.getAddress()) as TxPermissionHbbftMock; - const keyGenHistory = KeyGenFactory.attach(await keyGenHistoryProxy.getAddress()) as KeyGenHistory; - const certifier = CertifierFactory.attach(await certifierProxy.getAddress()) as CertifierHbbft; - const randomHbbft = RandomHbbftFactory.attach(await randomHbbftProxy.getAddress()) as RandomHbbft - - const validatorSetHbbft = ValidatorSetFactory.attach( - await validatorSetHbbftProxy.getAddress() - ) as ValidatorSetHbbftMock; - - const stakingHbbft = StakingHbbftFactory.attach( - await stakingHbbftProxy.getAddress() - ) as StakingHbbftMock; - - const blockRewardHbbft = BlockRewardHbbftFactory.attach( - await blockRewardHbbftProxy.getAddress() - ) as BlockRewardHbbftMock + await stakingHbbft.waitForDeployment(); await validatorSetHbbft.setBlockRewardContract(await blockRewardHbbft.getAddress()); await validatorSetHbbft.setRandomContract(await randomHbbft.getAddress()); @@ -232,10 +235,43 @@ describe('ValidatorSetHbbft', () => { }); describe('initialize', async () => { - const blockRewardContractAddress = '0x2000000000000000000000000000000000000001'; - const randomContractAddress = '0x3000000000000000000000000000000000000001'; - const stakingContractAddress = '0x1100000000000000000000000000000000000001'; - const keyGenHistoryContractAddress = '0x8000000000000000000000000000000000000001'; + let ZeroInitializerTestCases = [ + { + caseName: "BlockRewardHbbft", + params: { + ...getValidatorSetParams(), + blockRewardContract: ethers.ZeroAddress, + } + }, + { + caseName: "RandomHbbft", + params: { + ...getValidatorSetParams(), + randomContract: ethers.ZeroAddress, + } + }, + { + caseName: "StakingHbbft", + params: { + ...getValidatorSetParams(), + stakingContract: ethers.ZeroAddress, + } + }, + { + caseName: "KeyGenHistory", + params: { + ...getValidatorSetParams(), + keyGenHistoryContract: ethers.ZeroAddress, + } + }, + { + caseName: "BonusScoreSystem", + params: { + ...getValidatorSetParams(), + bonusScoreContract: ethers.ZeroAddress, + } + }, + ] beforeEach(async () => { expect(initialValidators.length).to.be.equal(3); @@ -244,17 +280,31 @@ describe('ValidatorSetHbbft', () => { expect(initialValidators[2]).to.not.be.equal(ethers.ZeroAddress); }); + ZeroInitializerTestCases.forEach((args) => { + it(`should revert initialization with ${args.caseName} contract address`, async function () { + const ValidatorSetFactory = await ethers.getContractFactory("ValidatorSetHbbftMock"); + await expect(upgrades.deployProxy( + ValidatorSetFactory, + [ + owner.address, + args.params, + initialValidators, + initialStakingAddresses, + ], + { initializer: 'initialize' } + )).to.be.revertedWithCustomError(ValidatorSetFactory, "ZeroAddress"); + }); + }); + it('should initialize successfully', async () => { + const params = getValidatorSetParams(); + const ValidatorSetFactory = await ethers.getContractFactory("ValidatorSetHbbftMock"); const validatorSetHbbft = await upgrades.deployProxy( ValidatorSetFactory, [ owner.address, - blockRewardContractAddress, - randomContractAddress, - stakingContractAddress, - keyGenHistoryContractAddress, - validatorInactivityThreshold, + params, initialValidators, initialStakingAddresses, ], @@ -263,10 +313,11 @@ describe('ValidatorSetHbbft', () => { expect(await validatorSetHbbft.waitForDeployment()); - expect(await validatorSetHbbft.blockRewardContract()).to.equal(blockRewardContractAddress); - expect(await validatorSetHbbft.randomContract()).to.equal(randomContractAddress); - expect(await validatorSetHbbft.getStakingContract()).to.equal(stakingContractAddress); - expect(await validatorSetHbbft.keyGenHistoryContract()).to.equal(keyGenHistoryContractAddress); + expect(await validatorSetHbbft.blockRewardContract()).to.equal(params.blockRewardContract); + expect(await validatorSetHbbft.randomContract()).to.equal(params.randomContract); + expect(await validatorSetHbbft.getStakingContract()).to.equal(params.stakingContract); + expect(await validatorSetHbbft.keyGenHistoryContract()).to.equal(params.keyGenHistoryContract); + expect(await validatorSetHbbft.bonusScoreSystem()).to.equal(params.bonusScoreContract); expect(await validatorSetHbbft.getValidators()).to.be.deep.equal(initialValidators); expect((await validatorSetHbbft.getPendingValidators()).length).to.be.equal(0); @@ -287,83 +338,7 @@ describe('ValidatorSetHbbft', () => { ValidatorSetFactory, [ ethers.ZeroAddress, - blockRewardContractAddress, - randomContractAddress, - stakingContractAddress, - keyGenHistoryContractAddress, - validatorInactivityThreshold, - initialValidators, - initialStakingAddresses, - ], - { initializer: 'initialize' } - )).to.be.revertedWithCustomError(ValidatorSetFactory, "ZeroAddress"); - }); - - it('should fail if BlockRewardHbbft contract address is zero', async () => { - const ValidatorSetFactory = await ethers.getContractFactory("ValidatorSetHbbftMock"); - await expect(upgrades.deployProxy( - ValidatorSetFactory, - [ - owner.address, - ethers.ZeroAddress, - randomContractAddress, - stakingContractAddress, - keyGenHistoryContractAddress, - validatorInactivityThreshold, - initialValidators, - initialStakingAddresses, - ], - { initializer: 'initialize' } - )).to.be.revertedWithCustomError(ValidatorSetFactory, "ZeroAddress"); - }); - - it('should fail if RandomHbbft contract address is zero', async () => { - const ValidatorSetFactory = await ethers.getContractFactory("ValidatorSetHbbftMock"); - await expect(upgrades.deployProxy( - ValidatorSetFactory, - [ - owner.address, - blockRewardContractAddress, - ethers.ZeroAddress, - stakingContractAddress, - keyGenHistoryContractAddress, - validatorInactivityThreshold, - initialValidators, - initialStakingAddresses, - ], - { initializer: 'initialize' } - )).to.be.revertedWithCustomError(ValidatorSetFactory, "ZeroAddress"); - }); - - it('should fail if StakingHbbft contract address is zero', async () => { - const ValidatorSetFactory = await ethers.getContractFactory("ValidatorSetHbbftMock"); - await expect(upgrades.deployProxy( - ValidatorSetFactory, - [ - owner.address, - blockRewardContractAddress, - randomContractAddress, - ethers.ZeroAddress, - keyGenHistoryContractAddress, - validatorInactivityThreshold, - initialValidators, - initialStakingAddresses, - ], - { initializer: 'initialize' } - )).to.be.revertedWithCustomError(ValidatorSetFactory, "ZeroAddress"); - }); - - it('should fail if KeyGenHistory contract address is zero', async () => { - const ValidatorSetFactory = await ethers.getContractFactory("ValidatorSetHbbftMock"); - await expect(upgrades.deployProxy( - ValidatorSetFactory, - [ - owner.address, - blockRewardContractAddress, - randomContractAddress, - stakingContractAddress, - ethers.ZeroAddress, - validatorInactivityThreshold, + getValidatorSetParams(), initialValidators, initialStakingAddresses, ], @@ -377,11 +352,7 @@ describe('ValidatorSetHbbft', () => { ValidatorSetFactory, [ owner.address, - blockRewardContractAddress, - randomContractAddress, - stakingContractAddress, - keyGenHistoryContractAddress, - validatorInactivityThreshold, + getValidatorSetParams(), [], initialStakingAddresses, ], @@ -395,11 +366,7 @@ describe('ValidatorSetHbbft', () => { ValidatorSetFactory, [ owner.address, - blockRewardContractAddress, - randomContractAddress, - stakingContractAddress, - keyGenHistoryContractAddress, - validatorInactivityThreshold, + getValidatorSetParams(), initialValidators, initialStakingAddresses, ], @@ -408,11 +375,7 @@ describe('ValidatorSetHbbft', () => { await expect(validatorSetHbbft.initialize( owner.address, - blockRewardContractAddress, - randomContractAddress, - stakingContractAddress, - keyGenHistoryContractAddress, - validatorInactivityThreshold, + getValidatorSetParams(), initialValidators, initialStakingAddresses, )).to.be.revertedWithCustomError(validatorSetHbbft, "InvalidInitialization"); @@ -426,11 +389,7 @@ describe('ValidatorSetHbbft', () => { ValidatorSetFactory, [ owner.address, - blockRewardContractAddress, - randomContractAddress, - stakingContractAddress, - keyGenHistoryContractAddress, - validatorInactivityThreshold, + getValidatorSetParams(), initialValidators, initialStakingAddressesShort, ], @@ -444,11 +403,7 @@ describe('ValidatorSetHbbft', () => { ValidatorSetFactory, [ owner.address, - blockRewardContractAddress, - randomContractAddress, - stakingContractAddress, - keyGenHistoryContractAddress, - validatorInactivityThreshold, + getValidatorSetParams(), initialValidators, initialValidators, ], @@ -464,11 +419,7 @@ describe('ValidatorSetHbbft', () => { ValidatorSetFactory, [ owner.address, - blockRewardContractAddress, - randomContractAddress, - stakingContractAddress, - keyGenHistoryContractAddress, - validatorInactivityThreshold, + getValidatorSetParams(), initialValidators, initialStakingAddresses, ], @@ -484,11 +435,7 @@ describe('ValidatorSetHbbft', () => { ValidatorSetFactory, [ owner.address, - blockRewardContractAddress, - randomContractAddress, - stakingContractAddress, - keyGenHistoryContractAddress, - validatorInactivityThreshold, + getValidatorSetParams(), initialValidators, initialStakingAddresses, ], @@ -504,11 +451,7 @@ describe('ValidatorSetHbbft', () => { ValidatorSetFactory, [ owner.address, - blockRewardContractAddress, - randomContractAddress, - stakingContractAddress, - keyGenHistoryContractAddress, - validatorInactivityThreshold, + getValidatorSetParams(), initialValidators, initialStakingAddresses, ], @@ -525,11 +468,7 @@ describe('ValidatorSetHbbft', () => { ValidatorSetFactory, [ owner.address, - blockRewardContractAddress, - randomContractAddress, - stakingContractAddress, - keyGenHistoryContractAddress, - validatorInactivityThreshold, + getValidatorSetParams(), initialValidators, initialStakingAddresses, ], @@ -546,11 +485,7 @@ describe('ValidatorSetHbbft', () => { ValidatorSetFactory, [ owner.address, - blockRewardContractAddress, - randomContractAddress, - stakingContractAddress, - keyGenHistoryContractAddress, - validatorInactivityThreshold, + getValidatorSetParams(), initialValidators, initialStakingAddresses, ], @@ -567,11 +502,7 @@ describe('ValidatorSetHbbft', () => { ValidatorSetFactory, [ owner.address, - blockRewardContractAddress, - randomContractAddress, - stakingContractAddress, - keyGenHistoryContractAddress, - validatorInactivityThreshold, + getValidatorSetParams(), initialValidators, initialStakingAddresses, ], @@ -622,34 +553,36 @@ describe('ValidatorSetHbbft', () => { const stubAddress = owner.address; + const validatorSetParams = getValidatorSetParams(); + + const bonusScoreContractMockFactory = await ethers.getContractFactory("BonusScoreSystemMock"); + const bonusScoreContractMock = await bonusScoreContractMockFactory.deploy(); + await bonusScoreContractMock.waitForDeployment(); + const ValidatorSetFactory = await ethers.getContractFactory("ValidatorSetHbbftMock"); - const validatorSetHbbftProxy = await upgrades.deployProxy( + const validatorSetHbbft = await upgrades.deployProxy( ValidatorSetFactory, [ owner.address, - stubAddress, // _blockRewardContract - '0x3000000000000000000000000000000000000001', // _randomContract - stubAddress, // _stakingContract - '0x8000000000000000000000000000000000000001', // _keyGenHistoryContract - validatorInactivityThreshold, // _validatorInactivityThreshold + validatorSetParams, initialMiningAddr, // _initialMiningAddresses initialStakingAddr, // _initialStakingAddresses ], { initializer: 'initialize' } - ); + ) as unknown as ValidatorSetHbbftMock; - await validatorSetHbbftProxy.waitForDeployment(); + await validatorSetHbbft.waitForDeployment(); const BlockRewardHbbftFactory = await ethers.getContractFactory("BlockRewardHbbftMock"); const blockRewardHbbft = await upgrades.deployProxy( BlockRewardHbbftFactory, [ owner.address, - await validatorSetHbbftProxy.getAddress(), + await validatorSetHbbft.getAddress(), stubAddress ], { initializer: 'initialize' } - ); + ) as unknown as BlockRewardHbbftMock; await blockRewardHbbft.waitForDeployment(); @@ -658,31 +591,31 @@ describe('ValidatorSetHbbft', () => { CertifierFactory, [ [owner.address], - await validatorSetHbbftProxy.getAddress(), + await validatorSetHbbft.getAddress(), owner.address ], { initializer: 'initialize' } - ); + ) as unknown as CertifierHbbft; await certifier.waitForDeployment(); const keyGenHistoryFake = "0x8000000000000000000000000000000000000001"; const TxPermissionFactory = await ethers.getContractFactory("TxPermissionHbbft"); - const txPermissionProxy = await upgrades.deployProxy( + const txPermission = await upgrades.deployProxy( TxPermissionFactory, [ [owner.address], await certifier.getAddress(), - await validatorSetHbbftProxy.getAddress(), + await validatorSetHbbft.getAddress(), keyGenHistoryFake, stubAddress, owner.address ], { initializer: 'initialize' } - ); + ) as unknown as TxPermissionHbbft; - await txPermissionProxy.waitForDeployment(); + await txPermission.waitForDeployment(); let stakingParams = { @@ -693,14 +626,15 @@ describe('ValidatorSetHbbft', () => { _maxStake: 5000, _stakingTransitionTimeframeLength: 10, _stakingWithdrawDisallowPeriod: 10, - _validatorSetContract: await validatorSetHbbftProxy.getAddress(), + _validatorSetContract: await validatorSetHbbft.getAddress(), + _bonusScoreContract: await bonusScoreContractMock.getAddress(), }; const fakePK = "0xa255fd7ad199f0ee814ee00cce44ef2b1fa1b52eead5d8013ed85eade03034ae"; const fakeIP = "0x00000000000000000000000000000001" const StakingHbbftFactory = await ethers.getContractFactory("StakingHbbftMock"); - const stakingHbbftProxy = await upgrades.deployProxy( + const stakingHbbft = await upgrades.deployProxy( StakingHbbftFactory, [ owner.address, @@ -709,21 +643,9 @@ describe('ValidatorSetHbbft', () => { [fakeIP] // _internetAddresses ], { initializer: 'initialize' } - ); - - await stakingHbbftProxy.waitForDeployment(); + ) as unknown as StakingHbbftMock; - const txPermission = TxPermissionFactory.attach( - await txPermissionProxy.getAddress() - ) as TxPermissionHbbft; - - const validatorSetHbbft = ValidatorSetFactory.attach( - await validatorSetHbbftProxy.getAddress() - ) as ValidatorSetHbbftMock; - - const stakingHbbft = StakingHbbftFactory.attach( - await stakingHbbftProxy.getAddress() - ) as StakingHbbftMock; + await stakingHbbft.waitForDeployment(); validatorSetPermission = new Permission(txPermission, validatorSetHbbft, false); @@ -732,8 +654,8 @@ describe('ValidatorSetHbbft', () => { expect(await validatorSetHbbft.blockRewardContract()).to.equal(await blockRewardHbbft.getAddress()); expect(await validatorSetHbbft.getStakingContract()).to.equal(await stakingHbbft.getAddress()); - expect(await validatorSetHbbft.randomContract()).to.equal('0x3000000000000000000000000000000000000001'); - expect(await validatorSetHbbft.keyGenHistoryContract()).to.equal('0x8000000000000000000000000000000000000001') + expect(await validatorSetHbbft.randomContract()).to.equal(validatorSetParams.randomContract); + expect(await validatorSetHbbft.keyGenHistoryContract()).to.equal(validatorSetParams.keyGenHistoryContract) expect(await stakingHbbft.getPools()).to.be.not.empty; @@ -971,26 +893,6 @@ describe('ValidatorSetHbbft', () => { }); }); - describe('removeMaliciousValidators', async () => { - it("should restrict calling to system address", async () => { - const { validatorSetHbbft } = await helpers.loadFixture(deployContractsFixture); - - await expect(validatorSetHbbft.connect(owner).removeMaliciousValidators([ethers.ZeroAddress])) - .to.be.revertedWithCustomError(validatorSetHbbft, "Unauthorized"); - }); - - it("should call by system address", async () => { - const { validatorSetHbbft } = await helpers.loadFixture(deployContractsFixture); - - const systemSigner = await impersonateAcc(SystemAccountAddress); - expect(await validatorSetHbbft.connect(systemSigner).removeMaliciousValidators( - [initialValidators[1]] - )); - - await helpers.stopImpersonatingAccount(systemSigner.address); - }); - }); - describe('setStakingAddress', async () => { it("should restrict calling to staking contract", async () => { const { validatorSetHbbft } = await helpers.loadFixture(deployContractsFixture); @@ -1038,146 +940,6 @@ describe('ValidatorSetHbbft', () => { }); }); - describe('setBanDuration', async () => { - it("should restrict calling to contract owner", async () => { - const { validatorSetHbbft } = await helpers.loadFixture(deployContractsFixture); - - const caller = accounts[5]; - - await expect(validatorSetHbbft.connect(caller).setBanDuration(0n)) - .to.be.revertedWithCustomError(validatorSetHbbft, "OwnableUnauthorizedAccount") - .withArgs(caller.address); - }); - - it("should set ban duration", async () => { - const { validatorSetHbbft } = await helpers.loadFixture(deployContractsFixture); - const newValue = 150n; - - await expect(validatorSetHbbft.connect(owner).setBanDuration(newValue)) - .to.emit(validatorSetHbbft, "SetBanDuration") - .withArgs(newValue); - - expect(await validatorSetHbbft.banDuration()).to.equal(newValue); - }); - }); - - describe('reportMalicious', async () => { - let validatorSetHbbftContract: ValidatorSetHbbftMock; - - beforeEach(async () => { - const { validatorSetHbbft, stakingHbbft } = await helpers.loadFixture(deployContractsFixture); - - validatorSetHbbftContract = validatorSetHbbft; - - // fill validators pool - const additionalValidators = accountAddresses.slice(7, 52 + 1); // accounts[7...32] - const additionalStakingAddresses = accountAddresses.slice(53, 99 + 1); // accounts[33...59] - - expect(additionalValidators).to.be.lengthOf(46); - expect(additionalStakingAddresses).to.be.lengthOf(46); - - await network.provider.send("evm_setIntervalMining", [8]); - - for (let i = 0; i < additionalValidators.length; i++) { - let stakingAddress = await ethers.getSigner(additionalStakingAddresses[i]); - let miningAddress = await ethers.getSigner(additionalValidators[i]); - - await stakingHbbft.connect(stakingAddress).addPool( - miningAddress.address, - ethers.zeroPadBytes("0x00", 64), - ethers.zeroPadBytes("0x00", 16), - { value: MIN_STAKE } - ); - await announceAvailability(validatorSetHbbftContract, miningAddress.address); - - } - await validatorSetHbbftContract.setBlockRewardContract(accounts[4].address); - await validatorSetHbbftContract.connect(accounts[4]).newValidatorSet(); - await validatorSetHbbftContract.connect(accounts[4]).finalizeChange(); - - // after epoch was finalized successfully, validator set length is healthy - expect(await validatorSetHbbft.getValidators()).to.be.lengthOf(25); - }); - - it("Should be able to increase max amount of active validators", async () => { - await validatorSetHbbftContract.setMaxValidators(30); - - await validatorSetHbbftContract.setBlockRewardContract(accounts[4].address); - await validatorSetHbbftContract.connect(accounts[4]).newValidatorSet(); - await validatorSetHbbftContract.connect(accounts[4]).finalizeChange(); - - // after epoch was finalized successfully, validator set length is healthy - expect(await validatorSetHbbftContract.getValidators()).to.be.lengthOf(30); - }) - - it("Should be able to report a malicious validator", async () => { - let reportBlock = (await ethers.provider.getBlockNumber()) - 1; - let maliciousMiningAddress = (await validatorSetHbbftContract.getValidators())[0]; - - let reportingMiningAddress = await ethers.getSigner((await validatorSetHbbftContract.getValidators())[1]) - await validatorSetHbbftContract.connect(reportingMiningAddress).reportMalicious( - maliciousMiningAddress, - reportBlock, - EmptyBytes, - ); - - const reportsForBlock = await validatorSetHbbftContract.maliceReportedForBlock(maliciousMiningAddress, reportBlock); - - expect(reportsForBlock[0]).to.be.eq(reportingMiningAddress.address); - }) - - it("Shouldn't be able to report a malicious validator in a future block", async () => { - let reportBlock = (await ethers.provider.getBlockNumber()) + 10; - let maliciousMiningAddress = (await validatorSetHbbftContract.getValidators())[0]; - - let reportingMiningAddress = await ethers.getSigner((await validatorSetHbbftContract.getValidators())[1]) - await validatorSetHbbftContract.connect(reportingMiningAddress).reportMalicious( - maliciousMiningAddress, - reportBlock, - EmptyBytes, - ); - - expect(await validatorSetHbbftContract.maliceReportedForBlock(maliciousMiningAddress, reportBlock)).to.be.empty; - }) - - it("Should ban validator after 17 reports", async () => { - let currentValidatorSet = await validatorSetHbbftContract.getValidators() - let reportBlock = (await ethers.provider.getBlockNumber()) - 1; - let maliciousMiningAddress = (await validatorSetHbbftContract.getValidators())[0]; - - for (let i = 1; i < currentValidatorSet.length; i++) { - let reportingMiningAddress = await ethers.getSigner(currentValidatorSet[i]) - await validatorSetHbbftContract.connect(reportingMiningAddress).reportMalicious( - maliciousMiningAddress, - reportBlock, - EmptyBytes, - ); - } - - expect(await validatorSetHbbftContract.maliceReportedForBlock(maliciousMiningAddress, reportBlock)).to.be.lengthOf(17); - expect(await validatorSetHbbftContract.isValidatorBanned(maliciousMiningAddress)).to.be.true; - }) - - it("Validator should get banned if spamming reports (50*maxValidators)", async () => { - let currentValidatorSet = await validatorSetHbbftContract.getValidators() - let reportBlock = (await ethers.provider.getBlockNumber()) - 1; - let reportingMiningAddress = await ethers.getSigner((await validatorSetHbbftContract.getValidators())[0]) - - for (let i = 1; i < 54; i++) { - for (let j = 1; j < currentValidatorSet.length; j++) { - let maliciousMiningAddress = currentValidatorSet[j] - await validatorSetHbbftContract.connect(reportingMiningAddress).reportMalicious( - maliciousMiningAddress, - reportBlock - i, - EmptyBytes, - ); - } - } - - expect(await validatorSetHbbftContract.isValidatorBanned(reportingMiningAddress.address)).to.be.true; - }) - }); - describe('getPublicKey', async () => { it("should get public key by mining address", async () => { const { validatorSetHbbft } = await helpers.loadFixture(deployContractsFixture);