From 66bd1bca9f99ae478367a5ee89e5809bc12602c5 Mon Sep 17 00:00:00 2001 From: Roshan <48975233+Pythonberg1997@users.noreply.github.com> Date: Wed, 29 Nov 2023 11:44:30 +0800 Subject: [PATCH] chore: add init lock amount (#425) * chore: add init lock amount * add annotations * more comment --------- Co-authored-by: zjubfd <296179868@qq.com> --- contracts/BC_fusion/StakeCredit.sol | 66 ++++++++++-- contracts/BC_fusion/StakeHub.sol | 110 +++++++++++++++++++- contracts/BC_fusion/interface/IStakeHub.sol | 2 + test/StakeHub.t.sol | 83 ++++++++++----- test/utils/interface/IBSCGovernor.sol | 7 ++ test/utils/interface/IBSCTimelock.sol | 6 ++ test/utils/interface/IGovToken.sol | 11 +- test/utils/interface/IStakeCredit.sol | 16 ++- test/utils/interface/IStakeHub.sol | 30 +++++- test/utils/interface/IStaking.sol | 4 +- 10 files changed, 290 insertions(+), 45 deletions(-) diff --git a/contracts/BC_fusion/StakeCredit.sol b/contracts/BC_fusion/StakeCredit.sol index de708bf5..ddece4cb 100644 --- a/contracts/BC_fusion/StakeCredit.sol +++ b/contracts/BC_fusion/StakeCredit.sol @@ -17,6 +17,8 @@ contract StakeCredit is System, Initializable, ReentrancyGuardUpgradeable, ERC20 uint256 private constant COMMISSION_RATE_BASE = 10_000; // 100% /*----------------- errors -----------------*/ + error ZeroTotalShares(); + error ZeroTotalPooledBNB(); error TransferNotAllowed(); error ApproveNotAllowed(); error WrongInitContext(); @@ -47,6 +49,10 @@ contract StakeCredit is System, Initializable, ReentrancyGuardUpgradeable, ERC20 event RewardReceived(uint256 rewardToAll, uint256 commission); /*----------------- init -----------------*/ + /* + * @param _validator validator's operator address + * @param _moniker validator's moniker + */ function initialize(address _validator, string calldata _moniker) external payable initializer onlyStakeHub { string memory name_ = string.concat("Stake ", _moniker, " Credit"); string memory symbol_ = string.concat("st", _moniker); @@ -58,11 +64,20 @@ contract StakeCredit is System, Initializable, ReentrancyGuardUpgradeable, ERC20 } /*----------------- external functions -----------------*/ + /** + * @param delegator the address of the delegator + * @return shares the amount of shares minted + */ function delegate(address delegator) external payable onlyStakeHub returns (uint256 shares) { if (msg.value == 0) revert ZeroAmount(); shares = _mintAndSync(delegator, msg.value); } + /** + * @param delegator the address of the delegator + * @param shares the amount of shares to be undelegated + * @return bnbAmount the amount of BNB to be unlocked + */ function undelegate(address delegator, uint256 shares) external onlyStakeHub returns (uint256 bnbAmount) { if (shares == 0) revert ZeroAmount(); if (shares > balanceOf(delegator)) revert InsufficientBalance(); @@ -77,8 +92,10 @@ contract StakeCredit is System, Initializable, ReentrancyGuardUpgradeable, ERC20 } /** - * @dev Unbond immediately without adding to the queue. - * Only for redelegate process. + * @dev Unbond immediately without adding to the queue. Only for redelegate process. + * @param delegator the address of the delegator + * @param shares the amount of shares to be undelegated + * @return bnbAmount the amount of BNB unlocked */ function unbond(address delegator, uint256 shares) external onlyStakeHub returns (uint256 bnbAmount) { if (shares == 0) revert ZeroAmount(); @@ -90,6 +107,11 @@ contract StakeCredit is System, Initializable, ReentrancyGuardUpgradeable, ERC20 if (!success) revert TransferFailed(); } + /** + * @param delegator the address of the delegator + * @param number the number of unbond requests to be claimed. 0 means claim all + * @return _totalBnbAmount the total amount of BNB claimed + */ function claim(address payable delegator, uint256 number) external onlyStakeHub nonReentrant returns (uint256) { // number == 0 means claim all // number should not exceed the length of the queue @@ -122,6 +144,10 @@ contract StakeCredit is System, Initializable, ReentrancyGuardUpgradeable, ERC20 return _totalBnbAmount; } + /** + * @dev Distribute the reward to the validator and all delegators. Only the `StakeHub` contract can call this function. + * @param commissionRate the commission rate of the validator + */ function distributeReward(uint64 commissionRate) external payable onlyStakeHub { uint256 bnbAmount = msg.value; uint256 _commission = (bnbAmount * uint256(commissionRate)) / COMMISSION_RATE_BASE; @@ -134,6 +160,11 @@ contract StakeCredit is System, Initializable, ReentrancyGuardUpgradeable, ERC20 emit RewardReceived(_reward, _commission); } + /** + * @dev Slash the validator. Only the `StakeHub` contract can call this function. + * @param slashBnbAmount the amount of BNB to be slashed + * @return realSlashBnbAmount the real amount of BNB slashed + */ function slash(uint256 slashBnbAmount) external onlyStakeHub returns (uint256) { uint256 selfDelegation = balanceOf(validator); uint256 slashShares = getSharesByPooledBNB(slashBnbAmount); @@ -152,9 +183,7 @@ contract StakeCredit is System, Initializable, ReentrancyGuardUpgradeable, ERC20 * @return the amount of shares that corresponds to `_bnbAmount` protocol-controlled BNB. */ function getSharesByPooledBNB(uint256 bnbAmount) public view returns (uint256) { - if (totalPooledBNB == 0) { - return 0; - } + if (totalPooledBNB == 0) revert ZeroTotalPooledBNB(); return (bnbAmount * totalSupply()) / totalPooledBNB; } @@ -162,17 +191,21 @@ contract StakeCredit is System, Initializable, ReentrancyGuardUpgradeable, ERC20 * @return the amount of BNB that corresponds to `_sharesAmount` token shares. */ function getPooledBNBByShares(uint256 shares) public view returns (uint256) { - if (totalSupply() == 0) { - return 0; - } + if (totalSupply() == 0) revert ZeroTotalShares(); return (shares * totalPooledBNB) / totalSupply(); } + /** + * @return the unbond request at _index and the total length of delegator's unbond queue. + */ function unbondRequest(address delegator, uint256 _index) public view returns (UnbondRequest memory, uint256) { bytes32 hash = _unbondRequestsQueue[delegator].at(_index); return (_unbondRequests[hash], _unbondRequestsQueue[delegator].length()); } + /** + * @return the total amount of BNB locked in the unbond queue. + */ function lockedBNBs(address delegator) public view returns (uint256) { uint256 length = _unbondRequestsQueue[delegator].length(); if (length == 0) { @@ -188,10 +221,16 @@ contract StakeCredit is System, Initializable, ReentrancyGuardUpgradeable, ERC20 return _totalBnbAmount; } + /** + * @return the personal unbond sequence of the delegator. + */ function unbondSequence(address delegator) public view returns (uint256) { return _unbondSequence[delegator].current(); } + /** + * @return the total amount of BNB staked and reward of the delegator. + */ function getPooledBNB(address account) public view returns (uint256) { return getPooledBNBByShares(balanceOf(account)); } @@ -199,11 +238,16 @@ contract StakeCredit is System, Initializable, ReentrancyGuardUpgradeable, ERC20 /*----------------- internal functions -----------------*/ function _bootstrapInitialHolder(uint256 initAmount) internal onlyInitializing { // check before mint - if (initAmount == 0 || validator == address(0) || totalSupply() != 0) revert WrongInitContext(); + uint256 toLock = IStakeHub(STAKE_HUB_ADDR).INIT_LOCK_AMOUNT(); + if (initAmount <= toLock || validator == address(0) || totalSupply() != 0) revert WrongInitContext(); - // mint initial tokens to the validator + // mint initial tokens to the validator and lock some of them // shares is equal to the amount of BNB staked - _mint(validator, initAmount); + address deadAddress = IStakeHub(STAKE_HUB_ADDR).DEAD_ADDRESS(); + _mint(deadAddress, toLock); + uint256 initShares = initAmount - toLock; + _mint(validator, initShares); + totalPooledBNB = initAmount; } diff --git a/contracts/BC_fusion/StakeHub.sol b/contracts/BC_fusion/StakeHub.sol index 96a185c3..0d4fef61 100644 --- a/contracts/BC_fusion/StakeHub.sol +++ b/contracts/BC_fusion/StakeHub.sol @@ -20,7 +20,8 @@ contract StakeHub is System, Initializable { uint256 private constant BLS_PUBKEY_LENGTH = 48; uint256 private constant BLS_SIG_LENGTH = 96; - address private constant DEAD_ADDRESS = address(0xdead); + address public constant DEAD_ADDRESS = address(0xdead); + uint256 public constant INIT_LOCK_AMOUNT = 1 ether; //TODO bytes private constant INIT_BC_CONSENSUS_ADDRESSES = @@ -87,7 +88,7 @@ contract StakeHub is System, Initializable { uint256 public numOfJailed; // max number of jailed validators per day(only for malicious vote and double sign) uint256 private felonyPerDay; - // day => number of malicious vote and double sign slash + // day index(timestamp / 86400) => number of malicious vote and double sign slash mapping(uint256 => uint256) private _felonyMap; address public assetProtector; @@ -184,7 +185,9 @@ contract StakeHub is System, Initializable { if (_isRedelegating != 1) revert(); } - /*----------------- init -----------------*/ + /** + * @dev this function is invoked by BSC Parlia consensus engine during the hard fork + */ function initialize() external initializer onlyCoinbase onlyZeroGasPrice { transferGasLimit = 5000; minSelfDelegationBNB = 2_000 ether; @@ -212,6 +215,13 @@ contract StakeHub is System, Initializable { } /*----------------- external functions -----------------*/ + /** + * @param consensusAddress the consensus address of the validator + * @param voteAddress the vote address of the validator + * @param blsProof the bls proof of the vote address + * @param commission the commission of the validator + * @param description the description of the validator + */ function createValidator( address consensusAddress, bytes calldata voteAddress, @@ -230,13 +240,15 @@ contract StakeHub is System, Initializable { } uint256 delegation = msg.value; - if (delegation < minSelfDelegationBNB) revert SelfDelegationNotEnough(); + if (delegation < minSelfDelegationBNB + INIT_LOCK_AMOUNT) revert SelfDelegationNotEnough(); + if (consensusAddress == address(0)) revert InvalidConsensusAddress(); if ( commission.maxRate > 5_000 || commission.rate > commission.maxRate || commission.maxChangeRate > commission.maxRate ) revert InvalidCommission(); if (!_checkMoniker(description.moniker)) revert InvalidMoniker(); + // proof-of-possession verify if (!_checkVoteAddress(voteAddress, blsProof)) revert InvalidVoteAddress(); // deploy stake credit proxy contract @@ -257,6 +269,9 @@ contract StakeHub is System, Initializable { emit ValidatorCreated(consensusAddress, operatorAddress, creditContract, voteAddress); } + /** + * @param newConsensusAddress the new consensus address of the validator + */ function editConsensusAddress(address newConsensusAddress) external whenNotPaused @@ -279,6 +294,9 @@ contract StakeHub is System, Initializable { emit ConsensusAddressEdited(operatorAddress, newConsensusAddress); } + /** + * @param commissionRate the new commission rate of the validator + */ function editCommissionRate(uint64 commissionRate) external whenNotPaused @@ -301,6 +319,9 @@ contract StakeHub is System, Initializable { emit CommissionRateEdited(operatorAddress, commissionRate); } + /** + * @param description the new description of the validator + */ function editDescription(Description calldata description) external whenNotPaused @@ -319,10 +340,15 @@ contract StakeHub is System, Initializable { emit DescriptionEdited(operatorAddress); } + /** + * @param newVoteAddress the new vote address of the validator + * @param blsProof the bls proof of the vote address + */ function editVoteAddress( bytes calldata newVoteAddress, bytes calldata blsProof ) external whenNotPaused notInBlackList validatorExist(msg.sender) { + // proof-of-possession verify if (!_checkVoteAddress(newVoteAddress, blsProof)) revert InvalidVoteAddress(); if (_voteToOperator[newVoteAddress] != address(0) || _legacyVoteAddress[newVoteAddress]) { revert DuplicateVoteAddress(); @@ -339,6 +365,9 @@ contract StakeHub is System, Initializable { emit VoteAddressEdited(operatorAddress, newVoteAddress); } + /** + * @param operatorAddress the operator address of the validator to be unjailed + */ function unjail(address operatorAddress) external whenNotPaused validatorExist(operatorAddress) { Validator storage valInfo = _validators[operatorAddress]; if (!valInfo.jailed) revert ValidatorNotJailed(); @@ -353,6 +382,10 @@ contract StakeHub is System, Initializable { emit ValidatorUnjailed(operatorAddress); } + /** + * @param operatorAddress the operator address of the validator to be delegated to + * @param delegateVotePower whether to delegate vote power to the validator + */ function delegate( address operatorAddress, bool delegateVotePower @@ -373,6 +406,11 @@ contract StakeHub is System, Initializable { } } + /** + * @dev Undelegate BNB from a validator, fund is only claimable few days later + * @param operatorAddress the operator address of the validator to be undelegated from + * @param shares the shares to be undelegated + */ function undelegate( address operatorAddress, uint256 shares @@ -392,6 +430,12 @@ contract StakeHub is System, Initializable { IGovToken(GOV_TOKEN_ADDR).sync(valInfo.creditContract, delegator); } + /** + * @param srcValidator the operator address of the validator to be redelegated from + * @param dstValidator the operator address of the validator to be redelegated to + * @param shares the shares to be redelegated + * @param delegateVotePower whether to delegate vote power to the dstValidator + */ function redelegate( address srcValidator, address dstValidator, @@ -428,6 +472,8 @@ contract StakeHub is System, Initializable { /** * @dev Claim the undelegated BNB from the pool after unbondPeriod + * @param operatorAddress the operator address of the validator + * @param requestNumber the request number of the undelegation. 0 means claim all */ function claim( address operatorAddress, @@ -437,6 +483,11 @@ contract StakeHub is System, Initializable { emit Claimed(operatorAddress, msg.sender, bnbAmount); } + /** + * @dev Sync the gov tokens of validators in operatorAddresses + * @param operatorAddresses the operator addresses of the validators + * @param account the account to sync gov tokens to + */ function syncGovToken( address[] calldata operatorAddresses, address account @@ -469,6 +520,9 @@ contract StakeHub is System, Initializable { emit RewardDistributed(operatorAddress, msg.value); } + /** + * @dev Downtime slash. Only the `SlashIndicator` contract can call this function. + */ function downtimeSlash(address consensusAddress) external onlySlash { address operatorAddress = _consensusToOperator[consensusAddress]; if (!_validatorSet.contains(operatorAddress)) revert ValidatorNotExist(); // should never happen @@ -484,6 +538,9 @@ contract StakeHub is System, Initializable { IGovToken(GOV_TOKEN_ADDR).sync(valInfo.creditContract, operatorAddress); } + /** + * @dev Malicious vote slash. Only the `SlashIndicator` contract can call this function. + */ function maliciousVoteSlash(bytes calldata _voteAddr) external onlySlash { address operatorAddress = _voteToOperator[_voteAddr]; if (!_validatorSet.contains(operatorAddress)) revert ValidatorNotExist(); // should never happen @@ -504,6 +561,9 @@ contract StakeHub is System, Initializable { IGovToken(GOV_TOKEN_ADDR).sync(valInfo.creditContract, operatorAddress); } + /** + * @dev Double sign slash. Only the `SlashIndicator` contract can call this function. + */ function doubleSignSlash(address consensusAddress) external onlySlash { address operatorAddress = _consensusToOperator[consensusAddress]; if (!_validatorSet.contains(operatorAddress)) revert ValidatorNotExist(); // should never happen @@ -524,24 +584,40 @@ contract StakeHub is System, Initializable { IGovToken(GOV_TOKEN_ADDR).sync(valInfo.creditContract, operatorAddress); } + /** + * @dev Pause the whole system in emergency + */ function pause() external onlyAssetProtector { _paused = true; emit Paused(); } + /** + * @dev Resume the whole system + */ function resume() external onlyAssetProtector { _paused = false; emit Resumed(); } + /** + * @dev Add an address to the black list + */ function addToBlackList(address account) external onlyAssetProtector { blackList[account] = true; } + /** + * @dev Remove an address from the black list + */ function removeFromBlackList(address account) external onlyAssetProtector { blackList[account] = false; } + /** + * @param key the key of the param + * @param value the value of the param + */ function updateParam(string calldata key, bytes calldata value) external onlyGov { if (key.compareStrings("transferGasLimit")) { if (value.length != 32) revert InvalidValue(key, value); @@ -609,10 +685,17 @@ contract StakeHub is System, Initializable { } /*----------------- view functions -----------------*/ + /** + * @return is the system paused + */ function isPaused() external view returns (bool) { return _paused; } + /** + * @return the basic info of a validator + * including consensus address, credit contract, vote address, jailed and jailUntil + */ function getValidatorBasicInfo(address operatorAddress) external view @@ -633,6 +716,9 @@ contract StakeHub is System, Initializable { jailUntil = valInfo.jailUntil; } + /** + * @return the description of a validator + */ function getValidatorDescription(address operatorAddress) external view @@ -642,6 +728,9 @@ contract StakeHub is System, Initializable { return _validators[operatorAddress].description; } + /** + * @return the commission of a validator + */ function getValidatorCommission(address operatorAddress) external view @@ -651,6 +740,13 @@ contract StakeHub is System, Initializable { return _validators[operatorAddress].commission; } + /** + * @dev this function will be invoked by Parlia consensus engine. + * @return the election info of a validator + * including consensus address, voting power and vote address. + * The voting power will be 0 if the validator is jailed. + * This function is for the consensus engine. + */ function getValidatorElectionInfo( uint256 offset, uint256 limit @@ -683,10 +779,16 @@ contract StakeHub is System, Initializable { } } + /** + * @return the operator address that ever used the vote address + */ function getOperatorAddressByVoteAddress(bytes calldata voteAddress) external view returns (address) { return _voteToOperator[voteAddress]; } + /** + * @return the operator address that ever used the consensus address + */ function getOperatorAddressByConsensusAddress(address consensusAddress) external view returns (address) { return _consensusToOperator[consensusAddress]; } diff --git a/contracts/BC_fusion/interface/IStakeHub.sol b/contracts/BC_fusion/interface/IStakeHub.sol index 20ea1dc3..a84c8073 100644 --- a/contracts/BC_fusion/interface/IStakeHub.sol +++ b/contracts/BC_fusion/interface/IStakeHub.sol @@ -2,6 +2,8 @@ pragma solidity 0.8.17; interface IStakeHub { + function DEAD_ADDRESS() external view returns (address); + function INIT_LOCK_AMOUNT() external view returns (uint256); function unbondPeriod() external view returns (uint256); function transferGasLimit() external view returns (uint256); } diff --git a/test/StakeHub.t.sol b/test/StakeHub.t.sol index c43468fd..00409dee 100644 --- a/test/StakeHub.t.sol +++ b/test/StakeHub.t.sol @@ -40,7 +40,7 @@ contract StakeHubTest is Deployer { (address validator,) = _createValidator(2000 ether); vm.startPrank(validator); - vm.expectRevert(bytes("UPDATE_TOO_FREQUENTLY")); + vm.expectRevert(); stakeHub.editConsensusAddress(address(1)); // 2. edit consensus address @@ -54,9 +54,9 @@ contract StakeHubTest is Deployer { // 3. edit commission rate vm.warp(block.timestamp + 1 days); - vm.expectRevert(bytes("INVALID_COMMISSION_RATE")); + vm.expectRevert(); stakeHub.editCommissionRate(110); - vm.expectRevert(bytes("INVALID_COMMISSION_RATE")); + vm.expectRevert(); stakeHub.editCommissionRate(16); vm.expectEmit(true, false, false, true, address(stakeHub)); emit CommissionRateEdited(validator, 15); @@ -69,19 +69,19 @@ contract StakeHubTest is Deployer { StakeHub.Description memory description = stakeHub.getValidatorDescription(validator); // invalid moniker description.moniker = "test"; - vm.expectRevert(bytes("INVALID_MONIKER")); + vm.expectRevert(); stakeHub.editDescription(description); description.moniker = "T"; - vm.expectRevert(bytes("INVALID_MONIKER")); + vm.expectRevert(); stakeHub.editDescription(description); description.moniker = "Test;"; - vm.expectRevert(bytes("INVALID_MONIKER")); + vm.expectRevert(); stakeHub.editDescription(description); description.moniker = "Test "; - vm.expectRevert(bytes("INVALID_MONIKER")); + vm.expectRevert(); stakeHub.editDescription(description); // valid moniker @@ -112,7 +112,7 @@ contract StakeHubTest is Deployer { vm.startPrank(delegator); // failed with too small delegation amount - vm.expectRevert(bytes("INVALID_DELEGATION_AMOUNT")); + vm.expectRevert(); stakeHub.delegate{ value: 1 }(validator, false); // success case @@ -134,14 +134,14 @@ contract StakeHubTest is Deployer { uint256 shares = IStakeCredit(credit).balanceOf(delegator); // failed with not enough shares - vm.expectRevert(bytes("INSUFFICIENT_BALANCE")); + vm.expectRevert(); stakeHub.undelegate(validator, shares + 1); // success case stakeHub.undelegate(validator, shares / 2); // claim failed - vm.expectRevert(bytes("NO_CLAIMABLE_UNBOND_REQUEST")); + vm.expectRevert(); stakeHub.claim(validator, 0); // claim success @@ -156,6 +156,34 @@ contract StakeHubTest is Deployer { vm.stopPrank(); } + function testUndelegateAll() public { + uint256 selfDelegation = 2000 ether; + uint256 toLock = stakeHub.INIT_LOCK_AMOUNT(); + (address validator, address credit) = _createValidator(selfDelegation); + uint256 _totalShares = IStakeCredit(credit).totalSupply(); + assertEq(_totalShares, selfDelegation + toLock, "wrong total shares"); + uint256 _totalPooledBNB = IStakeCredit(credit).totalPooledBNB(); + assertEq(_totalPooledBNB, selfDelegation + toLock, "wrong total pooled BNB"); + + vm.startPrank(validator); + + // 1. undelegate all + stakeHub.undelegate(validator, selfDelegation); + _totalShares = IStakeCredit(credit).totalSupply(); + assertEq(_totalShares, toLock, "wrong total shares"); + _totalPooledBNB = IStakeCredit(credit).totalPooledBNB(); + assertEq(_totalPooledBNB, toLock, "wrong total pooled BNB"); + + // 2. delegate again + stakeHub.delegate{ value: selfDelegation }(validator, false); + _totalShares = IStakeCredit(credit).totalSupply(); + assertEq(_totalShares, selfDelegation + toLock, "wrong total shares"); + _totalPooledBNB = IStakeCredit(credit).totalPooledBNB(); + assertEq(_totalPooledBNB, selfDelegation + toLock, "wrong total pooled BNB"); + + vm.stopPrank(); + } + function testRedelegate() public { address delegator = _getNextUserAddress(); (address validator1, address credit1) = _createValidator(2000 ether); @@ -167,11 +195,11 @@ contract StakeHubTest is Deployer { uint256 oldShares = IStakeCredit(credit1).balanceOf(delegator); // failed with too small redelegation amount - vm.expectRevert(bytes("INVALID_REDELEGATION_AMOUNT")); + vm.expectRevert(); stakeHub.redelegate(validator1, validator2, 1, false); // failed with not enough shares - vm.expectRevert(bytes("INSUFFICIENT_BALANCE")); + vm.expectRevert(); stakeHub.redelegate(validator1, validator2, oldShares + 1, false); // success case @@ -205,30 +233,32 @@ contract StakeHubTest is Deployer { // 3. check shares // reward: 100 ether // commissionToValidator: reward(100 ether) * commissionRate(10/10000) = 0.1 ether - // preTotalPooledBNB: selfDelegation(2000 ether) + delegation(100 ether) + (reward - commissionToValidator)(99.9 ether) = 2199.9 ether - // preTotalShares: selfDelegation(2000 ether) + delegation(100 ether) - // curTotalShares: preTotalShares + commissionToValidator * preTotalShares / preTotalPooledBNB = 2100095458884494749761 - // curTotalPooledBNB: preTotalPooledBNB + commissionToValidator = 2200 ether + // preTotalPooledBNB: locked amount(1 ether) + selfDelegation(2000 ether) + delegation(100 ether) + (reward - commissionToValidator)(99.9 ether) = 2200.9 ether + // preTotalShares: locked shares(1 ether) + selfDelegation(2000 ether) + delegation(100 ether) + // curTotalShares: preTotalShares + commissionToValidator * preTotalShares / preTotalPooledBNB = 2101095460947794084238 + // curTotalPooledBNB: preTotalPooledBNB + commissionToValidator = 2201 ether // expectedBnbAmount: shares(100 ether) * curTotalPooledBNB / curTotalShares - uint256 expectedBnbAmount = shares * 2200 ether / 2100095458884494749761; + uint256 _totalShares = IStakeCredit(credit).totalSupply(); + assertEq(_totalShares, 2101095460947794084238, "wrong total shares"); + uint256 expectedBnbAmount = shares * 2201 ether / uint256(2101095460947794084238); uint256 realBnbAmount = IStakeCredit(credit).getPooledBNBByShares(shares); - assertEq(realBnbAmount, expectedBnbAmount); + assertEq(realBnbAmount, expectedBnbAmount, "wrong BNB amount"); // 4. undelegate and submit new delegate vm.prank(delegator); stakeHub.undelegate(validator, shares); - // totalShares: 2100095458884494749761 - 100 ether - // totalPooledBNB: 2200 ether - (100 ether + 99.9 ether * 100 / 2000 ) = 2095242857142857142858 + // totalShares: 2101095460947794084238 - 100 ether = 2001095460947794084238 + // totalPooledBNB: 2201 ether - (100 ether + 99.9 ether * 100 / 2101 ) = 2096245121370775821038 // newShares: 100 ether * totalShares / totalPooledBNB uint256 _totalPooledBNB = IStakeCredit(credit).totalPooledBNB(); - assertEq(_totalPooledBNB, 2095242857142857142858); - uint256 expectedShares = 100 ether * 2000095458884494749761 / _totalPooledBNB; + assertEq(_totalPooledBNB, 2096245121370775821038, "wrong total pooled BNB"); + uint256 expectedShares = 100 ether * uint256(2001095460947794084238) / uint256(2096245121370775821038); address newDelegator = _getNextUserAddress(); vm.prank(newDelegator); stakeHub.delegate{ value: delegation }(validator, false); uint256 newShares = IStakeCredit(credit).balanceOf(newDelegator); - assertEq(newShares, expectedShares); + assertEq(newShares, expectedShares, "wrong new shares"); } function testDowntimeSlash() public { @@ -272,7 +302,7 @@ contract StakeHubTest is Deployer { // unjail (,,, bool jailed,) = stakeHub.getValidatorBasicInfo(validator); assertEq(jailed, true); - vm.expectRevert(bytes("STILL_JAILED")); + vm.expectRevert(); stakeHub.unjail(validator); vm.warp(block.timestamp + slashTime + 1); vm.expectEmit(true, false, false, true, address(stakeHub)); @@ -449,8 +479,11 @@ contract StakeHubTest is Deployer { bytes memory blsProof = new bytes(96); address consensusAddress = address(uint160(uint256(keccak256(blsPubKey)))); + uint256 toLock = stakeHub.INIT_LOCK_AMOUNT(); vm.prank(operatorAddress); - stakeHub.createValidator{ value: delegation }(consensusAddress, blsPubKey, blsProof, commission, description); + stakeHub.createValidator{ value: delegation + toLock }( + consensusAddress, blsPubKey, blsProof, commission, description + ); (, credit,,,) = stakeHub.getValidatorBasicInfo(operatorAddress); } diff --git a/test/utils/interface/IBSCGovernor.sol b/test/utils/interface/IBSCGovernor.sol index e53b7f8e..a8dd559c 100644 --- a/test/utils/interface/IBSCGovernor.sol +++ b/test/utils/interface/IBSCGovernor.sol @@ -11,6 +11,13 @@ interface BSCGovernor { } error Empty(); + error InvalidValue(string key, bytes value); + error NotWhitelisted(); + error OnlyCoinbase(); + error OnlySystemContract(address systemContract); + error OnlyZeroGasPrice(); + error TotalSupplyNotEnough(); + error UnknownParam(string key, bytes value); event EIP712DomainChanged(); event Initialized(uint8 version); diff --git a/test/utils/interface/IBSCTimelock.sol b/test/utils/interface/IBSCTimelock.sol index 94a1a8d2..764a5cae 100644 --- a/test/utils/interface/IBSCTimelock.sol +++ b/test/utils/interface/IBSCTimelock.sol @@ -2,6 +2,12 @@ pragma solidity ^0.8.10; interface BSCTimelock { + error InvalidValue(string key, bytes value); + error OnlyCoinbase(); + error OnlySystemContract(address systemContract); + error OnlyZeroGasPrice(); + error UnknownParam(string key, bytes value); + event CallExecuted(bytes32 indexed id, uint256 indexed index, address target, uint256 value, bytes data); event CallSalt(bytes32 indexed id, bytes32 salt); event CallScheduled( diff --git a/test/utils/interface/IGovToken.sol b/test/utils/interface/IGovToken.sol index 08464258..a9d4f7f7 100644 --- a/test/utils/interface/IGovToken.sol +++ b/test/utils/interface/IGovToken.sol @@ -7,6 +7,14 @@ interface GovToken { uint224 votes; } + error ApproveNotAllowed(); + error InvalidValue(string key, bytes value); + error OnlyCoinbase(); + error OnlySystemContract(address systemContract); + error OnlyZeroGasPrice(); + error TransferNotAllowed(); + error UnknownParam(string key, bytes value); + event Approval(address indexed owner, address indexed spender, uint256 value); event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate); event DelegateVotesChanged(address indexed delegate, uint256 previousBalance, uint256 newBalance); @@ -54,7 +62,8 @@ interface GovToken { function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external; function symbol() external view returns (string memory); - function sync(address[] memory stakeCredits, address account) external; + function sync(address stakeCredit, address account) external; + function syncBatch(address[] memory stakeCredits, address account) external; function totalSupply() external view returns (uint256); function transfer(address to, uint256 amount) external returns (bool); function transferFrom(address from, address to, uint256 amount) external returns (bool); diff --git a/test/utils/interface/IStakeCredit.sol b/test/utils/interface/IStakeCredit.sol index 68087394..87ef64c6 100644 --- a/test/utils/interface/IStakeCredit.sol +++ b/test/utils/interface/IStakeCredit.sol @@ -8,8 +8,23 @@ interface StakeCredit { uint256 unlockTime; } + error ApproveNotAllowed(); error Empty(); + error InsufficientBalance(); + error InvalidValue(string key, bytes value); + error NoClaimableUnbondRequest(); + error NoUnbondRequest(); + error OnlyCoinbase(); + error OnlySystemContract(address systemContract); + error OnlyZeroGasPrice(); error OutOfBounds(); + error TransferFailed(); + error TransferNotAllowed(); + error UnknownParam(string key, bytes value); + error WrongInitContext(); + error ZeroAmount(); + error ZeroTotalPooledBNB(); + error ZeroTotalShares(); event Approval(address indexed owner, address indexed spender, uint256 value); event Initialized(uint8 version); @@ -27,7 +42,6 @@ interface StakeCredit { function distributeReward(uint64 commissionRate) external payable; function getPooledBNB(address account) external view returns (uint256); function getPooledBNBByShares(uint256 shares) external view returns (uint256); - function getSelfDelegationBNB() external view returns (uint256); function getSharesByPooledBNB(uint256 bnbAmount) external view returns (uint256); function increaseAllowance(address spender, uint256 addedValue) external returns (bool); function initialize(address _validator, string memory _moniker) external payable; diff --git a/test/utils/interface/IStakeHub.sol b/test/utils/interface/IStakeHub.sol index 95301931..01401d76 100644 --- a/test/utils/interface/IStakeHub.sol +++ b/test/utils/interface/IStakeHub.sol @@ -17,6 +17,33 @@ interface StakeHub { string details; } + error AlreadySlashed(); + error DelegationAmountTooSmall(); + error DuplicateConsensusAddress(); + error DuplicateVoteAddress(); + error InBlackList(); + error InvalidCommission(); + error InvalidConsensusAddress(); + error InvalidMoniker(); + error InvalidValue(string key, bytes value); + error InvalidVoteAddress(); + error JailTimeNotExpired(); + error NoMoreFelonyToday(); + error OnlyAssetProtector(); + error OnlyCoinbase(); + error OnlySelfDelegation(); + error OnlySystemContract(address systemContract); + error OnlyZeroGasPrice(); + error SameValidator(); + error SelfDelegationNotEnough(); + error StakeHubPaused(); + error UnknownParam(string key, bytes value); + error UpdateTooFrequently(); + error ValidatorExisted(); + error ValidatorNotExist(); + error ValidatorNotJailed(); + error ZeroShares(); + event Claimed(address indexed operatorAddress, address indexed delegator, uint256 bnbAmount); event CommissionRateEdited(address indexed operatorAddress, uint64 commissionRate); event ConsensusAddressEdited(address indexed operatorAddress, address indexed newConsensusAddress); @@ -53,6 +80,8 @@ interface StakeHub { receive() external payable; + function DEAD_ADDRESS() external view returns (address); + function INIT_LOCK_AMOUNT() external view returns (uint256); function addToBlackList(address account) external; function assetProtector() external view returns (address); function blackList(address) external view returns (bool); @@ -75,7 +104,6 @@ interface StakeHub { function editDescription(Description memory description) external; function editVoteAddress(bytes memory newVoteAddress, bytes memory blsProof) external; function felonyJailTime() external view returns (uint256); - function felonyPerDay() external view returns (uint256); function felonySlashAmount() external view returns (uint256); function getOperatorAddressByConsensusAddress(address consensusAddress) external view returns (address); function getOperatorAddressByVoteAddress(bytes memory voteAddress) external view returns (address); diff --git a/test/utils/interface/IStaking.sol b/test/utils/interface/IStaking.sol index 135f66b2..dfeb9ab2 100644 --- a/test/utils/interface/IStaking.sol +++ b/test/utils/interface/IStaking.sol @@ -70,7 +70,7 @@ interface Staking { function bscChainID() external view returns (uint16); function claimReward() external returns (uint256 amount); function claimUndelegated() external returns (uint256 amount); - function delegate(address validator, uint256 amount) external payable; + function delegate(address, uint256) external payable; function getDelegated(address delegator, address validator) external view returns (uint256); function getDistributedReward(address delegator) external view returns (uint256); function getMinDelegation() external view returns (uint256); @@ -87,7 +87,7 @@ interface Staking { function handleFailAckPackage(uint8, bytes memory msgBytes) external; function handleSynPackage(uint8, bytes memory msgBytes) external returns (bytes memory); function minDelegation() external view returns (uint256); - function redelegate(address validatorSrc, address validatorDst, uint256 amount) external payable; + function redelegate(address, address, uint256) external payable; function relayerFee() external view returns (uint256); function transferGas() external view returns (uint256); function undelegate(address validator, uint256 amount) external payable;