diff --git a/contracts/BC_fusion/StakeHub.sol b/contracts/BC_fusion/StakeHub.sol index 46316c52..7a0ef4e5 100644 --- a/contracts/BC_fusion/StakeHub.sol +++ b/contracts/BC_fusion/StakeHub.sol @@ -89,6 +89,10 @@ contract StakeHub is System, Initializable, Protectable { error ConsensusAddressExpired(); // @notice signature: 0x0d7b78d4 error InvalidSynPackage(); + // @notice signature: 0xbebdc757 + error InvalidAgent(); + // @notice signature: 0x682a6e7c + error InvalidValidator(); /*----------------- storage -----------------*/ uint8 private _receiveFundStatus; @@ -135,6 +139,9 @@ contract StakeHub is System, Initializable, Protectable { // slash key => slash jail time mapping(bytes32 => uint256) private _felonyRecords; + // agent => validator operator address + mapping(address => address) public agentToOperator; + /*----------------- structs and events -----------------*/ struct StakeMigrationPackage { address operatorAddress; // the operator address of the target validator to delegate to @@ -162,7 +169,9 @@ contract StakeHub is System, Initializable, Protectable { bool jailed; uint256 jailUntil; uint256 updateTime; - uint256[20] __reservedSlots; + // The agent can perform transactions on behalf of the operatorAddress in certain scenarios. + address agent; + uint256[19] __reservedSlots; } struct Description { @@ -219,6 +228,7 @@ contract StakeHub is System, Initializable, Protectable { address indexed operatorAddress, address indexed delegator, uint256 bnbAmount, StakeMigrationRespCode respCode ); event UnexpectedPackage(uint8 channelId, bytes msgBytes); + event AgentChanged(address indexed operatorAddress, address indexed oldAgent, address indexed newAgent); /*----------------- modifiers -----------------*/ modifier validatorExist(address operatorAddress) { @@ -315,6 +325,24 @@ contract StakeHub is System, Initializable, Protectable { } /*----------------- external functions -----------------*/ + function updateAgent(address newAgent) external validatorExist(msg.sender) { + if (agentToOperator[newAgent] != address(0)) revert InvalidAgent(); + if (_validatorSet.contains(newAgent)) revert InvalidAgent(); + + address operatorAddress = msg.sender; + address oldAgent = _validators[operatorAddress].agent; + if (oldAgent == newAgent) revert InvalidAgent(); + + if (oldAgent != address(0)) { + delete agentToOperator[oldAgent]; + } + + _validators[operatorAddress].agent = newAgent; + agentToOperator[newAgent] = operatorAddress; + + emit AgentChanged(operatorAddress, oldAgent, newAgent); + } + /** * @param consensusAddress the consensus address of the validator * @param voteAddress the vote address of the validator @@ -332,6 +360,8 @@ contract StakeHub is System, Initializable, Protectable { // basic check address operatorAddress = msg.sender; if (_validatorSet.contains(operatorAddress)) revert ValidatorExisted(); + if (agentToOperator[operatorAddress] != address(0)) revert InvalidValidator(); + if (consensusToOperator[consensusAddress] != address(0) || _legacyConsensusAddress[consensusAddress]) { revert DuplicateConsensusAddress(); } @@ -384,14 +414,14 @@ contract StakeHub is System, Initializable, Protectable { external whenNotPaused notInBlackList - validatorExist(msg.sender) + validatorExist(_bep410MsgSender()) { if (newConsensusAddress == address(0)) revert InvalidConsensusAddress(); if (consensusToOperator[newConsensusAddress] != address(0) || _legacyConsensusAddress[newConsensusAddress]) { revert DuplicateConsensusAddress(); } - address operatorAddress = msg.sender; + address operatorAddress = _bep410MsgSender(); Validator storage valInfo = _validators[operatorAddress]; if (valInfo.updateTime + BREATHE_BLOCK_INTERVAL > block.timestamp) revert UpdateTooFrequently(); @@ -410,9 +440,9 @@ contract StakeHub is System, Initializable, Protectable { external whenNotPaused notInBlackList - validatorExist(msg.sender) + validatorExist(_bep410MsgSender()) { - address operatorAddress = msg.sender; + address operatorAddress = _bep410MsgSender(); Validator storage valInfo = _validators[operatorAddress]; if (valInfo.updateTime + BREATHE_BLOCK_INTERVAL > block.timestamp) revert UpdateTooFrequently(); @@ -436,9 +466,9 @@ contract StakeHub is System, Initializable, Protectable { external whenNotPaused notInBlackList - validatorExist(msg.sender) + validatorExist(_bep410MsgSender()) { - address operatorAddress = msg.sender; + address operatorAddress = _bep410MsgSender(); Validator storage valInfo = _validators[operatorAddress]; if (valInfo.updateTime + BREATHE_BLOCK_INTERVAL > block.timestamp) revert UpdateTooFrequently(); @@ -456,9 +486,9 @@ contract StakeHub is System, Initializable, Protectable { function editVoteAddress( bytes calldata newVoteAddress, bytes calldata blsProof - ) external whenNotPaused notInBlackList validatorExist(msg.sender) { + ) external whenNotPaused notInBlackList validatorExist(_bep410MsgSender()) { // proof-of-possession verify - address operatorAddress = msg.sender; + address operatorAddress = _bep410MsgSender(); if (!_checkVoteAddress(operatorAddress, newVoteAddress, blsProof)) revert InvalidVoteAddress(); if (voteToOperator[newVoteAddress] != address(0) || _legacyVoteAddress[newVoteAddress]) { revert DuplicateVoteAddress(); @@ -1179,4 +1209,12 @@ contract StakeHub is System, Initializable, Protectable { uint256 bnbAmount = IStakeCredit(_validators[operatorAddress].creditContract).claim(msg.sender, requestNumber); emit Claimed(operatorAddress, msg.sender, bnbAmount); } + + function _bep410MsgSender() internal view returns (address) { + if (agentToOperator[msg.sender] != address(0)) { + return agentToOperator[msg.sender]; + } + + return msg.sender; + } } diff --git a/test/StakeHub.t.sol b/test/StakeHub.t.sol index 247c9a63..92d42b43 100644 --- a/test/StakeHub.t.sol +++ b/test/StakeHub.t.sol @@ -27,6 +27,7 @@ contract StakeHubTest is Deployer { event MigrateFailed( address indexed operatorAddress, address indexed delegator, uint256 bnbAmount, StakeMigrationRespCode respCode ); + event AgentChanged(address indexed operatorAddress, address indexed oldAgent, address indexed newAgent); enum StakeMigrationRespCode { MIGRATE_SUCCESS, @@ -726,4 +727,73 @@ contract StakeHubTest is Deployer { elements[1] = vals.encodeList(); return elements.encodeList(); } + + function testAgent() external { + // create validator + (address validator,,,) = _createValidator(2000 ether); + vm.startPrank(validator); + + // edit failed because of `UpdateTooFrequently` + vm.expectRevert(StakeHub.UpdateTooFrequently.selector); + stakeHub.editConsensusAddress(address(1)); + + // update agent + address newAgent = validator; + vm.expectRevert(StakeHub.InvalidAgent.selector); + stakeHub.updateAgent(newAgent); + + newAgent = address(0x1234); + vm.expectEmit(true, true, false, true, address(stakeHub)); + emit AgentChanged(validator, address(0), newAgent); + stakeHub.updateAgent(newAgent); + + vm.stopPrank(); + + vm.startPrank(newAgent); + // 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 realConsensusAddr = stakeHub.getValidatorConsensusAddress(validator); + assertEq(realConsensusAddr, newConsensusAddress); + + // edit commission rate + vm.warp(block.timestamp + 1 days); + vm.expectRevert(StakeHub.InvalidCommission.selector); + stakeHub.editCommissionRate(110); + vm.expectRevert(StakeHub.InvalidCommission.selector); + stakeHub.editCommissionRate(16); + vm.expectEmit(true, false, false, true, address(stakeHub)); + emit CommissionRateEdited(validator, 15); + stakeHub.editCommissionRate(15); + StakeHub.Commission memory realComm = stakeHub.getValidatorCommission(validator); + assertEq(realComm.rate, 15); + + // edit description + vm.warp(block.timestamp + 1 days); + StakeHub.Description memory description = stakeHub.getValidatorDescription(validator); + description.moniker = "Test"; + description.website = "Test"; + vm.expectEmit(true, false, false, true, address(stakeHub)); + emit DescriptionEdited(validator); + stakeHub.editDescription(description); + StakeHub.Description memory realDesc = stakeHub.getValidatorDescription(validator); + assertNotEq(realDesc.moniker, "Test"); // edit moniker will be ignored + assertEq(realDesc.website, "Test"); + + // edit vote address + vm.warp(block.timestamp + 1 days); + bytes memory newVoteAddress = + hex"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001234"; + bytes memory blsProof = new bytes(96); + vm.expectEmit(true, false, false, true, address(stakeHub)); + emit VoteAddressEdited(validator, newVoteAddress); + stakeHub.editVoteAddress(newVoteAddress, blsProof); + bytes memory realVoteAddr = stakeHub.getValidatorVoteAddress(validator); + assertEq(realVoteAddr, newVoteAddress); + + vm.stopPrank(); + } } diff --git a/test/utils/interface/IStakeHub.sol b/test/utils/interface/IStakeHub.sol index 45968a8b..28c829be 100644 --- a/test/utils/interface/IStakeHub.sol +++ b/test/utils/interface/IStakeHub.sol @@ -51,6 +51,8 @@ interface StakeHub { error ValidatorNotJailed(); error VoteAddressExpired(); error ZeroShares(); + error InvalidAgent(); + error InvalidValidator(); event BlackListed(address indexed target); event Claimed(address indexed operatorAddress, address indexed delegator, uint256 bnbAmount); @@ -94,6 +96,7 @@ interface StakeHub { ); event ValidatorUnjailed(address indexed operatorAddress); event VoteAddressEdited(address indexed operatorAddress, bytes newVoteAddress); + event AgentChanged(address indexed operatorAddress, address indexed oldAgent, address indexed newAgent); receive() external payable; @@ -179,4 +182,7 @@ interface StakeHub { function updateParam(string memory key, bytes memory value) external; function voteExpiration(bytes memory) external view returns (uint256); function voteToOperator(bytes memory) external view returns (address); + + function agentToOperator(address) external view returns (address); + function updateAgent(address newAgent) external; }