diff --git a/abi/stakehub.abi b/abi/stakehub.abi index 4bad86d5c..c10b65f00 100644 --- a/abi/stakehub.abi +++ b/abi/stakehub.abi @@ -649,6 +649,25 @@ "outputs": [], "stateMutability": "nonpayable" }, + { + "type": "function", + "name": "isMonikerExisted", + "inputs": [ + { + "name": "moniker", + "type": "string", + "internalType": "string" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, { "type": "function", "name": "isPaused", @@ -1342,6 +1361,11 @@ "name": "DuplicateConsensusAddress", "inputs": [] }, + { + "type": "error", + "name": "DuplicateMoniker", + "inputs": [] + }, { "type": "error", "name": "DuplicateVoteAddress", diff --git a/contracts/BC_fusion/StakeHub.sol b/contracts/BC_fusion/StakeHub.sol index 7a1e24cb2..cbc09d120 100644 --- a/contracts/BC_fusion/StakeHub.sol +++ b/contracts/BC_fusion/StakeHub.sol @@ -49,6 +49,8 @@ contract StakeHub is System, Initializable { error DuplicateConsensusAddress(); // @notice signature: 0x11fdb947 error DuplicateVoteAddress(); + // @notice signature: 0xc0bf4143 + error DuplicateMoniker(); // @notice signature: 0x2f64097e error SelfDelegationNotEnough(); // @notice signature: 0xdc81db85 @@ -106,6 +108,8 @@ contract StakeHub is System, Initializable { EnumerableSet.AddressSet private _validatorSet; // validator operator address => validator info mapping(address => Validator) private _validators; + // validator moniker set(hash of the moniker) + mapping(bytes32 => bool) private _monikerSet; // validator consensus address => validator operator address mapping(address => address) public consensusToOperator; // validator consensus address => expiry date @@ -277,6 +281,8 @@ contract StakeHub is System, Initializable { if (voteToOperator[voteAddress] != address(0) || _legacyVoteAddress[voteAddress]) { revert DuplicateVoteAddress(); } + bytes32 monikerHash = keccak256(abi.encodePacked(description.moniker)); + if (_monikerSet[monikerHash]) revert DuplicateMoniker(); uint256 delegation = msg.value; if (delegation < minSelfDelegationBNB + LOCK_AMOUNT) revert SelfDelegationNotEnough(); @@ -294,6 +300,7 @@ contract StakeHub is System, Initializable { address creditContract = _deployStakeCredit(operatorAddress, description.moniker); _validatorSet.add(operatorAddress); + _monikerSet[monikerHash] = true; Validator storage valInfo = _validators[operatorAddress]; valInfo.consensusAddress = consensusAddress; valInfo.operatorAddress = operatorAddress; @@ -477,7 +484,7 @@ contract StakeHub is System, Initializable { /** * @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 redeloperatorAddressegated + * @param shares the shares to be redelegated * @param delegateVotePower whether to delegate vote power to the dstValidator */ function redelegate( @@ -884,6 +891,10 @@ contract StakeHub is System, Initializable { } } + function isMonikerExisted(string calldata moniker) external view returns (bool) { + return _monikerSet[keccak256(abi.encodePacked(moniker))]; + } + /*----------------- internal functions -----------------*/ function _checkMoniker(string memory moniker) internal pure returns (bool) { bytes memory bz = bytes(moniker); @@ -954,7 +965,7 @@ contract StakeHub is System, Initializable { return; } if (IStakeCredit(valInfo.creditContract).getPooledBNB(operatorAddress) < minSelfDelegationBNB) { - _jailValidator(valInfo, 0); + _jailValidator(valInfo, downtimeJailTime); // need to inform BSCValidatorSet contract to remove the validator IBSCValidatorSet(VALIDATOR_CONTRACT_ADDR).jailValidator(valInfo.consensusAddress); } diff --git a/package.json b/package.json index 92259636d..8716f74ff 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "scripts": { "lint:check": "forge fmt ./contracts/BC_fusion --check", "lint:write": "forge fmt ./contracts/BC_fusion", - "generate:dev": "poetry run python -m scripts.generate dev --breath-block-interval \"1 minutes\" --block-interval \"1 seconds\" --init-bc-consensus-addresses 'hex\"00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb9226600000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc\"' --init-bc-vote-addresses 'hex\"00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000030b86b3146bdd2200b1dbdb1cea5e40d3451c028cbb4fb03b1826f7f2d82bee76bbd5cd68a74a16a7eceea093fd5826b9200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003087ce273bb9b51fd69e50de7a8d9a99cfb3b1a5c6a7b85f6673d137a5a2ce7df3d6ee4e6d579a142d58b0606c4a7a1c27000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030a33ac14980d85c0d154c5909ebf7a11d455f54beb4d5d0dc1d8b3670b9c4a6b6c450ee3d623ecc48026f09ed1f0b5c1200000000000000000000000000000000\"' --asset-protector \"0xdF87F0e2B8519Ea2DD4aBd8B639cdD628497eD25\" --unbond-period \"2 minutes\" --downtime-jail-time \"2 minutes\" --felony-jail-time \"3 minutes\" --init-voting-delay \"1 minutes / BLOCK_INTERVAL\" --init-voting-period \"2 minutes / BLOCK_INTERVAL\" --init-min-period-after-quorum \"uint64(1 minutes / BLOCK_INTERVAL)\" --governor-protector \"0xdF87F0e2B8519Ea2DD4aBd8B639cdD628497eD25\" --init-minimal-delay \"1 minutes\"" + "generate:dev": "poetry run python -m scripts.generate dev --init-felony-slash-scope \"60\" --breathe-block-interval \"1 minutes\" --block-interval \"1 seconds\" --init-bc-consensus-addresses 'hex\"00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb9226600000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc\"' --init-bc-vote-addresses 'hex\"00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000030b86b3146bdd2200b1dbdb1cea5e40d3451c028cbb4fb03b1826f7f2d82bee76bbd5cd68a74a16a7eceea093fd5826b9200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003087ce273bb9b51fd69e50de7a8d9a99cfb3b1a5c6a7b85f6673d137a5a2ce7df3d6ee4e6d579a142d58b0606c4a7a1c27000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030a33ac14980d85c0d154c5909ebf7a11d455f54beb4d5d0dc1d8b3670b9c4a6b6c450ee3d623ecc48026f09ed1f0b5c1200000000000000000000000000000000\"' --asset-protector \"0xdF87F0e2B8519Ea2DD4aBd8B639cdD628497eD25\" --unbond-period \"2 minutes\" --downtime-jail-time \"2 minutes\" --felony-jail-time \"3 minutes\" --init-voting-delay \"1 minutes / BLOCK_INTERVAL\" --init-voting-period \"2 minutes / BLOCK_INTERVAL\" --init-min-period-after-quorum \"uint64(1 minutes / BLOCK_INTERVAL)\" --governor-protector \"0xdF87F0e2B8519Ea2DD4aBd8B639cdD628497eD25\" --init-minimal-delay \"1 minutes\"" }, "dependencies": { "@openzeppelin/contracts": "^4.9.3", diff --git a/scripts/generate.py b/scripts/generate.py index 74d0ce2a1..4dbb45728 100644 --- a/scripts/generate.py +++ b/scripts/generate.py @@ -116,13 +116,15 @@ def generate_relayer_hub(whitelist_1, whitelist_2): insert(contract, "alreadyInit = true;", "\t\twhitelistInit();") -def generate_slash_indicator(): - if network == "dev": - contract = "SlashIndicator.sol" - backup_file( - os.path.join(work_dir, "contracts", contract), os.path.join(work_dir, "contracts", contract[:-4] + ".bak") - ) +def generate_slash_indicator(init_felony_slash_scope): + contract = "SlashIndicator.sol" + backup_file( + os.path.join(work_dir, "contracts", contract), os.path.join(work_dir, "contracts", contract[:-4] + ".bak") + ) + replace_parameter(contract, "uint256 public constant INIT_FELONY_SLASH_SCOPE", f"{init_felony_slash_scope}") + + if network == "dev": insert(contract, "alreadyInit = true;", "\t\tenableMaliciousVoteSlash = true;") @@ -348,6 +350,7 @@ def dev( str, typer.Option(help="whitelist relayer2's address")] = "0x316b2Fa7C8a2ab7E21110a4B3f58771C01A71344", source_chain_id: Annotated[ str, typer.Option(help="source chain id of the token recover portal")] = "Binance-Chain-Ganges", + init_felony_slash_scope: str = "86400", breathe_block_interval: Annotated[str, typer.Option(help="breath block interval of Parlia")] = "1 days", block_interval: Annotated[str, typer.Option(help="block interval of Parlia")] = "3 seconds", init_bc_consensus_addresses: @@ -390,7 +393,7 @@ def dev( generate_system() generate_cross_chain() generate_system_reward() - generate_slash_indicator() + generate_slash_indicator(init_felony_slash_scope) generate_relayer_hub(whitelist_1, whitelist_2) generate_tendermint_light_client(init_consensus_bytes) generate_validator_set(init_burn_ratio, init_validatorset_bytes) diff --git a/test/StakeHub.t.sol b/test/StakeHub.t.sol index 82e7a630a..3b5a36c30 100644 --- a/test/StakeHub.t.sol +++ b/test/StakeHub.t.sol @@ -35,24 +35,69 @@ contract StakeHubTest is Deployer { stakeHub.initialize(); } - function testCreateAndEditValidator() public { - // 1. create validator + function testCreateValidator() public { + // create validator success + (address validator,) = _createValidator(2000 ether); + (address consensusAddress,,, bytes memory voteAddress,,) = stakeHub.getValidatorBasicInfo(validator); + + // create failed with duplicate consensus address + uint256 delegation = 2000 ether; + address operatorAddress = _getNextUserAddress(); + StakeHub.Commission memory commission = StakeHub.Commission({ rate: 10, maxRate: 100, maxChangeRate: 5 }); + StakeHub.Description memory description = StakeHub.Description({ + moniker: string.concat("T", vm.toString(uint24(uint160(operatorAddress)))), + identity: vm.toString(operatorAddress), + website: vm.toString(operatorAddress), + details: vm.toString(operatorAddress) + }); + bytes memory blsPubKey = bytes.concat( + hex"00000000000000000000000000000000000000000000000000000000", abi.encodePacked(operatorAddress) + ); + bytes memory blsProof = new bytes(96); + + uint256 toLock = stakeHub.LOCK_AMOUNT(); + vm.prank(operatorAddress); + vm.expectRevert(); + stakeHub.createValidator{ value: delegation + toLock }( + consensusAddress, blsPubKey, blsProof, commission, description + ); + + // create failed with duplicate vote address + consensusAddress = address(uint160(uint256(keccak256(blsPubKey)))); + vm.prank(operatorAddress); + vm.expectRevert(); + stakeHub.createValidator{ value: delegation + toLock }( + consensusAddress, voteAddress, blsProof, commission, description + ); + + // create failed with duplicate moniker + description = stakeHub.getValidatorDescription(validator); + vm.prank(operatorAddress); + vm.expectRevert(); + stakeHub.createValidator{ value: delegation + toLock }( + consensusAddress, voteAddress, blsProof, commission, description + ); + } + + function testEditValidator() public { + // create validator (address validator,) = _createValidator(2000 ether); vm.startPrank(validator); + // edit failed because of `UpdateTooFrequently` vm.expectRevert(); stakeHub.editConsensusAddress(address(1)); - // 2. edit consensus address + // edit consensus address vm.warp(block.timestamp + 1 days); address newConsensusAddress = address(0x1234); vm.expectEmit(true, true, false, true, address(stakeHub)); emit ConsensusAddressEdited(validator, newConsensusAddress); stakeHub.editConsensusAddress(newConsensusAddress); - (address realAddr,,,,,) = stakeHub.getValidatorBasicInfo(validator); - assertEq(realAddr, newConsensusAddress); + (address realConsensusAddr,,,,,) = stakeHub.getValidatorBasicInfo(validator); + assertEq(realConsensusAddr, newConsensusAddress); - // 3. edit commission rate + // edit commission rate vm.warp(block.timestamp + 1 days); vm.expectRevert(); stakeHub.editCommissionRate(110); @@ -64,7 +109,7 @@ contract StakeHubTest is Deployer { StakeHub.Commission memory realComm = stakeHub.getValidatorCommission(validator); assertEq(realComm.rate, 15); - // 4. edit description + // edit description vm.warp(block.timestamp + 1 days); StakeHub.Description memory description = stakeHub.getValidatorDescription(validator); description.moniker = "Test"; @@ -73,10 +118,10 @@ contract StakeHubTest is Deployer { emit DescriptionEdited(validator); stakeHub.editDescription(description); StakeHub.Description memory realDesc = stakeHub.getValidatorDescription(validator); - assertNotEq(realDesc.moniker, "Test"); // edit moniker is not allowed + assertNotEq(realDesc.moniker, "Test"); // edit moniker will be ignored assertEq(realDesc.website, "Test"); - // 5. edit vote address + // edit vote address vm.warp(block.timestamp + 1 days); bytes memory newVoteAddress = hex"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001234"; diff --git a/test/utils/interface/IStakeHub.sol b/test/utils/interface/IStakeHub.sol index a3c09bceb..30e92b136 100644 --- a/test/utils/interface/IStakeHub.sol +++ b/test/utils/interface/IStakeHub.sol @@ -21,6 +21,7 @@ interface StakeHub { error ConsensusAddressExpired(); error DelegationAmountTooSmall(); error DuplicateConsensusAddress(); + error DuplicateMoniker(); error DuplicateVoteAddress(); error InBlackList(); error InvalidCommission(); @@ -139,6 +140,7 @@ interface StakeHub { function getValidatorRewardRecord(address operatorAddress, uint256 index) external view returns (uint256); function getValidatorTotalPooledBNBRecord(address operatorAddress, uint256 index) external view returns (uint256); function initialize() external; + function isMonikerExisted(string memory moniker) external view returns (bool); function isPaused() external view returns (bool); function maliciousVoteSlash(bytes memory voteAddress) external; function maxElectedValidators() external view returns (uint256);