diff --git a/contracts/extensions/RONTransferHelper.sol b/contracts/extensions/RONTransferHelper.sol index 766110845..949596dce 100644 --- a/contracts/extensions/RONTransferHelper.sol +++ b/contracts/extensions/RONTransferHelper.sol @@ -3,12 +3,17 @@ pragma solidity ^0.8.9; abstract contract RONTransferHelper { + /// @dev Error of recipient not accepting RON when transfer RON. + error ErrRecipientRevert(); + /// @dev Error of sender has insufficient balance. + error ErrInsufficientBalance(); + /** * @dev See `_sendRON`. * Reverts if the recipient does not receive RON. */ function _transferRON(address payable _recipient, uint256 _amount) internal { - require(_sendRON(_recipient, _amount), "RONTransfer: unable to transfer value, recipient may have reverted"); + if (!_sendRON(_recipient, _amount)) revert ErrRecipientRevert(); } /** @@ -20,7 +25,7 @@ abstract contract RONTransferHelper { * */ function _sendRON(address payable _recipient, uint256 _amount) internal returns (bool _success) { - require(address(this).balance >= _amount, "RONTransfer: insufficient balance"); + if (address(this).balance < _amount) revert ErrInsufficientBalance(); return _unsafeSendRON(_recipient, _amount); } @@ -37,4 +42,15 @@ abstract contract RONTransferHelper { function _unsafeSendRON(address payable _recipient, uint256 _amount) internal returns (bool _success) { (_success, ) = _recipient.call{ value: _amount }(""); } + + /** + * @dev Same purpose with {_unsafeSendRON(address,uin256)} but containing gas limit stipend forwarded in the call. + */ + function _unsafeSendRON( + address payable _recipient, + uint256 _amount, + uint256 _gas + ) internal returns (bool _success) { + (_success, ) = _recipient.call{ value: _amount, gas: _gas }(""); + } } diff --git a/contracts/extensions/collections/HasBridgeContract.sol b/contracts/extensions/collections/HasBridgeContract.sol index f72b1cbeb..74b10cad2 100644 --- a/contracts/extensions/collections/HasBridgeContract.sol +++ b/contracts/extensions/collections/HasBridgeContract.sol @@ -9,7 +9,7 @@ contract HasBridgeContract is IHasBridgeContract, HasProxyAdmin { IBridge internal _bridgeContract; modifier onlyBridgeContract() { - require(bridgeContract() == msg.sender, "HasBridgeContract: method caller must be bridge contract"); + if (bridgeContract() != msg.sender) revert ErrCallerMustBeBridgeContract(); _; } @@ -24,7 +24,7 @@ contract HasBridgeContract is IHasBridgeContract, HasProxyAdmin { * @inheritdoc IHasBridgeContract */ function setBridgeContract(address _addr) external virtual override onlyAdmin { - require(_addr.code.length > 0, "HasBridgeContract: set to non-contract"); + if (_addr.code.length <= 0) revert ErrZeroCodeContract(); _setBridgeContract(_addr); } diff --git a/contracts/extensions/collections/HasBridgeTrackingContract.sol b/contracts/extensions/collections/HasBridgeTrackingContract.sol index 9450df87b..d31178f0f 100644 --- a/contracts/extensions/collections/HasBridgeTrackingContract.sol +++ b/contracts/extensions/collections/HasBridgeTrackingContract.sol @@ -9,10 +9,7 @@ contract HasBridgeTrackingContract is IHasBridgeTrackingContract, HasProxyAdmin IBridgeTracking internal _bridgeTrackingContract; modifier onlyBridgeTrackingContract() { - require( - bridgeTrackingContract() == msg.sender, - "HasBridgeTrackingContract: method caller must be bridge tracking contract" - ); + if (bridgeTrackingContract() != msg.sender) revert ErrCallerMustBeBridgeTrackingContract(); _; } @@ -27,7 +24,7 @@ contract HasBridgeTrackingContract is IHasBridgeTrackingContract, HasProxyAdmin * @inheritdoc IHasBridgeTrackingContract */ function setBridgeTrackingContract(address _addr) external virtual override onlyAdmin { - require(_addr.code.length > 0, "HasBridgeTrackingContract: set to non-contract"); + if (_addr.code.length == 0) revert ErrZeroCodeContract(); _setBridgeTrackingContract(_addr); } diff --git a/contracts/extensions/collections/HasMaintenanceContract.sol b/contracts/extensions/collections/HasMaintenanceContract.sol index 47637381c..42bf5fd8a 100644 --- a/contracts/extensions/collections/HasMaintenanceContract.sol +++ b/contracts/extensions/collections/HasMaintenanceContract.sol @@ -9,10 +9,7 @@ contract HasMaintenanceContract is IHasMaintenanceContract, HasProxyAdmin { IMaintenance internal _maintenanceContract; modifier onlyMaintenanceContract() { - require( - maintenanceContract() == msg.sender, - "HasMaintenanceContract: method caller must be scheduled maintenance contract" - ); + if (maintenanceContract() != msg.sender) revert ErrCallerMustBeMaintenanceContract(); _; } @@ -27,7 +24,7 @@ contract HasMaintenanceContract is IHasMaintenanceContract, HasProxyAdmin { * @inheritdoc IHasMaintenanceContract */ function setMaintenanceContract(address _addr) external override onlyAdmin { - require(_addr.code.length > 0, "HasMaintenanceContract: set to non-contract"); + if (_addr.code.length == 0) revert ErrZeroCodeContract(); _setMaintenanceContract(_addr); } diff --git a/contracts/extensions/collections/HasRoninGovernanceAdminContract.sol b/contracts/extensions/collections/HasRoninGovernanceAdminContract.sol index 92b3fd346..16fef2bf4 100644 --- a/contracts/extensions/collections/HasRoninGovernanceAdminContract.sol +++ b/contracts/extensions/collections/HasRoninGovernanceAdminContract.sol @@ -9,10 +9,7 @@ contract HasRoninGovernanceAdminContract is IHasRoninGovernanceAdminContract, Ha IRoninGovernanceAdmin internal _roninGovernanceAdminContract; modifier onlyRoninGovernanceAdminContract() { - require( - roninGovernanceAdminContract() == msg.sender, - "HasRoninGovernanceAdminContract: method caller must be ronin governance admin contract" - ); + if (roninGovernanceAdminContract() != msg.sender) revert ErrCallerMustBeGovernanceAdminContract(); _; } @@ -27,7 +24,7 @@ contract HasRoninGovernanceAdminContract is IHasRoninGovernanceAdminContract, Ha * @inheritdoc IHasRoninGovernanceAdminContract */ function setRoninGovernanceAdminContract(address _addr) external override onlyAdmin { - require(_addr.code.length > 0, "HasRoninGovernanceAdminContract: set to non-contract"); + if (_addr.code.length == 0) revert ErrZeroCodeContract(); _setRoninGovernanceAdminContract(_addr); } diff --git a/contracts/extensions/collections/HasRoninTrustedOrganizationContract.sol b/contracts/extensions/collections/HasRoninTrustedOrganizationContract.sol index dde7b2bdc..bc7b32c0a 100644 --- a/contracts/extensions/collections/HasRoninTrustedOrganizationContract.sol +++ b/contracts/extensions/collections/HasRoninTrustedOrganizationContract.sol @@ -9,10 +9,7 @@ contract HasRoninTrustedOrganizationContract is IHasRoninTrustedOrganizationCont IRoninTrustedOrganization internal _roninTrustedOrganizationContract; modifier onlyRoninTrustedOrganizationContract() { - require( - roninTrustedOrganizationContract() == msg.sender, - "HasRoninTrustedOrganizationContract: method caller must be ronin trusted organization contract" - ); + if (roninTrustedOrganizationContract() != msg.sender) revert ErrCallerMustBeRoninTrustedOrgContract(); _; } @@ -27,7 +24,7 @@ contract HasRoninTrustedOrganizationContract is IHasRoninTrustedOrganizationCont * @inheritdoc IHasRoninTrustedOrganizationContract */ function setRoninTrustedOrganizationContract(address _addr) external virtual override onlyAdmin { - require(_addr.code.length > 0, "HasRoninTrustedOrganizationContract: set to non-contract"); + if (_addr.code.length == 0) revert ErrZeroCodeContract(); _setRoninTrustedOrganizationContract(_addr); } diff --git a/contracts/extensions/collections/HasSlashIndicatorContract.sol b/contracts/extensions/collections/HasSlashIndicatorContract.sol index 14899d7f5..cb4d78b26 100644 --- a/contracts/extensions/collections/HasSlashIndicatorContract.sol +++ b/contracts/extensions/collections/HasSlashIndicatorContract.sol @@ -9,10 +9,7 @@ contract HasSlashIndicatorContract is IHasSlashIndicatorContract, HasProxyAdmin ISlashIndicator internal _slashIndicatorContract; modifier onlySlashIndicatorContract() { - require( - slashIndicatorContract() == msg.sender, - "HasSlashIndicatorContract: method caller must be slash indicator contract" - ); + if (slashIndicatorContract() != msg.sender) revert ErrCallerMustBeSlashIndicatorContract(); _; } @@ -27,7 +24,7 @@ contract HasSlashIndicatorContract is IHasSlashIndicatorContract, HasProxyAdmin * @inheritdoc IHasSlashIndicatorContract */ function setSlashIndicatorContract(address _addr) external override onlyAdmin { - require(_addr.code.length > 0, "HasSlashIndicatorContract: set to non-contract"); + if (_addr.code.length == 0) revert ErrZeroCodeContract(); _setSlashIndicatorContract(_addr); } diff --git a/contracts/extensions/collections/HasStakingContract.sol b/contracts/extensions/collections/HasStakingContract.sol index e87cd6566..07b4a22eb 100644 --- a/contracts/extensions/collections/HasStakingContract.sol +++ b/contracts/extensions/collections/HasStakingContract.sol @@ -9,7 +9,7 @@ contract HasStakingContract is IHasStakingContract, HasProxyAdmin { IStaking internal _stakingContract; modifier onlyStakingContract() { - require(stakingContract() == msg.sender, "HasStakingContract: method caller must be staking contract"); + if (stakingContract() != msg.sender) revert ErrCallerMustBeStakingContract(); _; } @@ -24,7 +24,7 @@ contract HasStakingContract is IHasStakingContract, HasProxyAdmin { * @inheritdoc IHasStakingContract */ function setStakingContract(address _addr) external override onlyAdmin { - require(_addr.code.length > 0, "HasStakingContract: set to non-contract"); + if (_addr.code.length == 0) revert ErrZeroCodeContract(); _setStakingContract(_addr); } diff --git a/contracts/extensions/collections/HasStakingVestingContract.sol b/contracts/extensions/collections/HasStakingVestingContract.sol index 57fcf9b84..5fefb215d 100644 --- a/contracts/extensions/collections/HasStakingVestingContract.sol +++ b/contracts/extensions/collections/HasStakingVestingContract.sol @@ -9,10 +9,7 @@ contract HasStakingVestingContract is IHasStakingVestingContract, HasProxyAdmin IStakingVesting internal _stakingVestingContract; modifier onlyStakingVestingContract() { - require( - stakingVestingContract() == msg.sender, - "HasStakingVestingContract: method caller must be staking vesting contract" - ); + if (stakingVestingContract() != msg.sender) revert ErrCallerMustBeStakingVestingContract(); _; } @@ -27,7 +24,7 @@ contract HasStakingVestingContract is IHasStakingVestingContract, HasProxyAdmin * @inheritdoc IHasStakingVestingContract */ function setStakingVestingContract(address _addr) external override onlyAdmin { - require(_addr.code.length > 0, "HasStakingVestingContract: set to non-contract"); + if (_addr.code.length == 0) revert ErrZeroCodeContract(); _setStakingVestingContract(_addr); } diff --git a/contracts/extensions/collections/HasValidatorContract.sol b/contracts/extensions/collections/HasValidatorContract.sol index 5e880c83e..3e8f61866 100644 --- a/contracts/extensions/collections/HasValidatorContract.sol +++ b/contracts/extensions/collections/HasValidatorContract.sol @@ -9,7 +9,7 @@ contract HasValidatorContract is IHasValidatorContract, HasProxyAdmin { IRoninValidatorSet internal _validatorContract; modifier onlyValidatorContract() { - require(validatorContract() == msg.sender, "HasValidatorContract: method caller must be validator contract"); + if (validatorContract() != msg.sender) revert ErrCallerMustBeValidatorContract(); _; } @@ -23,8 +23,8 @@ contract HasValidatorContract is IHasValidatorContract, HasProxyAdmin { /** * @inheritdoc IHasValidatorContract */ - function setValidatorContract(address _addr) external override onlyAdmin { - require(_addr.code.length > 0, "HasValidatorContract: set to non-contract"); + function setValidatorContract(address _addr) external virtual override onlyAdmin { + if (_addr.code.length == 0) revert ErrZeroCodeContract(); _setValidatorContract(_addr); } diff --git a/contracts/extensions/forwarder/Forwarder.sol b/contracts/extensions/forwarder/Forwarder.sol new file mode 100644 index 000000000..e8ca2ca25 --- /dev/null +++ b/contracts/extensions/forwarder/Forwarder.sol @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "./ForwarderLogic.sol"; +import "./ForwarderRole.sol"; + +contract Forwarder is ForwarderLogic, ForwarderRole { + /** + * @dev Initializes the forwarder with an initial target address and a contract admin. + */ + constructor(address __target, address __admin) payable { + _changeTargetTo(__target); + _changeAdminTo(__admin); + } + + modifier onlyModerator() { + require(_isModerator(msg.sender), "Forwarder: unauthorized call"); + _; + } + + modifier adminExecutesOrModeratorForwards() { + if (_isAdmin(msg.sender)) { + _; + } else { + require(_isModerator(msg.sender), "Forwarder: unauthorized call"); + _fallback(); + } + } + + /** + * @dev Forwards the call to the target (the `msg.value` is sent along in the call). + * + * Requirements: + * - Only moderator can invoke fallback method. + */ + fallback() external payable override onlyModerator { + _fallback(); + } + + /** + * @dev Receives RON transfer from all addresses. + */ + receive() external payable override {} + + /** + * @dev Returns the current admin. + * + * NOTE: Only the admin can call this function. See {ForwarderStorage-_getAdmin}. + * + * TIP: To get this value clients can read directly from the storage slot shown below using the + * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. + * `0xa8c82e6b38a127695961bbff56774712a221ab251224d4167eab01e23fcee6ca` + */ + function admin() external adminExecutesOrModeratorForwards returns (address admin_) { + admin_ = _getAdmin(); + } + + /** + * @dev Changes the admin of the forwarder. + * + * Emits an {AdminChanged} event. + * + * NOTE: Only the admin can call this function. See {ForwarderStorage-_changeAdminTo}. + */ + function changeAdminTo(address newAdmin) external virtual adminExecutesOrModeratorForwards { + _changeAdminTo(newAdmin); + } + + /** + * @dev Returns the current target. + * + * NOTE: Only the admin can call this function. See {ForwarderStorage-_getTarget}. + * + * TIP: To get this value clients can read directly from the storage slot shown below using the + * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. + * `0xcbec2a70e8f0a52aeb8f96e02517dc497e58d9a6fa86ab4056563f1e6baf3d3e` + */ + function moderator() external adminExecutesOrModeratorForwards returns (address moderator_) { + moderator_ = _getModerator(); + } + + /** + * @dev Changes the moderator of the forwarder. + * + * Emits an {ModeratorChanged} event. + * + * NOTE: Only the moderator can call this function. See {ForwarderStorage-_changeModeratorTo}. + */ + function changeModeratorTo(address newModerator) external virtual adminExecutesOrModeratorForwards { + _changeModeratorTo(newModerator); + } + + /** + * @dev Returns the current target. + * + * NOTE: Only the moderator can call this function. See {ForwarderStorage-_getTarget}. + * + * TIP: To get this value clients can read directly from the storage slot shown below using the + * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. + * `0x58221d865d4bfcbfe437720ee0c958ac3269c4e9c775f643bf474ed980d61168` + */ + function target() external adminExecutesOrModeratorForwards returns (address target_) { + target_ = _target(); + } + + /** + * @dev Changes the target of the forwarder. + * + * Emits an {TargetChanged} event. + * + * NOTE: Only the admin can call this function. See {ForwarderStorage-_changeTargetTo}. + */ + function changeTargetTo(address newTarget) external virtual adminExecutesOrModeratorForwards { + _changeTargetTo(newTarget); + } + + /** + * @dev Forwards the encoded call specified by `_data` to the target. The forwarder attachs `_val` value + * from the forwarder contract and sends along with the call. + * + * Requirements: + * - Only moderator can call this method. + */ + function functionCall(bytes memory _data, uint256 _val) external payable onlyModerator { + _functionCall(_data, _val); + } + + /** + * @dev Calls a function from the current forwarder to the target as specified by `_data`, which should be an encoded + * function call, with the value `_val`. + */ + function _functionCall(bytes memory _data, uint256 _val) internal { + require(_val <= address(this).balance, "Forwarder: invalid forwarding value"); + _call(_target(), _data, _val); + } + + /** + * @dev Returns the current target address. + */ + function _target() internal view virtual override returns (address target_) { + return _getTarget(); + } +} diff --git a/contracts/extensions/forwarder/ForwarderLogic.sol b/contracts/extensions/forwarder/ForwarderLogic.sol new file mode 100644 index 000000000..9d845ab54 --- /dev/null +++ b/contracts/extensions/forwarder/ForwarderLogic.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +abstract contract ForwarderLogic { + /** + * @dev Forwards the current call to `target`. + * + * This function does not return to its internal call site, it will return directly to the external caller. + */ + function _call( + address __target, + bytes memory __data, + uint256 __value + ) internal { + (bool _success, bytes memory _res) = __target.call{ value: __value }(__data); + + if (!_success) { + uint _size = _res.length; + require(_size >= 4, "Forwarder: target reverts silently"); + assembly { + _res := add(_res, 0x20) + revert(_res, _size) + } + } + } + + /** + * @dev This is a virtual function that should be overridden so it returns the address to which the fallback function + * and {_fallback} should forward. + */ + function _target() internal view virtual returns (address); + + /** + * @dev Forwards the current call to the address returned by `_target()`. + * + * This function does not return to its internal call site, it will return directly to the external caller. + */ + function _fallback() internal { + _call(_target(), msg.data, msg.value); + } + + /** + * @dev Fallback function that calls to the address returned by `_target()`. Will run if no other function in the + * contract matches the call data. + */ + fallback() external payable virtual { + _fallback(); + } + + /** + * @dev Fallback function that calls to the address returned by `_target()`. Will run if call data is empty. + */ + receive() external payable virtual { + _fallback(); + } +} diff --git a/contracts/extensions/forwarder/ForwarderRole.sol b/contracts/extensions/forwarder/ForwarderRole.sol new file mode 100644 index 000000000..a860d7f57 --- /dev/null +++ b/contracts/extensions/forwarder/ForwarderRole.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/utils/Address.sol"; +import "@openzeppelin/contracts/utils/StorageSlot.sol"; + +abstract contract ForwarderRole { + /// @dev Storage slot with the address of the current admin. This is the keccak-256 hash of "ronin.forwarder.admin" subtracted by 1. + bytes32 internal constant _ADMIN_SLOT = 0xa8c82e6b38a127695961bbff56774712a221ab251224d4167eab01e23fcee6ca; + /// @dev Storage slot with the address of the current target. This is the keccak-256 hash of "ronin.forwarder.target" subtracted by 1. + bytes32 internal constant _TARGET_SLOT = 0x58221d865d4bfcbfe437720ee0c958ac3269c4e9c775f643bf474ed980d61168; + /// @dev Storage slot with the address of the current target. This is the keccak-256 hash of "ronin.forwarder.moderator" subtracted by 1. + bytes32 internal constant _MODERATOR_SLOT = 0xcbec2a70e8f0a52aeb8f96e02517dc497e58d9a6fa86ab4056563f1e6baf3d3e; + + /// @dev Emitted when the target is changed. + event AdminChanged(address indexed admin); + /// @dev Emitted when the target is changed. + event TargetChanged(address indexed target); + /// @dev Emitted when the target is changed. + event ModeratorChanged(address indexed target); + + /** + * @dev Returns the current admin address. + */ + function _getAdmin() internal view returns (address) { + return StorageSlot.getAddressSlot(_ADMIN_SLOT).value; + } + + /** + * @dev Stores a new address in the admin slot. + */ + function _setAdmin(address newAdmin) private { + StorageSlot.getAddressSlot(_ADMIN_SLOT).value = newAdmin; + } + + function _isAdmin(address _addr) internal view returns (bool) { + return _addr == _getAdmin(); + } + + /** + * @dev Perform admin upgrade + * + * Emits an {AdminChanged} event. + */ + function _changeAdminTo(address newAdmin) internal { + _setAdmin(newAdmin); + emit AdminChanged(newAdmin); + } + + /** + * @dev Returns the current target address. + */ + function _getTarget() internal view returns (address) { + return StorageSlot.getAddressSlot(_TARGET_SLOT).value; + } + + /** + * @dev Stores a new address in the target slot. + */ + function _setTarget(address newTarget) private { + require(Address.isContract(newTarget), "ForwarderStorage: new target is not a contract"); + StorageSlot.getAddressSlot(_TARGET_SLOT).value = newTarget; + } + + /** + * @dev Perform target upgrade + * + * Emits an {TargetChanged} event. + */ + function _changeTargetTo(address newTarget) internal { + _setTarget(newTarget); + emit TargetChanged(newTarget); + } + + /** + * @dev Returns the current moderator address. + */ + function _getModerator() internal view returns (address) { + return StorageSlot.getAddressSlot(_MODERATOR_SLOT).value; + } + + /** + * @dev Stores a new address in the EIP1967 moderator slot. + */ + function _setModerator(address newModerator) private { + StorageSlot.getAddressSlot(_MODERATOR_SLOT).value = newModerator; + } + + /** + * @dev Perform moderator upgrade + * + * Emits an {ModeratorChanged} event. + */ + function _changeModeratorTo(address newModerator) internal { + _setModerator(newModerator); + emit ModeratorChanged(newModerator); + } + + function _isModerator(address _addr) internal view returns (bool) { + return _addr == _getModerator(); + } +} diff --git a/contracts/extensions/isolated-governance/IsolatedGovernance.sol b/contracts/extensions/isolated-governance/IsolatedGovernance.sol index 6951b8f81..5548e5c92 100644 --- a/contracts/extensions/isolated-governance/IsolatedGovernance.sol +++ b/contracts/extensions/isolated-governance/IsolatedGovernance.sol @@ -12,6 +12,10 @@ abstract contract IsolatedGovernance is VoteStatusConsumer { mapping(address => bytes32) voteHashOf; /// @dev Mapping from receipt hash => vote weight mapping(bytes32 => uint256) weight; + /// @dev The timestamp that voting is expired (no expiration=0) + uint256 expiredAt; + /// @dev The timestamp that voting is created + uint256 createdAt; } /** @@ -29,6 +33,11 @@ abstract contract IsolatedGovernance is VoteStatusConsumer { uint256 _minimumVoteWeight, bytes32 _hash ) internal virtual returns (VoteStatus _status) { + if (_proposal.expiredAt > 0 && _proposal.expiredAt <= block.timestamp) { + _proposal.status = VoteStatus.Expired; + return _proposal.status; + } + if (_voted(_proposal, _voter)) { revert( string(abi.encodePacked("IsolatedGovernance: ", Strings.toHexString(uint160(_voter), 20), " already voted")) diff --git a/contracts/extensions/isolated-governance/bridge-operator-governance/BOsGovernanceProposal.sol b/contracts/extensions/isolated-governance/bridge-operator-governance/BOsGovernanceProposal.sol index 798e47589..6fbb161cd 100644 --- a/contracts/extensions/isolated-governance/bridge-operator-governance/BOsGovernanceProposal.sol +++ b/contracts/extensions/isolated-governance/bridge-operator-governance/BOsGovernanceProposal.sol @@ -7,15 +7,15 @@ import "../../../libraries/BridgeOperatorsBallot.sol"; import "../../../interfaces/IRoninGovernanceAdmin.sol"; abstract contract BOsGovernanceProposal is SignatureConsumer, IsolatedGovernance, IRoninGovernanceAdmin { - /// @dev The last period that the brige operators synced. - uint256 internal _lastSyncedPeriod; - /// @dev Mapping from period index => bridge operators vote - mapping(uint256 => IsolatedVote) internal _vote; + /// @dev The last the brige operator set info. + BridgeOperatorsBallot.BridgeOperatorSet internal _lastSyncedBridgeOperatorSetInfo; + /// @dev Mapping from period index => epoch index => bridge operators vote + mapping(uint256 => mapping(uint256 => IsolatedVote)) internal _vote; /// @dev Mapping from bridge voter address => last block that the address voted mapping(address => uint256) internal _lastVotedBlock; - /// @dev Mapping from period => voter => signatures - mapping(uint256 => mapping(address => Signature)) internal _votingSig; + /// @dev Mapping from period index => epoch index => voter => signatures + mapping(uint256 => mapping(uint256 => mapping(address => Signature))) internal _votingSig; /** * @inheritdoc IRoninGovernanceAdmin @@ -24,6 +24,13 @@ abstract contract BOsGovernanceProposal is SignatureConsumer, IsolatedGovernance return _lastVotedBlock[_bridgeVoter]; } + /** + * @inheritdoc IRoninGovernanceAdmin + */ + function lastSyncedBridgeOperatorSetInfo() external view returns (BridgeOperatorsBallot.BridgeOperatorSet memory) { + return _lastSyncedBridgeOperatorSetInfo; + } + /** * @dev Votes for a set of bridge operators by signatures. * @@ -34,34 +41,41 @@ abstract contract BOsGovernanceProposal is SignatureConsumer, IsolatedGovernance * */ function _castVotesBySignatures( - address[] calldata _operators, + BridgeOperatorsBallot.BridgeOperatorSet calldata _ballot, Signature[] calldata _signatures, - uint256 _period, uint256 _minimumVoteWeight, bytes32 _domainSeperator ) internal { - require(_period >= _lastSyncedPeriod, "BOsGovernanceProposal: query for outdated period"); - require(_operators.length > 0 && _signatures.length > 0, "BOsGovernanceProposal: invalid array length"); + require( + _ballot.period >= _lastSyncedBridgeOperatorSetInfo.period && + _ballot.epoch >= _lastSyncedBridgeOperatorSetInfo.epoch, + "BOsGovernanceProposal: query for outdated bridge operator set" + ); + BridgeOperatorsBallot.verifyBallot(_ballot, _lastSyncedBridgeOperatorSetInfo); + require(_signatures.length > 0, "BOsGovernanceProposal: invalid array length"); - Signature memory _sig; address _signer; address _lastSigner; - bytes32 _hash = BridgeOperatorsBallot.hash(_period, _operators); + bytes32 _hash = BridgeOperatorsBallot.hash(_ballot); bytes32 _digest = ECDSA.toTypedDataHash(_domainSeperator, _hash); - IsolatedVote storage _v = _vote[_period]; + IsolatedVote storage _v = _vote[_ballot.period][_ballot.epoch]; + mapping(address => Signature) storage _signatureOf = _votingSig[_ballot.period][_ballot.epoch]; bool _hasValidVotes; for (uint256 _i = 0; _i < _signatures.length; _i++) { - _sig = _signatures[_i]; - _signer = ECDSA.recover(_digest, _sig.v, _sig.r, _sig.s); - require(_lastSigner < _signer, "BOsGovernanceProposal: invalid order"); - _lastSigner = _signer; + // Avoids stack too deeps + { + Signature calldata _sig = _signatures[_i]; + _signer = ECDSA.recover(_digest, _sig.v, _sig.r, _sig.s); + require(_lastSigner < _signer, "BOsGovernanceProposal: invalid signer order"); + _lastSigner = _signer; + } uint256 _weight = _getBridgeVoterWeight(_signer); if (_weight > 0) { _hasValidVotes = true; _lastVotedBlock[_signer] = block.number; - _votingSig[_period][_signer] = _sig; + _signatureOf[_signer] = _signatures[_i]; if (_castVote(_v, _signer, _weight, _minimumVoteWeight, _hash) == VoteStatus.Approved) { return; } diff --git a/contracts/extensions/isolated-governance/bridge-operator-governance/BOsGovernanceRelay.sol b/contracts/extensions/isolated-governance/bridge-operator-governance/BOsGovernanceRelay.sol index 4a33acccc..46390fb0d 100644 --- a/contracts/extensions/isolated-governance/bridge-operator-governance/BOsGovernanceRelay.sol +++ b/contracts/extensions/isolated-governance/bridge-operator-governance/BOsGovernanceRelay.sol @@ -6,10 +6,17 @@ import "../../../interfaces/consumers/SignatureConsumer.sol"; import "../../../libraries/BridgeOperatorsBallot.sol"; abstract contract BOsGovernanceRelay is SignatureConsumer, IsolatedGovernance { - /// @dev The last period that the brige operators synced. - uint256 internal _lastSyncedPeriod; - /// @dev Mapping from period index => bridge operators vote - mapping(uint256 => IsolatedVote) internal _vote; + /// @dev The last the brige operator set info. + BridgeOperatorsBallot.BridgeOperatorSet internal _lastSyncedBridgeOperatorSetInfo; + /// @dev Mapping from period index => epoch index => bridge operators vote + mapping(uint256 => mapping(uint256 => IsolatedVote)) internal _vote; + + /** + * @dev Returns the synced bridge operator set info. + */ + function lastSyncedBridgeOperatorSetInfo() external view returns (BridgeOperatorsBallot.BridgeOperatorSet memory) { + return _lastSyncedBridgeOperatorSetInfo; + } /** * @dev Relays votes by signatures. @@ -23,19 +30,23 @@ abstract contract BOsGovernanceRelay is SignatureConsumer, IsolatedGovernance { * */ function _relayVotesBySignatures( - address[] calldata _operators, + BridgeOperatorsBallot.BridgeOperatorSet calldata _ballot, Signature[] calldata _signatures, - uint256 _period, uint256 _minimumVoteWeight, bytes32 _domainSeperator ) internal { - require(_period > _lastSyncedPeriod, "BOsGovernanceRelay: query for outdated period"); - require(_operators.length > 0 && _signatures.length > 0, "BOsGovernanceRelay: invalid array length"); + require( + (_ballot.period >= _lastSyncedBridgeOperatorSetInfo.period && + _ballot.epoch > _lastSyncedBridgeOperatorSetInfo.epoch), + "BOsGovernanceRelay: query for outdated bridge operator set" + ); + BridgeOperatorsBallot.verifyBallot(_ballot, _lastSyncedBridgeOperatorSetInfo); + require(_signatures.length > 0, "BOsGovernanceRelay: invalid array length"); - Signature memory _sig; + Signature calldata _sig; address[] memory _signers = new address[](_signatures.length); address _lastSigner; - bytes32 _hash = BridgeOperatorsBallot.hash(_period, _operators); + bytes32 _hash = BridgeOperatorsBallot.hash(_ballot); bytes32 _digest = ECDSA.toTypedDataHash(_domainSeperator, _hash); for (uint256 _i = 0; _i < _signatures.length; _i++) { @@ -45,12 +56,12 @@ abstract contract BOsGovernanceRelay is SignatureConsumer, IsolatedGovernance { _lastSigner = _signers[_i]; } - IsolatedVote storage _v = _vote[_period]; + IsolatedVote storage _v = _vote[_ballot.period][_ballot.epoch]; uint256 _totalVoteWeight = _sumBridgeVoterWeights(_signers); if (_totalVoteWeight >= _minimumVoteWeight) { require(_totalVoteWeight > 0, "BOsGovernanceRelay: invalid vote weight"); _v.status = VoteStatus.Approved; - _lastSyncedPeriod = _period; + _lastSyncedBridgeOperatorSetInfo = _ballot; return; } diff --git a/contracts/extensions/sequential-governance/CoreGovernance.sol b/contracts/extensions/sequential-governance/CoreGovernance.sol index 196bfe91b..f29cc0978 100644 --- a/contracts/extensions/sequential-governance/CoreGovernance.sol +++ b/contracts/extensions/sequential-governance/CoreGovernance.sol @@ -22,6 +22,7 @@ abstract contract CoreGovernance is SignatureConsumer, VoteStatusConsumer, Chain address[] againstVoteds; // Array of addresses voting against uint256 expiryTimestamp; mapping(address => Signature) sig; + mapping(address => bool) voted; } /// @dev Emitted when a proposal is created @@ -108,10 +109,10 @@ abstract contract CoreGovernance is SignatureConsumer, VoteStatusConsumer, Chain bytes[] memory _calldatas, uint256[] memory _gasAmounts, address _creator - ) internal virtual returns (uint256 _round) { + ) internal virtual returns (Proposal.ProposalDetail memory _proposal) { require(_chainId != 0, "CoreGovernance: invalid chain id"); - Proposal.ProposalDetail memory _proposal = Proposal.ProposalDetail( + _proposal = Proposal.ProposalDetail( round[_chainId] + 1, _chainId, _expiryTimestamp, @@ -123,7 +124,7 @@ abstract contract CoreGovernance is SignatureConsumer, VoteStatusConsumer, Chain _proposal.validate(_proposalExpiryDuration); bytes32 _proposalHash = _proposal.hash(); - _round = _createVotingRound(_chainId, _proposalHash, _expiryTimestamp); + uint256 _round = _createVotingRound(_chainId, _proposalHash, _expiryTimestamp); emit ProposalCreated(_chainId, _round, _proposalHash, _proposal, _creator); } @@ -246,7 +247,11 @@ abstract contract CoreGovernance is SignatureConsumer, VoteStatusConsumer, Chain revert(string(abi.encodePacked("CoreGovernance: ", Strings.toHexString(uint160(_voter), 20), " already voted"))); } - _vote.sig[_voter] = _signature; + _vote.voted[_voter] = true; + // Stores the signature if it is not empty + if (_signature.r > 0 || _signature.s > 0 || _signature.v > 0) { + _vote.sig[_voter] = _signature; + } emit ProposalVoted(_vote.hash, _voter, _support, _voterWeight); uint256 _forVoteWeight; @@ -294,9 +299,11 @@ abstract contract CoreGovernance is SignatureConsumer, VoteStatusConsumer, Chain emit ProposalExpired(_proposalVote.hash); for (uint256 _i; _i < _proposalVote.forVoteds.length; _i++) { + delete _proposalVote.voted[_proposalVote.forVoteds[_i]]; delete _proposalVote.sig[_proposalVote.forVoteds[_i]]; } for (uint256 _i; _i < _proposalVote.againstVoteds.length; _i++) { + delete _proposalVote.voted[_proposalVote.againstVoteds[_i]]; delete _proposalVote.sig[_proposalVote.againstVoteds[_i]]; } delete _proposalVote.status; @@ -331,7 +338,7 @@ abstract contract CoreGovernance is SignatureConsumer, VoteStatusConsumer, Chain * @dev Returns whether the voter casted for the proposal. */ function _voted(ProposalVote storage _vote, address _voter) internal view returns (bool) { - return _vote.sig[_voter].r != 0; + return _vote.voted[_voter]; } /** diff --git a/contracts/extensions/sequential-governance/GovernanceProposal.sol b/contracts/extensions/sequential-governance/GovernanceProposal.sol index 787d50c9a..d4ba11ad4 100644 --- a/contracts/extensions/sequential-governance/GovernanceProposal.sol +++ b/contracts/extensions/sequential-governance/GovernanceProposal.sol @@ -26,7 +26,7 @@ abstract contract GovernanceProposal is CoreGovernance { address _lastSigner; address _signer; - Signature memory _sig; + Signature calldata _sig; bool _hasValidVotes; for (uint256 _i; _i < _signatures.length; _i++) { _sig = _signatures[_i]; diff --git a/contracts/extensions/sequential-governance/GovernanceRelay.sol b/contracts/extensions/sequential-governance/GovernanceRelay.sol index 358a973f3..8ef81531e 100644 --- a/contracts/extensions/sequential-governance/GovernanceRelay.sol +++ b/contracts/extensions/sequential-governance/GovernanceRelay.sol @@ -30,7 +30,7 @@ abstract contract GovernanceRelay is CoreGovernance { address _signer; address _lastSigner; Ballot.VoteType _support; - Signature memory _sig; + Signature calldata _sig; for (uint256 _i; _i < _signatures.length; _i++) { _sig = _signatures[_i]; diff --git a/contracts/interfaces/IRoninGovernanceAdmin.sol b/contracts/interfaces/IRoninGovernanceAdmin.sol index 01fb57581..a7fd8fa4f 100644 --- a/contracts/interfaces/IRoninGovernanceAdmin.sol +++ b/contracts/interfaces/IRoninGovernanceAdmin.sol @@ -1,9 +1,48 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; +import "../libraries/BridgeOperatorsBallot.sol"; + interface IRoninGovernanceAdmin { + /// @dev Emitted when the bridge operators are approved. + event BridgeOperatorsApproved(uint256 _period, uint256 _epoch, address[] _operators); + /// @dev Emitted when an emergency exit poll is created. + event EmergencyExitPollCreated( + bytes32 _voteHash, + address _consensusAddr, + address _recipientAfterUnlockedFund, + uint256 _requestedAt, + uint256 _expiredAt + ); + /// @dev Emitted when an emergency exit poll is approved. + event EmergencyExitPollApproved(bytes32 _voteHash); + /// @dev Emitted when an emergency exit poll is expired. + event EmergencyExitPollExpired(bytes32 _voteHash); + /** * @dev Returns the last voted block of the bridge voter. */ function lastVotedBlock(address _bridgeVoter) external view returns (uint256); + + /** + * @dev Returns the synced bridge operator set info. + */ + function lastSyncedBridgeOperatorSetInfo() + external + view + returns (BridgeOperatorsBallot.BridgeOperatorSet memory _bridgeOperatorSetInfo); + + /** + * @dev Create a vote to agree that an emergency exit is valid and should return the locked funds back.a + * + * Requirements: + * - The method caller is validator contract. + * + */ + function createEmergencyExitPoll( + address _consensusAddr, + address _recipientAfterUnlockedFund, + uint256 _requestedAt, + uint256 _expiredAt + ) external; } diff --git a/contracts/interfaces/collections/IHasBridgeContract.sol b/contracts/interfaces/collections/IHasBridgeContract.sol index d576a138c..654d26bfe 100644 --- a/contracts/interfaces/collections/IHasBridgeContract.sol +++ b/contracts/interfaces/collections/IHasBridgeContract.sol @@ -2,10 +2,15 @@ pragma solidity ^0.8.9; -interface IHasBridgeContract { +import "./IHasContract.sol"; + +interface IHasBridgeContract is IHasContract { /// @dev Emitted when the bridge contract is updated. event BridgeContractUpdated(address); + /// @dev Error of method caller must be bridge contract. + error ErrCallerMustBeBridgeContract(); + /** * @dev Returns the bridge contract. */ diff --git a/contracts/interfaces/collections/IHasBridgeTrackingContract.sol b/contracts/interfaces/collections/IHasBridgeTrackingContract.sol index e2811bd35..14b935764 100644 --- a/contracts/interfaces/collections/IHasBridgeTrackingContract.sol +++ b/contracts/interfaces/collections/IHasBridgeTrackingContract.sol @@ -2,10 +2,15 @@ pragma solidity ^0.8.9; -interface IHasBridgeTrackingContract { +import "./IHasContract.sol"; + +interface IHasBridgeTrackingContract is IHasContract { /// @dev Emitted when the bridge tracking contract is updated. event BridgeTrackingContractUpdated(address); + /// @dev Error of method caller must be bridge tracking contract. + error ErrCallerMustBeBridgeTrackingContract(); + /** * @dev Returns the bridge tracking contract. */ diff --git a/contracts/interfaces/collections/IHasContract.sol b/contracts/interfaces/collections/IHasContract.sol new file mode 100644 index 000000000..c59215a38 --- /dev/null +++ b/contracts/interfaces/collections/IHasContract.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.9; + +interface IHasContract { + /// @dev Error of set to non-contract. + error ErrZeroCodeContract(); +} diff --git a/contracts/interfaces/collections/IHasMaintenanceContract.sol b/contracts/interfaces/collections/IHasMaintenanceContract.sol index 604d213fd..0c4b149e8 100644 --- a/contracts/interfaces/collections/IHasMaintenanceContract.sol +++ b/contracts/interfaces/collections/IHasMaintenanceContract.sol @@ -2,10 +2,15 @@ pragma solidity ^0.8.9; -interface IHasMaintenanceContract { +import "./IHasContract.sol"; + +interface IHasMaintenanceContract is IHasContract { /// @dev Emitted when the maintenance contract is updated. event MaintenanceContractUpdated(address); + /// @dev Error of method caller must be maintenance contract. + error ErrCallerMustBeMaintenanceContract(); + /** * @dev Returns the maintenance contract. */ diff --git a/contracts/interfaces/collections/IHasRoninGovernanceAdminContract.sol b/contracts/interfaces/collections/IHasRoninGovernanceAdminContract.sol index 905cf66c9..dc22df13c 100644 --- a/contracts/interfaces/collections/IHasRoninGovernanceAdminContract.sol +++ b/contracts/interfaces/collections/IHasRoninGovernanceAdminContract.sol @@ -2,10 +2,15 @@ pragma solidity ^0.8.9; -interface IHasRoninGovernanceAdminContract { +import "./IHasContract.sol"; + +interface IHasRoninGovernanceAdminContract is IHasContract { /// @dev Emitted when the ronin governance admin contract is updated. event RoninGovernanceAdminContractUpdated(address); + /// @dev Error of method caller must be goverance admin contract. + error ErrCallerMustBeGovernanceAdminContract(); + /** * @dev Returns the ronin governance admin contract. */ diff --git a/contracts/interfaces/collections/IHasRoninTrustedOrganizationContract.sol b/contracts/interfaces/collections/IHasRoninTrustedOrganizationContract.sol index ba254716e..4e3bd98ba 100644 --- a/contracts/interfaces/collections/IHasRoninTrustedOrganizationContract.sol +++ b/contracts/interfaces/collections/IHasRoninTrustedOrganizationContract.sol @@ -2,10 +2,15 @@ pragma solidity ^0.8.9; -interface IHasRoninTrustedOrganizationContract { +import "./IHasContract.sol"; + +interface IHasRoninTrustedOrganizationContract is IHasContract { /// @dev Emitted when the ronin trusted organization contract is updated. event RoninTrustedOrganizationContractUpdated(address); + /// @dev Error of method caller must be Ronin trusted org contract. + error ErrCallerMustBeRoninTrustedOrgContract(); + /** * @dev Returns the ronin trusted organization contract. */ diff --git a/contracts/interfaces/collections/IHasSlashIndicatorContract.sol b/contracts/interfaces/collections/IHasSlashIndicatorContract.sol index 57d630b0a..8d30bfa41 100644 --- a/contracts/interfaces/collections/IHasSlashIndicatorContract.sol +++ b/contracts/interfaces/collections/IHasSlashIndicatorContract.sol @@ -2,10 +2,15 @@ pragma solidity ^0.8.9; -interface IHasSlashIndicatorContract { +import "./IHasContract.sol"; + +interface IHasSlashIndicatorContract is IHasContract { /// @dev Emitted when the slash indicator contract is updated. event SlashIndicatorContractUpdated(address); + /// @dev Error of method caller must be slash indicator contract. + error ErrCallerMustBeSlashIndicatorContract(); + /** * @dev Returns the slash indicator contract. */ diff --git a/contracts/interfaces/collections/IHasStakingContract.sol b/contracts/interfaces/collections/IHasStakingContract.sol index c285a99be..e9ddae203 100644 --- a/contracts/interfaces/collections/IHasStakingContract.sol +++ b/contracts/interfaces/collections/IHasStakingContract.sol @@ -2,10 +2,15 @@ pragma solidity ^0.8.9; -interface IHasStakingContract { +import "./IHasContract.sol"; + +interface IHasStakingContract is IHasContract { /// @dev Emitted when the staking contract is updated. event StakingContractUpdated(address); + /// @dev Error of method caller must be staking contract. + error ErrCallerMustBeStakingContract(); + /** * @dev Returns the staking contract. */ diff --git a/contracts/interfaces/collections/IHasStakingVestingContract.sol b/contracts/interfaces/collections/IHasStakingVestingContract.sol index 3fd56a781..e3bcfa9f7 100644 --- a/contracts/interfaces/collections/IHasStakingVestingContract.sol +++ b/contracts/interfaces/collections/IHasStakingVestingContract.sol @@ -2,10 +2,15 @@ pragma solidity ^0.8.9; -interface IHasStakingVestingContract { +import "./IHasContract.sol"; + +interface IHasStakingVestingContract is IHasContract { /// @dev Emitted when the staking vesting contract is updated. event StakingVestingContractUpdated(address); + /// @dev Error of method caller must be staking vesting contract. + error ErrCallerMustBeStakingVestingContract(); + /** * @dev Returns the staking vesting contract. */ diff --git a/contracts/interfaces/collections/IHasValidatorContract.sol b/contracts/interfaces/collections/IHasValidatorContract.sol index 184d329f9..637ce731f 100644 --- a/contracts/interfaces/collections/IHasValidatorContract.sol +++ b/contracts/interfaces/collections/IHasValidatorContract.sol @@ -2,10 +2,15 @@ pragma solidity ^0.8.9; -interface IHasValidatorContract { +import "./IHasContract.sol"; + +interface IHasValidatorContract is IHasContract { /// @dev Emitted when the validator contract is updated. event ValidatorContractUpdated(address); + /// @dev Error of method caller must be validator contract. + error ErrCallerMustBeValidatorContract(); + /** * @dev Returns the validator contract. */ diff --git a/contracts/interfaces/consumers/VoteStatusConsumer.sol b/contracts/interfaces/consumers/VoteStatusConsumer.sol index 7db10d388..9cbb4d5d8 100644 --- a/contracts/interfaces/consumers/VoteStatusConsumer.sol +++ b/contracts/interfaces/consumers/VoteStatusConsumer.sol @@ -6,6 +6,7 @@ interface VoteStatusConsumer { Pending, Approved, Executed, - Rejected + Rejected, + Expired } } diff --git a/contracts/interfaces/staking/IBaseStaking.sol b/contracts/interfaces/staking/IBaseStaking.sol index 2bd2d6999..7624b1ccc 100644 --- a/contracts/interfaces/staking/IBaseStaking.sol +++ b/contracts/interfaces/staking/IBaseStaking.sol @@ -23,16 +23,48 @@ interface IBaseStaking { /// @dev Emitted when the number of seconds that a candidate must wait to be revoked. event WaitingSecsToRevokeUpdated(uint256 secs); + /// @dev Error of cannot transfer RON. + error ErrCannotTransferRON(); + /// @dev Error of receiving zero message value. + error ErrZeroValue(); + /// @dev Error of pool admin is not allowed to call. + error ErrPoolAdminForbidden(); + /// @dev Error of no one is allowed to call but the pool's admin. + error ErrOnlyPoolAdminAllowed(); + /// @dev Error of admin of any active pool cannot delegate. + error ErrAdminOfAnyActivePoolForbidden(address admin); + /// @dev Error of querying inactive pool. + error ErrInactivePool(address poolAddr); + /// @dev Error of length of input arrays are not of the same. + error ErrInvalidArrays(); + /** * @dev Returns whether the `_poolAdminAddr` is currently active. */ - function isActivePoolAdmin(address _poolAdminAddr) external view returns (bool); + function isAdminOfActivePool(address _poolAdminAddr) external view returns (bool); /** * @dev Returns the consensus address corresponding to the pool admin. */ function getPoolAddressOf(address _poolAdminAddr) external view returns (address); + /** + * @dev Returns the staking pool detail. + */ + function getPoolDetail(address) + external + view + returns ( + address _admin, + uint256 _stakingAmount, + uint256 _stakingTotal + ); + + /** + * @dev Returns the self-staking amounts of the pools. + */ + function getManySelfStakings(address[] calldata) external view returns (uint256[] memory); + /** * @dev Returns The cooldown time in seconds to undelegate from the last timestamp (s)he delegated. */ diff --git a/contracts/interfaces/staking/ICandidateStaking.sol b/contracts/interfaces/staking/ICandidateStaking.sol index d162dbd7d..62cd95404 100644 --- a/contracts/interfaces/staking/ICandidateStaking.sol +++ b/contracts/interfaces/staking/ICandidateStaking.sol @@ -32,6 +32,21 @@ interface ICandidateStaking is IRewardPool { uint256 contractBalance ); + /// @dev Error of cannot transfer RON to specified target. + error ErrCannotInitTransferRON(address addr, string extraInfo); + /// @dev Error of three interaction addresses must be of the same in applying for validator candidate. + error ErrThreeInteractionAddrsNotEqual(); + /// @dev Error of three operation addresses must be distinct in applying for validator candidate. + error ErrThreeOperationAddrsNotDistinct(); + /// @dev Error of unstaking zero amount. + error ErrUnstakeZeroAmount(); + /// @dev Error of invalid staking amount left after deducted. + error ErrStakingAmountLeft(); + /// @dev Error of insufficient staking amount for unstaking. + error ErrInsufficientStakingAmount(); + /// @dev Error of unstaking too early. + error ErrUnstakeTooEarly(); + /** * @dev Returns the minimum threshold for being a validator candidate. */ @@ -138,4 +153,14 @@ interface ICandidateStaking is IRewardPool { * */ function requestRenounce(address _consensusAddr) external; + + /** + * @dev Renounces being a validator candidate and takes back the delegating/staking amount. + * + * Requirements: + * - The consensus address is a validator candidate. + * - The method caller is the pool admin. + * + */ + function requestEmergencyExit(address _consensusAddr) external; } diff --git a/contracts/interfaces/staking/IDelegatorStaking.sol b/contracts/interfaces/staking/IDelegatorStaking.sol index 3252ff0c8..ec094eb0f 100644 --- a/contracts/interfaces/staking/IDelegatorStaking.sol +++ b/contracts/interfaces/staking/IDelegatorStaking.sol @@ -10,6 +10,13 @@ interface IDelegatorStaking is IRewardPool { /// @dev Emitted when the delegator unstaked from a validator candidate. event Undelegated(address indexed delegator, address indexed consensuAddr, uint256 amount); + /// @dev Error of undelegating zero amount. + error ErrUndelegateZeroAmount(); + /// @dev Error of undelegating insufficient amount. + error ErrInsufficientDelegatingAmount(); + /// @dev Error of undelegating too early. + error ErrUndelegateTooEarly(); + /** * @dev Stakes for a validator candidate `_consensusAddr`. * diff --git a/contracts/interfaces/staking/IRewardPool.sol b/contracts/interfaces/staking/IRewardPool.sol index 85beb814c..8c5e5048d 100644 --- a/contracts/interfaces/staking/IRewardPool.sol +++ b/contracts/interfaces/staking/IRewardPool.sol @@ -37,6 +37,9 @@ interface IRewardPool is PeriodWrapperConsumer { /// @dev Emitted when the contract fails when updating the pools that already set event PoolsUpdateConflicted(uint256 indexed period, address[] poolAddrs); + /// @dev Error of invalid pool share. + error ErrInvalidPoolShare(); + /** * @dev Returns the reward amount that user claimable. */ diff --git a/contracts/interfaces/staking/IStaking.sol b/contracts/interfaces/staking/IStaking.sol index 184e94140..f01c086d2 100644 --- a/contracts/interfaces/staking/IStaking.sol +++ b/contracts/interfaces/staking/IStaking.sol @@ -11,7 +11,7 @@ interface IStaking is IRewardPool, IBaseStaking, ICandidateStaking, IDelegatorSt * @dev Records the amount of rewards `_rewards` for the pools `_consensusAddrs`. * * Requirements: - * - The method caller is validator contract. + * - The method caller must be validator contract. * * Emits the event `PoolsUpdated` once the contract recorded the rewards successfully. * Emits the event `PoolsUpdateFailed` once the input array lengths are not equal. @@ -20,7 +20,7 @@ interface IStaking is IRewardPool, IBaseStaking, ICandidateStaking, IDelegatorSt * Note: This method should be called once at the period ending. * */ - function recordRewards( + function execRecordRewards( address[] calldata _consensusAddrs, uint256[] calldata _rewards, uint256 _period @@ -30,29 +30,12 @@ interface IStaking is IRewardPool, IBaseStaking, ICandidateStaking, IDelegatorSt * @dev Deducts from staking amount of the validator `_consensusAddr` for `_amount`. * * Requirements: - * - The method caller is validator contract. + * - The method caller must be validator contract. * * Emits the event `Unstaked`. * */ - function deductStakingAmount(address _consensusAddr, uint256 _amount) + function execDeductStakingAmount(address _consensusAddr, uint256 _amount) external returns (uint256 _actualDeductingAmount); - - /** - * @dev Returns the staking pool detail. - */ - function getStakingPool(address) - external - view - returns ( - address _admin, - uint256 _stakingAmount, - uint256 _stakingTotal - ); - - /** - * @dev Returns the self-staking amounts of the pools. - */ - function getManySelfStakings(address[] calldata) external view returns (uint256[] memory); } diff --git a/contracts/interfaces/validator/ICandidateManager.sol b/contracts/interfaces/validator/ICandidateManager.sol index 1868d41b9..db2292b14 100644 --- a/contracts/interfaces/validator/ICandidateManager.sol +++ b/contracts/interfaces/validator/ICandidateManager.sol @@ -51,6 +51,29 @@ interface ICandidateManager { /// @dev Emitted when the commission rate of a validator is updated. event CommissionRateUpdated(address indexed consensusAddr, uint256 rate); + /// @dev Error of exceeding maximum number of candidates. + error ErrExceedsMaxNumberOfCandidate(); + /// @dev Error of querying for already existent candidate. + error ErrExistentCandidate(); + /// @dev Error of querying for non-existent candidate. + error ErrNonExistentCandidate(); + /// @dev Error of candidate admin already exists. + error ErrExistentCandidateAdmin(address _candidateAdminAddr); + /// @dev Error of treasury already exists. + error ErrExistentTreasury(address _treasuryAddr); + /// @dev Error of bridge operator already exists. + error ErrExistentBridgeOperator(address _bridgeOperatorAddr); + /// @dev Error of invalid commission rate. + error ErrInvalidCommissionRate(); + /// @dev Error of invalid effective days onwards. + error ErrInvalidEffectiveDaysOnwards(); + /// @dev Error of invalid min effective days onwards. + error ErrInvalidMinEffectiveDaysOnwards(); + /// @dev Error of already requested revoking candidate before + error ErrAlreadyRequestedRevokingCandidate(); + /// @dev Error of commission change schedule exists + error ErrAlreadyRequestedUpdatingCommissionRate(); + /** * @dev Returns the maximum number of validator candidate. */ diff --git a/contracts/interfaces/validator/ICoinbaseExecution.sol b/contracts/interfaces/validator/ICoinbaseExecution.sol index b7ceafcf4..6705d3bb1 100644 --- a/contracts/interfaces/validator/ICoinbaseExecution.sol +++ b/contracts/interfaces/validator/ICoinbaseExecution.sol @@ -14,9 +14,9 @@ interface ICoinbaseExecution is ISlashingExecution { /// @dev Emitted when the validator set is updated event ValidatorSetUpdated(uint256 indexed period, address[] consensusAddrs); /// @dev Emitted when the bridge operator set is updated, to mirror the in-jail and maintaining status of the validator. - event BlockProducerSetUpdated(uint256 indexed period, address[] consensusAddrs); + event BlockProducerSetUpdated(uint256 indexed period, uint256 indexed epoch, address[] consensusAddrs); /// @dev Emitted when the bridge operator set is updated. - event BridgeOperatorSetUpdated(uint256 indexed period, address[] bridgeOperators); + event BridgeOperatorSetUpdated(uint256 indexed period, uint256 indexed epoch, address[] bridgeOperators); /// @dev Emitted when the reward of the block producer is deprecated. event BlockRewardDeprecated( @@ -54,13 +54,25 @@ interface ICoinbaseExecution is ISlashingExecution { ); /// @dev Emitted when the amount of RON reward is distributed to staking contract. - event StakingRewardDistributed(uint256 amount); + event StakingRewardDistributed(uint256 totalAmount, address[] consensusAddrs, uint256[] amounts); /// @dev Emitted when the contracts fails when distributing the amount of RON to the staking contract. - event StakingRewardDistributionFailed(uint256 amount, uint256 contractBalance); + event StakingRewardDistributionFailed( + uint256 totalAmount, + address[] consensusAddrs, + uint256[] amounts, + uint256 contractBalance + ); /// @dev Emitted when the epoch is wrapped up. event WrappedUpEpoch(uint256 indexed periodNumber, uint256 indexed epochNumber, bool periodEnding); + /// @dev Error of method caller must be coinbase + error ErrCallerMustBeCoinbase(); + /// @dev Error of only allowed at the end of epoch + error ErrAtEndOfEpochOnly(); + /// @dev Error of query for already wrapped up epoch + error ErrAlreadyWrappedEpoch(); + /** * @dev Submits reward of the current block. * diff --git a/contracts/interfaces/validator/IEmergencyExit.sol b/contracts/interfaces/validator/IEmergencyExit.sol new file mode 100644 index 000000000..d6a2cb5e8 --- /dev/null +++ b/contracts/interfaces/validator/IEmergencyExit.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.9; + +interface IEmergencyExit { + /// @dev Emitted when the fund is locked from an emergency exit request + event EmergencyExitRequested(address indexed consensusAddr, uint256 lockedAmount); + /// @dev Emitted when the fund that locked from an emergency exit request is transferred to the recipient. + event EmergencyExitLockedFundReleased( + address indexed consensusAddr, + address indexed recipient, + uint256 unlockedAmount + ); + /// @dev Emitted when the fund that locked from an emergency exit request is failed to transferred back. + event EmergencyExitLockedFundReleasingFailed( + address indexed consensusAddr, + address indexed recipient, + uint256 unlockedAmount, + uint256 contractBalance + ); + + /// @dev Emitted when the emergency exit locked amount is updated. + event EmergencyExitLockedAmountUpdated(uint256 amount); + /// @dev Emitted when the emergency expiry duration is updated. + event EmergencyExpiryDurationUpdated(uint256 amount); + + /// @dev Error of already requested emergency exit before. + error ErrAlreadyRequestedEmergencyExit(); + + /** + * @dev Returns the amount of RON to lock from a consensus address. + */ + function emergencyExitLockedAmount() external returns (uint256); + + /** + * @dev Returns the duration that an emergency request is expired and the fund will be recycled. + */ + function emergencyExpiryDuration() external returns (uint256); + + /** + * @dev Sets the amount of RON to lock from a consensus address. + * + * Requirements: + * - The method caller is admin. + * + * Emits the event `EmergencyExitLockedAmountUpdated`. + * + */ + function setEmergencyExitLockedAmount(uint256 _emergencyExitLockedAmount) external; + + /** + * @dev Sets the duration that an emergency request is expired and the fund will be recycled. + * + * Requirements: + * - The method caller is admin. + * + * Emits the event `EmergencyExpiryDurationUpdated`. + * + */ + function setEmergencyExpiryDuration(uint256 _emergencyExpiryDuration) external; + + /** + * @dev Unlocks fund for emergency exit request. + * + * Requirements: + * - The method caller is admin. + * + * Emits the event `EmergencyExitLockedFundReleased` if the fund is successfully unlocked. + * Emits the event `EmergencyExitLockedFundReleasingFailed` if the fund is failed to unlock. + * + */ + function execReleaseLockedFundForEmergencyExitRequest(address _consensusAddr, address payable _recipient) external; + + /** + * @dev Fallback function of `IStaking-requestEmergencyExit`. + * + * Requirements: + * - The method caller is staking contract. + * + */ + function execEmergencyExit(address _consensusAddr, uint256 _secLeftToRevoke) external; +} diff --git a/contracts/interfaces/validator/IRoninValidatorSet.sol b/contracts/interfaces/validator/IRoninValidatorSet.sol index c1818a5a4..6628bf136 100644 --- a/contracts/interfaces/validator/IRoninValidatorSet.sol +++ b/contracts/interfaces/validator/IRoninValidatorSet.sol @@ -6,5 +6,12 @@ import "./ICandidateManager.sol"; import "./info-fragments/ICommonInfo.sol"; import "./ICoinbaseExecution.sol"; import "./ISlashingExecution.sol"; +import "./IEmergencyExit.sol"; -interface IRoninValidatorSet is ICandidateManager, ICommonInfo, ISlashingExecution, ICoinbaseExecution {} +interface IRoninValidatorSet is + ICandidateManager, + ICommonInfo, + ISlashingExecution, + ICoinbaseExecution, + IEmergencyExit +{} diff --git a/contracts/interfaces/validator/info-fragments/ICommonInfo.sol b/contracts/interfaces/validator/info-fragments/ICommonInfo.sol index ecd278281..e9e5a888c 100644 --- a/contracts/interfaces/validator/info-fragments/ICommonInfo.sol +++ b/contracts/interfaces/validator/info-fragments/ICommonInfo.sol @@ -7,13 +7,27 @@ import "./ITimingInfo.sol"; import "./IValidatorInfo.sol"; interface ICommonInfo is ITimingInfo, IJailingInfo, IValidatorInfo { + struct EmergencyExitInfo { + uint256 lockedAmount; + // The timestamp that this locked amount will be recycled to staking vesting contract + uint256 recyclingAt; + } + /// @dev Emitted when the deprecated reward is withdrawn. event DeprecatedRewardRecycled(address indexed recipientAddr, uint256 amount); /// @dev Emitted when the deprecated reward withdrawal is failed event DeprecatedRewardRecycleFailed(address indexed recipientAddr, uint256 amount, uint256 balance); + // Error thrown when receives RON from neither staking vesting contract nor staking contract" + error UnauthorizedReceiveRON(); + /** * @dev Returns the total deprecated reward, which includes reward that is not sent for slashed validators and unsastified bridge operators */ function totalDeprecatedReward() external view returns (uint256); + + /** + * @dev Returns the emergency exit request. + */ + function getEmergencyExitInfo(address _consensusAddr) external view returns (EmergencyExitInfo memory); } diff --git a/contracts/interfaces/validator/info-fragments/IValidatorInfo.sol b/contracts/interfaces/validator/info-fragments/IValidatorInfo.sol index 34050b55b..92da6dec2 100644 --- a/contracts/interfaces/validator/info-fragments/IValidatorInfo.sol +++ b/contracts/interfaces/validator/info-fragments/IValidatorInfo.sol @@ -3,18 +3,21 @@ pragma solidity ^0.8.9; interface IValidatorInfo { - /// @dev Emitted when the number of max validator is updated + /// @dev Emitted when the number of max validator is updated. event MaxValidatorNumberUpdated(uint256); - /// @dev Emitted when the number of reserved slots for prioritized validators is updated + /// @dev Emitted when the number of reserved slots for prioritized validators is updated. event MaxPrioritizedValidatorNumberUpdated(uint256); + /// @dev Error of number of prioritized greater than number of max validators. + error InvalidMaxPrioitizedValidatorNumber(); + /** - * @dev Returns the maximum number of validators in the epoch + * @dev Returns the maximum number of validators in the epoch. */ function maxValidatorNumber() external view returns (uint256 _maximumValidatorNumber); /** - * @dev Returns the number of reserved slots for prioritized validators + * @dev Returns the number of reserved slots for prioritized validators. */ function maxPrioritizedValidatorNumber() external view returns (uint256 _maximumPrioritizedValidatorNumber); @@ -53,6 +56,11 @@ interface IValidatorInfo { */ function isBridgeOperator(address _addr) external view returns (bool); + /** + * @dev Returns whether the consensus address is operatoring the bridge or not. + */ + function isOperatingBridge(address _consensusAddr) external view returns (bool); + /** * @dev Returns total numbers of the bridge operators. */ diff --git a/contracts/libraries/BridgeOperatorsBallot.sol b/contracts/libraries/BridgeOperatorsBallot.sol index 2b033f4a3..586ea9fbd 100644 --- a/contracts/libraries/BridgeOperatorsBallot.sol +++ b/contracts/libraries/BridgeOperatorsBallot.sol @@ -2,22 +2,64 @@ pragma solidity ^0.8.0; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import "../interfaces/consumers/WeightedAddressConsumer.sol"; library BridgeOperatorsBallot { - // keccak256("BridgeOperatorsBallot(uint256 period,address[] operators)"); + struct BridgeOperatorSet { + uint256 period; + uint256 epoch; + address[] operators; + } + + // keccak256("BridgeOperatorsBallot(uint256 period,uint256 epoch,address[] operators)"); bytes32 public constant BRIDGE_OPERATORS_BALLOT_TYPEHASH = - 0xeea5e3908ac28cbdbbce8853e49444c558a0a03597e98ef19e6ff86162ed9ae3; + 0xd679a49e9e099fa9ed83a5446aaec83e746b03ec6723d6f5efb29d37d7f0b78a; + + /** + * @dev Verifies whether the ballot is valid or not. + * + * Requirements: + * - The ballot is not for an empty operator set. + * - The epoch and period are not older than the current ones. + * - The bridge operator set is changed compared with the latest one. + * - The operator address list is in order. + * + */ + function verifyBallot(BridgeOperatorSet calldata _ballot, BridgeOperatorSet storage _latest) internal view { + require(_ballot.operators.length > 0, "BridgeOperatorsBallot: invalid array length"); + + bytes32 _ballotOperatorsHash; + bytes32 _latestOperatorsHash; + address[] memory _ballotOperators = _ballot.operators; + address[] memory _latestOperators = _latest.operators; + + assembly { + _ballotOperatorsHash := keccak256(add(_ballotOperators, 32), mul(mload(_ballotOperators), 32)) + _latestOperatorsHash := keccak256(add(_latestOperators, 32), mul(mload(_latestOperators), 32)) + } + + require( + _ballotOperatorsHash != _latestOperatorsHash, + "BridgeOperatorsBallot: bridge operator set is already voted" + ); + + address _addr = _ballotOperators[0]; + for (uint _i = 1; _i < _ballotOperators.length; _i++) { + require(_addr < _ballotOperators[_i], "BridgeOperatorsBallot: invalid order of bridge operators"); + _addr = _ballotOperators[_i]; + } + } /** * @dev Returns hash of the ballot. */ - function hash(uint256 _period, address[] memory _operators) internal pure returns (bytes32) { + function hash(BridgeOperatorSet calldata _ballot) internal pure returns (bytes32) { bytes32 _operatorsHash; + address[] memory _operators = _ballot.operators; + assembly { _operatorsHash := keccak256(add(_operators, 32), mul(mload(_operators), 32)) } - return keccak256(abi.encode(BRIDGE_OPERATORS_BALLOT_TYPEHASH, _period, _operatorsHash)); + return keccak256(abi.encode(BRIDGE_OPERATORS_BALLOT_TYPEHASH, _ballot.period, _ballot.epoch, _operatorsHash)); } } diff --git a/contracts/libraries/EmergencyExitBallot.sol b/contracts/libraries/EmergencyExitBallot.sol new file mode 100644 index 000000000..06b9519c7 --- /dev/null +++ b/contracts/libraries/EmergencyExitBallot.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; + +library EmergencyExitBallot { + // keccak256("EmergencyExitBallot(address consensusAddress,address recipientAfterUnlockedFund,uint256 requestedAt,uint256 expiredAt)"); + bytes32 public constant EMERGENCY_EXIT_BALLOT_TYPEHASH = + 0x697acba4deaf1a718d8c2d93e42860488cb7812696f28ca10eed17bac41e7027; + + /** + * @dev Returns hash of the ballot. + */ + function hash( + address _consensusAddress, + address _recipientAfterUnlockedFund, + uint256 _requestedAt, + uint256 _expiredAt + ) internal pure returns (bytes32) { + return + keccak256( + abi.encode( + EMERGENCY_EXIT_BALLOT_TYPEHASH, + _consensusAddress, + _recipientAfterUnlockedFund, + _requestedAt, + _expiredAt + ) + ); + } +} diff --git a/contracts/mainchain/MainchainGovernanceAdmin.sol b/contracts/mainchain/MainchainGovernanceAdmin.sol index 199642074..ad58243e3 100644 --- a/contracts/mainchain/MainchainGovernanceAdmin.sol +++ b/contracts/mainchain/MainchainGovernanceAdmin.sol @@ -34,8 +34,8 @@ contract MainchainGovernanceAdmin is AccessControlEnumerable, GovernanceRelay, G /** * @dev Returns whether the voter `_voter` casted vote for bridge operators at a specific period. */ - function bridgeOperatorsRelayed(uint256 _period) external view returns (bool) { - return _vote[_period].status != VoteStatus.Pending; + function bridgeOperatorsRelayed(uint256 _period, uint256 _epoch) external view returns (bool) { + return _vote[_period][_epoch].status != VoteStatus.Pending; } /** @@ -84,13 +84,12 @@ contract MainchainGovernanceAdmin is AccessControlEnumerable, GovernanceRelay, G * */ function relayBridgeOperators( - uint256 _period, - address[] calldata _operators, + BridgeOperatorsBallot.BridgeOperatorSet calldata _ballot, Signature[] calldata _signatures ) external onlyRole(RELAYER_ROLE) { - _relayVotesBySignatures(_operators, _signatures, _period, _getMinimumVoteWeight(), DOMAIN_SEPARATOR); + _relayVotesBySignatures(_ballot, _signatures, _getMinimumVoteWeight(), DOMAIN_SEPARATOR); TransparentUpgradeableProxyV2(payable(bridgeContract())).functionDelegateCall( - abi.encodeWithSelector(_bridgeContract.replaceBridgeOperators.selector, _operators) + abi.encodeWithSelector(_bridgeContract.replaceBridgeOperators.selector, _ballot.operators) ); } diff --git a/contracts/mocks/MockStaking.sol b/contracts/mocks/MockStaking.sol index d310b8709..1e73a084f 100644 --- a/contracts/mocks/MockStaking.sol +++ b/contracts/mocks/MockStaking.sol @@ -30,7 +30,7 @@ contract MockStaking is RewardCalculation { uint256[] memory _rewards = new uint256[](1); _addrs[0] = poolAddr; _rewards[0] = pendingReward; - this.recordRewards(_addrs, _rewards); + this.execRecordRewards(_addrs, _rewards); pendingReward = 0; lastUpdatedPeriod++; @@ -64,7 +64,7 @@ contract MockStaking is RewardCalculation { pendingReward -= _amount; } - function recordRewards(address[] calldata _addrList, uint256[] calldata _rewards) external { + function execRecordRewards(address[] calldata _addrList, uint256[] calldata _rewards) external { _recordRewards(_addrList, _rewards, _currentPeriod()); } diff --git a/contracts/mocks/forwarder/MockForwarderTarget.sol b/contracts/mocks/forwarder/MockForwarderTarget.sol new file mode 100644 index 000000000..a48b2bf62 --- /dev/null +++ b/contracts/mocks/forwarder/MockForwarderTarget.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.9; + +import "../../extensions/RONTransferHelper.sol"; + +contract MockForwarderTarget is RONTransferHelper { + address public owner; + uint256 public data; + + event TargetWithdrawn(address indexed _origin, address indexed _caller, address indexed _recipient); + + error ErrIntentionally(); + + modifier onlyOwner() { + require(msg.sender == owner, "MockForwarderContract: only owner can call method"); + _; + } + + fallback() external payable { + _fallback(); + } + + receive() external payable { + _fallback(); + } + + constructor(address _owner, uint256 _data) payable { + owner = _owner; + data = _data; + } + + function foo(uint256 _data) external onlyOwner { + data = _data; + } + + function fooPayable(uint256 _data) external payable onlyOwner { + data = _data; + } + + function fooSilentRevert() external view onlyOwner { + revert(); + } + + function fooCustomErrorRevert() external view onlyOwner { + revert ErrIntentionally(); + } + + function fooRevert() external view onlyOwner { + revert("MockForwarderContract: revert intentionally"); + } + + function getBalance() external view returns (uint256) { + return address(this).balance; + } + + function withdrawAll() external onlyOwner { + emit TargetWithdrawn(tx.origin, msg.sender, msg.sender); + _transferRON(payable(msg.sender), address(this).balance); + } + + function _fallback() private pure { + revert("MockForwardTarget: hello from fallback"); + } +} diff --git a/contracts/mocks/precompile-usages/MockPrecompileUsagePickValidatorSet.sol b/contracts/mocks/precompile-usages/MockPCUPickValidatorSet.sol similarity index 85% rename from contracts/mocks/precompile-usages/MockPrecompileUsagePickValidatorSet.sol rename to contracts/mocks/precompile-usages/MockPCUPickValidatorSet.sol index bd2e47108..8e58d17d1 100644 --- a/contracts/mocks/precompile-usages/MockPrecompileUsagePickValidatorSet.sol +++ b/contracts/mocks/precompile-usages/MockPCUPickValidatorSet.sol @@ -2,9 +2,9 @@ pragma solidity ^0.8.9; -import "../../precompile-usages/PrecompileUsagePickValidatorSet.sol"; +import "../../precompile-usages/PCUPickValidatorSet.sol"; -contract MockPrecompileUsagePickValidatorSet is PrecompileUsagePickValidatorSet { +contract MockPCUPickValidatorSet is PCUPickValidatorSet { address internal _precompileSortValidatorAddress; constructor(address _precompile) { diff --git a/contracts/mocks/precompile-usages/MockPrecompileUsageSortValidators.sol b/contracts/mocks/precompile-usages/MockPCUSortValidators.sol similarity index 82% rename from contracts/mocks/precompile-usages/MockPrecompileUsageSortValidators.sol rename to contracts/mocks/precompile-usages/MockPCUSortValidators.sol index 83dad44f6..e4dfb255d 100644 --- a/contracts/mocks/precompile-usages/MockPrecompileUsageSortValidators.sol +++ b/contracts/mocks/precompile-usages/MockPCUSortValidators.sol @@ -2,9 +2,9 @@ pragma solidity ^0.8.9; -import "../../precompile-usages/PrecompileUsageSortValidators.sol"; +import "../../precompile-usages/PCUSortValidators.sol"; -contract MockPrecompileUsageSortValidators is PrecompileUsageSortValidators { +contract MockPCUSortValidators is PCUSortValidators { address internal _precompileSortValidatorAddress; constructor(address _precompile) { diff --git a/contracts/mocks/precompile-usages/MockPrecompileUsageValidateDoubleSign.sol b/contracts/mocks/precompile-usages/MockPCUValidateDoubleSign.sol similarity index 80% rename from contracts/mocks/precompile-usages/MockPrecompileUsageValidateDoubleSign.sol rename to contracts/mocks/precompile-usages/MockPCUValidateDoubleSign.sol index 8475820a5..fb2df7291 100644 --- a/contracts/mocks/precompile-usages/MockPrecompileUsageValidateDoubleSign.sol +++ b/contracts/mocks/precompile-usages/MockPCUValidateDoubleSign.sol @@ -2,9 +2,9 @@ pragma solidity ^0.8.9; -import "../../precompile-usages/PrecompileUsageValidateDoubleSign.sol"; +import "../../precompile-usages/PCUValidateDoubleSign.sol"; -contract MockPrecompileUsageValidateDoubleSign is PrecompileUsageValidateDoubleSign { +contract MockPCUValidateDoubleSign is PCUValidateDoubleSign { address internal _precompileValidateDoubleSignAddress; constructor(address _precompile) { diff --git a/contracts/mocks/validator/MockRoninValidatorSetExtended.sol b/contracts/mocks/validator/MockRoninValidatorSetExtended.sol index 78f284c8e..5cbc7ae8f 100644 --- a/contracts/mocks/validator/MockRoninValidatorSetExtended.sol +++ b/contracts/mocks/validator/MockRoninValidatorSetExtended.sol @@ -6,17 +6,25 @@ import "./MockRoninValidatorSetOverridePrecompile.sol"; import "../../libraries/EnumFlags.sol"; contract MockRoninValidatorSetExtended is MockRoninValidatorSetOverridePrecompile { + bool private _initialized; uint256[] internal _epochs; constructor() {} + function initEpoch() public { + if (!_initialized) { + _epochs.push(0); + _initialized = true; + } + } + function endEpoch() external { _epochs.push(block.number); } function epochOf(uint256 _block) public view override returns (uint256 _epoch) { for (uint256 _i = _epochs.length; _i > 0; _i--) { - if (_block >= _epochs[_i - 1]) { + if (_block > _epochs[_i - 1]) { return _i; } } @@ -34,7 +42,7 @@ contract MockRoninValidatorSetExtended is MockRoninValidatorSetOverridePrecompil function getJailUntils(address[] calldata _addrs) public view returns (uint256[] memory jailUntils_) { jailUntils_ = new uint256[](_addrs.length); for (uint _i = 0; _i < _addrs.length; _i++) { - jailUntils_[_i] = _jailedUntil[_addrs[_i]]; + jailUntils_[_i] = _blockProducerJailedBlock[_addrs[_i]]; } } diff --git a/contracts/mocks/validator/MockValidatorSet.sol b/contracts/mocks/validator/MockValidatorSet.sol index 678a446ea..913de8eed 100644 --- a/contracts/mocks/validator/MockValidatorSet.sol +++ b/contracts/mocks/validator/MockValidatorSet.sol @@ -140,4 +140,29 @@ contract MockValidatorSet is IRoninValidatorSet, CandidateManager { {} function totalDeprecatedReward() external view override returns (uint256) {} + + function _bridgeOperatorOf(address _consensusAddr) internal view override returns (address) { + return super._bridgeOperatorOf(_consensusAddr); + } + + function execReleaseLockedFundForEmergencyExitRequest(address _consensusAddr, address payable _recipient) + external + override + {} + + function emergencyExitLockedAmount() external override returns (uint256) {} + + function emergencyExpiryDuration() external override returns (uint256) {} + + function setEmergencyExitLockedAmount(uint256 _emergencyExitLockedAmount) external override {} + + function setEmergencyExpiryDuration(uint256 _emergencyExpiryDuration) external override {} + + function getEmergencyExitInfo(address _consensusAddr) external view override returns (EmergencyExitInfo memory) {} + + function execEmergencyExit(address, uint256) external {} + + function isOperatingBridge(address) external view returns (bool) {} + + function _emergencyExitLockedFundReleased(address _consensusAddr) internal virtual override returns (bool) {} } diff --git a/contracts/precompile-usages/PrecompileUsagePickValidatorSet.sol b/contracts/precompile-usages/PCUPickValidatorSet.sol similarity index 91% rename from contracts/precompile-usages/PrecompileUsagePickValidatorSet.sol rename to contracts/precompile-usages/PCUPickValidatorSet.sol index bee39ff08..a63659bf5 100644 --- a/contracts/precompile-usages/PrecompileUsagePickValidatorSet.sol +++ b/contracts/precompile-usages/PCUPickValidatorSet.sol @@ -2,7 +2,9 @@ pragma solidity ^0.8.9; -abstract contract PrecompileUsagePickValidatorSet { +import "./PrecompiledUsage.sol"; + +abstract contract PCUPickValidatorSet is PrecompiledUsage { /// @dev Gets the address of the precompile of picking validator set function precompilePickValidatorSetAddress() public view virtual returns (address) { return address(0x68); @@ -49,7 +51,7 @@ abstract contract PrecompileUsagePickValidatorSet { _result := add(_result, 0x20) } - require(_success, "PrecompileUsagePickValidatorSet: call to precompile fails"); + if (!_success) revert ErrCallPrecompiled(); _newValidatorCount = _result.length; } diff --git a/contracts/precompile-usages/PrecompileUsageSortValidators.sol b/contracts/precompile-usages/PCUSortValidators.sol similarity index 89% rename from contracts/precompile-usages/PrecompileUsageSortValidators.sol rename to contracts/precompile-usages/PCUSortValidators.sol index b9ab6168f..5a6e6d2be 100644 --- a/contracts/precompile-usages/PrecompileUsageSortValidators.sol +++ b/contracts/precompile-usages/PCUSortValidators.sol @@ -2,7 +2,9 @@ pragma solidity ^0.8.9; -abstract contract PrecompileUsageSortValidators { +import "./PrecompiledUsage.sol"; + +abstract contract PCUSortValidators is PrecompiledUsage { /// @dev Gets the address of the precompile of sorting validators function precompileSortValidatorsAddress() public view virtual returns (address) { return address(0x66); @@ -40,6 +42,6 @@ abstract contract PrecompileUsageSortValidators { _result := add(_result, 0x20) } - require(_success, "PrecompileUsageSortValidators: call to precompile fails"); + if (!_success) revert ErrCallPrecompiled(); } } diff --git a/contracts/precompile-usages/PrecompileUsageValidateDoubleSign.sol b/contracts/precompile-usages/PCUValidateDoubleSign.sol similarity index 89% rename from contracts/precompile-usages/PrecompileUsageValidateDoubleSign.sol rename to contracts/precompile-usages/PCUValidateDoubleSign.sol index 216abb6de..e49653f3a 100644 --- a/contracts/precompile-usages/PrecompileUsageValidateDoubleSign.sol +++ b/contracts/precompile-usages/PCUValidateDoubleSign.sol @@ -2,7 +2,9 @@ pragma solidity ^0.8.9; -abstract contract PrecompileUsageValidateDoubleSign { +import "./PrecompiledUsage.sol"; + +abstract contract PCUValidateDoubleSign is PrecompiledUsage { /// @dev Gets the address of the precompile of validating double sign evidence function precompileValidateDoubleSignAddress() public view virtual returns (address) { return address(0x67); @@ -38,7 +40,7 @@ abstract contract PrecompileUsageValidateDoubleSign { } } - require(_success, "PrecompileUsageValidateDoubleSign: call to precompile fails"); + if (!_success) revert ErrCallPrecompiled(); return (_output[0] != 0); } } diff --git a/contracts/precompile-usages/PrecompiledUsage.sol b/contracts/precompile-usages/PrecompiledUsage.sol new file mode 100644 index 000000000..0a7e9939f --- /dev/null +++ b/contracts/precompile-usages/PrecompiledUsage.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.9; + +import "./PrecompiledUsage.sol"; + +abstract contract PrecompiledUsage { + /// @dev Error of call to precompile fails. + error ErrCallPrecompiled(); +} diff --git a/contracts/ronin/RoninGovernanceAdmin.sol b/contracts/ronin/RoninGovernanceAdmin.sol index ba1dc086e..f796cdd34 100644 --- a/contracts/ronin/RoninGovernanceAdmin.sol +++ b/contracts/ronin/RoninGovernanceAdmin.sol @@ -3,32 +3,59 @@ pragma solidity ^0.8.0; import "../extensions/isolated-governance/bridge-operator-governance/BOsGovernanceProposal.sol"; import "../extensions/sequential-governance/GovernanceProposal.sol"; +import "../extensions/collections/HasValidatorContract.sol"; import "../extensions/GovernanceAdmin.sol"; -import "../interfaces/IBridge.sol"; +import "../libraries/EmergencyExitBallot.sol"; +import "../interfaces/IRoninGovernanceAdmin.sol"; -contract RoninGovernanceAdmin is GovernanceAdmin, GovernanceProposal, BOsGovernanceProposal { - /// @dev Emitted when the bridge operators are approved. - event BridgeOperatorsApproved(uint256 _period, address[] _operators); +contract RoninGovernanceAdmin is + IRoninGovernanceAdmin, + GovernanceAdmin, + GovernanceProposal, + BOsGovernanceProposal, + HasValidatorContract +{ + using Proposal for Proposal.ProposalDetail; + + /// @dev Mapping from request hash => emergency poll + mapping(bytes32 => IsolatedVote) internal _emergencyExitPoll; modifier onlyGovernor() { - require(_getWeight(msg.sender) > 0, "GovernanceAdmin: sender is not governor"); + require(_getWeight(msg.sender) > 0, "RoninGovernanceAdmin: sender is not governor"); _; } constructor( address _roninTrustedOrganizationContract, address _bridgeContract, + address _validatorContract, uint256 _proposalExpiryDuration - ) GovernanceAdmin(_roninTrustedOrganizationContract, _bridgeContract, _proposalExpiryDuration) {} + ) GovernanceAdmin(_roninTrustedOrganizationContract, _bridgeContract, _proposalExpiryDuration) { + _setValidatorContract(_validatorContract); + } + + /** + * @inheritdoc IHasValidatorContract + */ + function setValidatorContract(address _addr) external override onlySelfCall { + require(_addr.code.length > 0, "RoninGovernanceAdmin: set to non-contract"); + _setValidatorContract(_addr); + } /** * @dev Returns the voted signatures for the proposals. * + * Note: The signatures can be empty in case the proposal is voted on the current network. + * */ function getProposalSignatures(uint256 _chainId, uint256 _round) external view - returns (Ballot.VoteType[] memory _supports, Signature[] memory _signatures) + returns ( + address[] memory _voters, + Ballot.VoteType[] memory _supports, + Signature[] memory _signatures + ) { ProposalVote storage _vote = vote[_chainId][_round]; @@ -38,13 +65,16 @@ contract RoninGovernanceAdmin is GovernanceAdmin, GovernanceProposal, BOsGoverna _supports = new Ballot.VoteType[](_voterLength); _signatures = new Signature[](_voterLength); + _voters = new address[](_voterLength); for (uint256 _i; _i < _forLength; _i++) { _supports[_i] = Ballot.VoteType.For; _signatures[_i] = vote[_chainId][_round].sig[_vote.forVoteds[_i]]; + _voters[_i] = _vote.forVoteds[_i]; } for (uint256 _i; _i < _againstLength; _i++) { _supports[_i + _forLength] = Ballot.VoteType.Against; _signatures[_i + _forLength] = vote[_chainId][_round].sig[_vote.againstVoteds[_i]]; + _voters[_i + _forLength] = _vote.againstVoteds[_i]; } } @@ -55,14 +85,14 @@ contract RoninGovernanceAdmin is GovernanceAdmin, GovernanceProposal, BOsGoverna * Please consider filtering for empty signatures after calling this function. * */ - function getBridgeOperatorVotingSignatures(uint256 _period, address[] calldata _voters) - external - view - returns (Signature[] memory _signatures) - { + function getBridgeOperatorVotingSignatures( + uint256 _period, + uint256 _epoch, + address[] calldata _voters + ) external view returns (Signature[] memory _signatures) { _signatures = new Signature[](_voters.length); for (uint256 _i; _i < _voters.length; _i++) { - _signatures[_i] = _votingSig[_period][_voters[_i]]; + _signatures[_i] = _votingSig[_period][_epoch][_voters[_i]]; } } @@ -80,8 +110,19 @@ contract RoninGovernanceAdmin is GovernanceAdmin, GovernanceProposal, BOsGoverna /** * @dev Returns whether the voter `_voter` casted vote for bridge operators at a specific period. */ - function bridgeOperatorsVoted(uint256 _period, address _voter) external view returns (bool) { - return _voted(_vote[_period], _voter); + function bridgeOperatorsVoted( + uint256 _period, + uint256 _epoch, + address _voter + ) external view returns (bool) { + return _voted(_vote[_period][_epoch], _voter); + } + + /** + * @dev Returns whether the voter casted vote for emergency exit poll. + */ + function emergencyPollVoted(bytes32 _voteHash, address _voter) external view returns (bool) { + return _voted(_emergencyExitPoll[_voteHash], _voter); } /** @@ -107,6 +148,7 @@ contract RoninGovernanceAdmin is GovernanceAdmin, GovernanceProposal, BOsGoverna * * Requirements: * - The method caller is governor. + * - The proposal is for the current network. * */ function proposeProposalStructAndCastVotes( @@ -117,6 +159,49 @@ contract RoninGovernanceAdmin is GovernanceAdmin, GovernanceProposal, BOsGoverna _proposeProposalStructAndCastVotes(_proposal, _supports, _signatures, DOMAIN_SEPARATOR, msg.sender); } + /** + * @dev Proposes and casts vote for a proposal on the current network. + * + * Requirements: + * - The method caller is governor. + * - The proposal is for the current network. + * + */ + function proposeProposalForCurrentNetwork( + uint256 _expiryTimestamp, + address[] calldata _targets, + uint256[] calldata _values, + bytes[] calldata _calldatas, + uint256[] calldata _gasAmounts, + Ballot.VoteType _support + ) external onlyGovernor { + address _voter = msg.sender; + Proposal.ProposalDetail memory _proposal = _proposeProposal( + block.chainid, + _expiryTimestamp, + _targets, + _values, + _calldatas, + _gasAmounts, + _voter + ); + _castProposalVoteForCurrentNetwork(_voter, _proposal, _support); + } + + /** + * @dev Casts vote for a proposal on the current network. + * + * Requirements: + * - The method caller is governor. + * + */ + function castProposalVoteForCurrentNetwork(Proposal.ProposalDetail calldata _proposal, Ballot.VoteType _support) + external + onlyGovernor + { + _castProposalVoteForCurrentNetwork(msg.sender, _proposal, _support); + } + /** * @dev See `GovernanceProposal-_castProposalBySignatures`. */ @@ -206,19 +291,71 @@ contract RoninGovernanceAdmin is GovernanceAdmin, GovernanceProposal, BOsGoverna * @dev See `BOsGovernanceProposal-_castVotesBySignatures`. */ function voteBridgeOperatorsBySignatures( - uint256 _period, - address[] calldata _operators, + BridgeOperatorsBallot.BridgeOperatorSet calldata _ballot, Signature[] calldata _signatures ) external { - _castVotesBySignatures(_operators, _signatures, _period, _getMinimumVoteWeight(), DOMAIN_SEPARATOR); - IsolatedVote storage _v = _vote[_period]; + _castVotesBySignatures(_ballot, _signatures, _getMinimumVoteWeight(), DOMAIN_SEPARATOR); + IsolatedVote storage _v = _vote[_ballot.period][_ballot.epoch]; if (_v.status == VoteStatus.Approved) { - _lastSyncedPeriod = _period; - emit BridgeOperatorsApproved(_period, _operators); + _lastSyncedBridgeOperatorSetInfo = _ballot; + emit BridgeOperatorsApproved(_ballot.period, _ballot.epoch, _ballot.operators); _v.status = VoteStatus.Executed; } } + /** + * @inheritdoc IRoninGovernanceAdmin + */ + function createEmergencyExitPoll( + address _consensusAddr, + address _recipientAfterUnlockedFund, + uint256 _requestedAt, + uint256 _expiredAt + ) external onlyValidatorContract { + bytes32 _hash = EmergencyExitBallot.hash(_consensusAddr, _recipientAfterUnlockedFund, _requestedAt, _expiredAt); + IsolatedVote storage _v = _emergencyExitPoll[_hash]; + _v.createdAt = block.timestamp; + _v.expiredAt = _expiredAt; + emit EmergencyExitPollCreated(_hash, _consensusAddr, _recipientAfterUnlockedFund, _requestedAt, _expiredAt); + } + + /** + * @dev Votes for an emergency exit. Executes to unlock fund for the emergency exit's requester. + * + * Requirements: + * - The voter is governor. + * - The voting is existent. + * - The voting is not expired yet. + * + */ + function voteEmergencyExit( + bytes32 _voteHash, + address _consensusAddr, + address _recipientAfterUnlockedFund, + uint256 _requestedAt, + uint256 _expiredAt + ) external { + address _voter = msg.sender; + uint256 _weight = _getWeight(_voter); + require(_weight > 0, "RoninGovernanceAdmin: sender is not governor"); + + bytes32 _hash = EmergencyExitBallot.hash(_consensusAddr, _recipientAfterUnlockedFund, _requestedAt, _expiredAt); + require(_voteHash == _hash, "RoninGovernanceAdmin: invalid vote hash"); + + IsolatedVote storage _v = _emergencyExitPoll[_hash]; + require(_v.createdAt > 0, "RoninGovernanceAdmin: query for non-existent vote"); + require(_v.status != VoteStatus.Expired, "RoninGovernanceAdmin: query for expired vote"); + + VoteStatus _stt = _castVote(_v, _voter, _weight, _getMinimumVoteWeight(), _hash); + if (_stt == VoteStatus.Approved) { + _execReleaseLockedFundForEmergencyExitRequest(_consensusAddr, _recipientAfterUnlockedFund); + emit EmergencyExitPollApproved(_hash); + _v.status = VoteStatus.Executed; + } else if (_stt == VoteStatus.Expired) { + emit EmergencyExitPollExpired(_hash); + } + } + /** * @inheritdoc GovernanceProposal */ @@ -250,9 +387,61 @@ contract RoninGovernanceAdmin is GovernanceAdmin, GovernanceProposal, BOsGoverna } /** - * @dev See {CoreGovernance-_getChainType} + * @dev Trigger function from validator contract to unlock fund for emeregency exit request. + */ + function _execReleaseLockedFundForEmergencyExitRequest(address _consensusAddr, address _recipientAfterUnlockedFund) + internal + virtual + { + (bool _success, ) = validatorContract().call( + abi.encodeWithSelector( + // TransparentUpgradeableProxyV2.functionDelegateCall.selector, + 0x4bb5274a, + abi.encodeWithSelector( + _validatorContract.execReleaseLockedFundForEmergencyExitRequest.selector, + _consensusAddr, + _recipientAfterUnlockedFund + ) + ) + ); + require( + _success, + "GovernanceAdmin: proxy call `execReleaseLockedFundForEmergencyExitRequest(address,address)` failed" + ); + } + + /** + * @dev See `CoreGovernance-_getChainType`. */ function _getChainType() internal pure override returns (ChainType) { return ChainType.RoninChain; } + + /** + * @dev See `castProposalVoteForCurrentNetwork`. + */ + function _castProposalVoteForCurrentNetwork( + address _voter, + Proposal.ProposalDetail memory _proposal, + Ballot.VoteType _support + ) internal { + require(_proposal.chainId == block.chainid, "RoninGovernanceAdmin: invalid chain id"); + require( + vote[_proposal.chainId][_proposal.nonce].hash == _proposal.hash(), + "RoninGovernanceAdmin: cast vote for invalid proposal" + ); + + uint256 _minimumForVoteWeight = _getMinimumVoteWeight(); + uint256 _minimumAgainstVoteWeight = _getTotalWeights() - _minimumForVoteWeight + 1; + Signature memory _emptySignature; + _castVote( + _proposal, + _support, + _minimumForVoteWeight, + _minimumAgainstVoteWeight, + _voter, + _emptySignature, + _getWeight(_voter) + ); + } } diff --git a/contracts/ronin/VaultForwarder.sol b/contracts/ronin/VaultForwarder.sol new file mode 100644 index 000000000..aea760075 --- /dev/null +++ b/contracts/ronin/VaultForwarder.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.9; + +import "../extensions/forwarder/Forwarder.sol"; +import "../extensions/RONTransferHelper.sol"; + +/** + * @title A vault contract that keeps RON, and behaves as an EOA account to interact with a target contract. + * @dev There are three roles of interaction: + * - Admin: top-up and withdraw RON to the vault, cannot forward call to the target. + * - Moderator: forward all calls to the target, can top-up RON, cannot withdraw RON. + * - Others: can top-up RON, cannot execute any other actions. + */ +contract VaultForwarder is Forwarder, RONTransferHelper { + /// @dev Emitted when the admin withdraws all RON from the forwarder contract. + event ForwarderRONWithdrawn(address indexed _recipient, uint256 _value); + + constructor(address _target, address _admin) Forwarder(_target, _admin) {} + + /** + * @dev Withdraws all balance from the forward to the admin. + * + * Requirements: + * - Only forwarder admin can call this method. + */ + function withdrawAll() external adminExecutesOrModeratorForwards { + uint256 _value = address(this).balance; + emit ForwarderRONWithdrawn(msg.sender, _value); + _transferRON(payable(msg.sender), _value); + } +} diff --git a/contracts/ronin/slash-indicator/SlashDoubleSign.sol b/contracts/ronin/slash-indicator/SlashDoubleSign.sol index f048891e2..afa70c049 100644 --- a/contracts/ronin/slash-indicator/SlashDoubleSign.sol +++ b/contracts/ronin/slash-indicator/SlashDoubleSign.sol @@ -3,10 +3,10 @@ pragma solidity ^0.8.9; import "../../interfaces/slash-indicator/ISlashDoubleSign.sol"; -import "../../precompile-usages/PrecompileUsageValidateDoubleSign.sol"; +import "../../precompile-usages/PCUValidateDoubleSign.sol"; import "../../extensions/collections/HasValidatorContract.sol"; -abstract contract SlashDoubleSign is ISlashDoubleSign, HasValidatorContract, PrecompileUsageValidateDoubleSign { +abstract contract SlashDoubleSign is ISlashDoubleSign, HasValidatorContract, PCUValidateDoubleSign { /// @dev The amount of RON to slash double sign. uint256 internal _slashDoubleSignAmount; /// @dev The block number that the punished validator will be jailed until, due to double signing. diff --git a/contracts/ronin/staking/BaseStaking.sol b/contracts/ronin/staking/BaseStaking.sol index cc5c273a7..4b14522cb 100644 --- a/contracts/ronin/staking/BaseStaking.sol +++ b/contracts/ronin/staking/BaseStaking.sol @@ -24,8 +24,8 @@ abstract contract BaseStaking is /// @dev The number of seconds that a candidate must wait to be revoked and take the self-staking amount back. uint256 internal _waitingSecsToRevoke; - /// @dev Mapping from active pool admin address => consensus address. - mapping(address => address) internal _activePoolAdminMapping; + /// @dev Mapping from admin address of an active pool => consensus address. + mapping(address => address) internal _adminOfActivePoolMapping; /** * @dev This empty reserved space is put in place to allow future versions to add new * variables without shifting down storage in the inheritance chain. @@ -33,37 +33,63 @@ abstract contract BaseStaking is uint256[49] private ______gap; modifier noEmptyValue() { - require(msg.value > 0, "BaseStaking: query with empty value"); + if (msg.value == 0) revert ErrZeroValue(); _; } modifier notPoolAdmin(PoolDetail storage _pool, address _delegator) { - require(_pool.admin != _delegator, "BaseStaking: delegator must not be the pool admin"); + if (_pool.admin == _delegator) revert ErrPoolAdminForbidden(); _; } modifier onlyPoolAdmin(PoolDetail storage _pool, address _requester) { - require(_pool.admin == _requester, "BaseStaking: requester must be the pool admin"); + if (_pool.admin != _requester) revert ErrOnlyPoolAdminAllowed(); _; } - modifier poolExists(address _poolAddr) { - require(_validatorContract.isValidatorCandidate(_poolAddr), "BaseStaking: query for non-existent pool"); + modifier poolIsActive(address _poolAddr) { + if (!_validatorContract.isValidatorCandidate(_poolAddr)) revert ErrInactivePool(_poolAddr); _; } /** * @inheritdoc IBaseStaking */ - function isActivePoolAdmin(address _poolAdminAddr) public view override returns (bool) { - return _activePoolAdminMapping[_poolAdminAddr] != address(0); + function isAdminOfActivePool(address _poolAdminAddr) public view override returns (bool) { + return _adminOfActivePoolMapping[_poolAdminAddr] != address(0); } /** * @inheritdoc IBaseStaking */ function getPoolAddressOf(address _poolAdminAddr) external view override returns (address) { - return _activePoolAdminMapping[_poolAdminAddr]; + return _adminOfActivePoolMapping[_poolAdminAddr]; + } + + /** + * @inheritdoc IBaseStaking + */ + function getPoolDetail(address _poolAddr) + external + view + returns ( + address _admin, + uint256 _stakingAmount, + uint256 _stakingTotal + ) + { + PoolDetail storage _pool = _stakingPool[_poolAddr]; + return (_pool.admin, _pool.stakingAmount, _pool.stakingTotal); + } + + /** + * @inheritdoc IBaseStaking + */ + function getManySelfStakings(address[] calldata _pools) external view returns (uint256[] memory _selfStakings) { + _selfStakings = new uint256[](_pools.length); + for (uint _i = 0; _i < _pools.length; _i++) { + _selfStakings[_i] = _stakingPool[_pools[_i]].stakingAmount; + } } /** @@ -104,7 +130,7 @@ abstract contract BaseStaking is override returns (uint256[] memory _stakingAmounts) { - require(_poolAddrs.length == _userList.length, "BaseStaking: invalid input array"); + if (_poolAddrs.length != _userList.length) revert ErrInvalidArrays(); _stakingAmounts = new uint256[](_poolAddrs.length); for (uint _i = 0; _i < _stakingAmounts.length; _i++) { _stakingAmounts[_i] = _stakingPool[_poolAddrs[_i]].delegatingAmount[_userList[_i]]; diff --git a/contracts/ronin/staking/CandidateStaking.sol b/contracts/ronin/staking/CandidateStaking.sol index 15e077812..47bf31540 100644 --- a/contracts/ronin/staking/CandidateStaking.sol +++ b/contracts/ronin/staking/CandidateStaking.sol @@ -40,7 +40,7 @@ abstract contract CandidateStaking is BaseStaking, ICandidateStaking { address _bridgeOperatorAddr, uint256 _commissionRate ) external payable override nonReentrant { - require(!isActivePoolAdmin(msg.sender), "CandidateStaking: pool admin is active"); + if (isAdminOfActivePool(msg.sender)) revert ErrAdminOfAnyActivePoolForbidden(msg.sender); uint256 _amount = msg.value; address payable _poolAdmin = payable(msg.sender); @@ -57,7 +57,7 @@ abstract contract CandidateStaking is BaseStaking, ICandidateStaking { PoolDetail storage _pool = _stakingPool[_consensusAddr]; _pool.admin = _poolAdmin; _pool.addr = _consensusAddr; - _activePoolAdminMapping[_poolAdmin] = _consensusAddr; + _adminOfActivePoolMapping[_poolAdmin] = _consensusAddr; _stake(_stakingPool[_consensusAddr], _poolAdmin, _amount); emit PoolApproved(_consensusAddr, _poolAdmin); @@ -70,7 +70,7 @@ abstract contract CandidateStaking is BaseStaking, ICandidateStaking { address _consensusAddr, uint256 _effectiveDaysOnwards, uint256 _commissionRate - ) external override poolExists(_consensusAddr) onlyPoolAdmin(_stakingPool[_consensusAddr], msg.sender) { + ) external override poolIsActive(_consensusAddr) onlyPoolAdmin(_stakingPool[_consensusAddr], msg.sender) { _validatorContract.execRequestUpdateCommissionRate(_consensusAddr, _effectiveDaysOnwards, _commissionRate); } @@ -86,13 +86,13 @@ abstract contract CandidateStaking is BaseStaking, ICandidateStaking { for (uint _i = 0; _i < _pools.length; _i++) { PoolDetail storage _pool = _stakingPool[_pools[_i]]; // Deactivate the pool admin in the active mapping. - delete _activePoolAdminMapping[_pool.admin]; + delete _adminOfActivePoolMapping[_pool.admin]; // Deduct and transfer the self staking amount to the pool admin. _amount = _pool.stakingAmount; if (_amount > 0) { _deductStakingAmount(_pool, _amount); - if (!_unsafeSendRON(payable(_pool.admin), _amount)) { + if (!_unsafeSendRON(payable(_pool.admin), _amount, 3500)) { emit StakingAmountTransferFailed(_pool.addr, _pool.admin, _amount, address(this).balance); } } @@ -104,22 +104,27 @@ abstract contract CandidateStaking is BaseStaking, ICandidateStaking { /** * @inheritdoc ICandidateStaking */ - function stake(address _consensusAddr) external payable override noEmptyValue poolExists(_consensusAddr) { + function stake(address _consensusAddr) external payable override noEmptyValue poolIsActive(_consensusAddr) { _stake(_stakingPool[_consensusAddr], msg.sender, msg.value); } /** * @inheritdoc ICandidateStaking */ - function unstake(address _consensusAddr, uint256 _amount) external override nonReentrant poolExists(_consensusAddr) { - require(_amount > 0, "CandidateStaking: invalid amount"); - address _delegator = msg.sender; + function unstake(address _consensusAddr, uint256 _amount) + external + override + nonReentrant + poolIsActive(_consensusAddr) + { + if (_amount == 0) revert ErrUnstakeZeroAmount(); + address _requester = msg.sender; PoolDetail storage _pool = _stakingPool[_consensusAddr]; uint256 _remainAmount = _pool.stakingAmount - _amount; - require(_remainAmount >= _minValidatorStakingAmount, "CandidateStaking: invalid staking amount left"); + if (_remainAmount < _minValidatorStakingAmount) revert ErrStakingAmountLeft(); - _unstake(_pool, _delegator, _amount); - require(_sendRON(payable(_delegator), _amount), "CandidateStaking: could not transfer RON"); + _unstake(_pool, _requester, _amount); + if (!_unsafeSendRON(payable(_requester), _amount, 3500)) revert ErrCannotTransferRON(); } /** @@ -128,12 +133,24 @@ abstract contract CandidateStaking is BaseStaking, ICandidateStaking { function requestRenounce(address _consensusAddr) external override - poolExists(_consensusAddr) + poolIsActive(_consensusAddr) onlyPoolAdmin(_stakingPool[_consensusAddr], msg.sender) { _validatorContract.requestRevokeCandidate(_consensusAddr, _waitingSecsToRevoke); } + /** + * @inheritdoc ICandidateStaking + */ + function requestEmergencyExit(address _consensusAddr) + external + override + poolIsActive(_consensusAddr) + onlyPoolAdmin(_stakingPool[_consensusAddr], msg.sender) + { + _validatorContract.execEmergencyExit(_consensusAddr, _waitingSecsToRevoke); + } + /** * @dev See `ICandidateStaking-applyValidatorCandidate` */ @@ -146,23 +163,17 @@ abstract contract CandidateStaking is BaseStaking, ICandidateStaking { uint256 _commissionRate, uint256 _amount ) internal { - require(_sendRON(_poolAdmin, 0), "CandidateStaking: pool admin cannot receive RON"); - require(_sendRON(_treasuryAddr, 0), "CandidateStaking: treasury cannot receive RON"); - require(_amount >= _minValidatorStakingAmount, "CandidateStaking: insufficient amount"); + if (!_unsafeSendRON(_poolAdmin, 0)) revert ErrCannotInitTransferRON(_poolAdmin, "pool admin"); + if (!_unsafeSendRON(_treasuryAddr, 0)) revert ErrCannotInitTransferRON(_treasuryAddr, "treasury"); + if (_amount < _minValidatorStakingAmount) revert ErrInsufficientStakingAmount(); - require( - _poolAdmin == _candidateAdmin && _candidateAdmin == _treasuryAddr, - "CandidateStaking: three interaction addresses must be of the same" - ); + if (_poolAdmin != _candidateAdmin || _candidateAdmin != _treasuryAddr) revert ErrThreeInteractionAddrsNotEqual(); address[] memory _diffAddrs = new address[](3); _diffAddrs[0] = _poolAdmin; _diffAddrs[1] = _consensusAddr; _diffAddrs[2] = _bridgeOperatorAddr; - require( - !AddressArrayUtils.hasDuplicate(_diffAddrs), - "CandidateStaking: three operation addresses must be distinct" - ); + if (AddressArrayUtils.hasDuplicate(_diffAddrs)) revert ErrThreeOperationAddrsNotDistinct(); _validatorContract.grantValidatorCandidate( _candidateAdmin, @@ -195,11 +206,10 @@ abstract contract CandidateStaking is BaseStaking, ICandidateStaking { address _requester, uint256 _amount ) internal onlyPoolAdmin(_pool, _requester) { - require(_amount <= _pool.stakingAmount, "CandidateStaking: insufficient staking amount"); - require( - _pool.lastDelegatingTimestamp[_requester] + _cooldownSecsToUndelegate <= block.timestamp, - "CandidateStaking: unstake too early" - ); + if (_amount > _pool.stakingAmount) revert ErrInsufficientStakingAmount(); + if (_pool.lastDelegatingTimestamp[_requester] + _cooldownSecsToUndelegate > block.timestamp) { + revert ErrUnstakeTooEarly(); + } _pool.stakingAmount -= _amount; _changeDelegatingAmount(_pool, _requester, _pool.stakingAmount, _pool.stakingTotal - _amount); diff --git a/contracts/ronin/staking/DelegatorStaking.sol b/contracts/ronin/staking/DelegatorStaking.sol index efa89a08a..524c27bf0 100644 --- a/contracts/ronin/staking/DelegatorStaking.sol +++ b/contracts/ronin/staking/DelegatorStaking.sol @@ -15,8 +15,8 @@ abstract contract DelegatorStaking is BaseStaking, IDelegatorStaking { /** * @inheritdoc IDelegatorStaking */ - function delegate(address _consensusAddr) external payable noEmptyValue poolExists(_consensusAddr) { - require(!isActivePoolAdmin(msg.sender), "DelegatorStaking: admin of an active pool cannot delegate"); + function delegate(address _consensusAddr) external payable noEmptyValue poolIsActive(_consensusAddr) { + if (isAdminOfActivePool(msg.sender)) revert ErrAdminOfAnyActivePoolForbidden(msg.sender); _delegate(_stakingPool[_consensusAddr], msg.sender, msg.value); } @@ -26,17 +26,14 @@ abstract contract DelegatorStaking is BaseStaking, IDelegatorStaking { function undelegate(address _consensusAddr, uint256 _amount) external nonReentrant { address payable _delegator = payable(msg.sender); _undelegate(_stakingPool[_consensusAddr], _delegator, _amount); - require(_sendRON(_delegator, _amount), "DelegatorStaking: could not transfer RON"); + if (!_sendRON(_delegator, _amount)) revert ErrCannotTransferRON(); } /** * @inheritdoc IDelegatorStaking */ function bulkUndelegate(address[] calldata _consensusAddrs, uint256[] calldata _amounts) external nonReentrant { - require( - _consensusAddrs.length > 0 && _consensusAddrs.length == _amounts.length, - "DelegatorStaking: invalid array length" - ); + if (_consensusAddrs.length == 0 || _consensusAddrs.length != _amounts.length) revert ErrInvalidArrays(); address payable _delegator = payable(msg.sender); uint256 _total; @@ -46,7 +43,7 @@ abstract contract DelegatorStaking is BaseStaking, IDelegatorStaking { _undelegate(_stakingPool[_consensusAddrs[_i]], _delegator, _amounts[_i]); } - require(_sendRON(_delegator, _total), "DelegatorStaking: could not transfer RON"); + if (!_sendRON(_delegator, _total)) revert ErrCannotTransferRON(); } /** @@ -56,7 +53,7 @@ abstract contract DelegatorStaking is BaseStaking, IDelegatorStaking { address _consensusAddrSrc, address _consensusAddrDst, uint256 _amount - ) external nonReentrant poolExists(_consensusAddrDst) { + ) external nonReentrant poolIsActive(_consensusAddrDst) { address _delegator = msg.sender; _undelegate(_stakingPool[_consensusAddrSrc], _delegator, _amount); _delegate(_stakingPool[_consensusAddrDst], _delegator, _amount); @@ -82,7 +79,7 @@ abstract contract DelegatorStaking is BaseStaking, IDelegatorStaking { external override nonReentrant - poolExists(_consensusAddrDst) + poolIsActive(_consensusAddrDst) returns (uint256 _amount) { return _delegateRewards(msg.sender, _consensusAddrList, _consensusAddrDst); @@ -150,12 +147,12 @@ abstract contract DelegatorStaking is BaseStaking, IDelegatorStaking { address _delegator, uint256 _amount ) private notPoolAdmin(_pool, _delegator) { - require(_amount > 0, "DelegatorStaking: invalid amount"); - require(_pool.delegatingAmount[_delegator] >= _amount, "DelegatorStaking: insufficient amount to undelegate"); - require( - _pool.lastDelegatingTimestamp[_delegator] + _cooldownSecsToUndelegate < block.timestamp, - "DelegatorStaking: undelegate too early" - ); + if (_amount == 0) revert ErrUndelegateZeroAmount(); + if (_pool.delegatingAmount[_delegator] < _amount) revert ErrInsufficientDelegatingAmount(); + if (_pool.lastDelegatingTimestamp[_delegator] + _cooldownSecsToUndelegate >= block.timestamp) { + revert ErrUndelegateTooEarly(); + } + _changeDelegatingAmount( _pool, _delegator, diff --git a/contracts/ronin/staking/RewardCalculation.sol b/contracts/ronin/staking/RewardCalculation.sol index f56e500ad..81de55ae7 100644 --- a/contracts/ronin/staking/RewardCalculation.sol +++ b/contracts/ronin/staking/RewardCalculation.sol @@ -132,7 +132,7 @@ abstract contract RewardCalculation is IRewardPool { uint256 _diffAmount = _reward.minAmount - _minAmount; if (_diffAmount > 0) { _reward.minAmount = _minAmount; - require(_pool.shares.inner >= _diffAmount, "RewardCalculation: invalid pool shares"); + if (_pool.shares.inner < _diffAmount) revert ErrInvalidPoolShare(); _pool.shares.inner -= _diffAmount; } } diff --git a/contracts/ronin/staking/Staking.sol b/contracts/ronin/staking/Staking.sol index 74a33e6c5..d2ff4cd00 100644 --- a/contracts/ronin/staking/Staking.sol +++ b/contracts/ronin/staking/Staking.sol @@ -35,34 +35,7 @@ contract Staking is IStaking, CandidateStaking, DelegatorStaking, Initializable /** * @inheritdoc IStaking */ - function getStakingPool(address _poolAddr) - external - view - poolExists(_poolAddr) - returns ( - address _admin, - uint256 _stakingAmount, - uint256 _stakingTotal - ) - { - PoolDetail storage _pool = _stakingPool[_poolAddr]; - return (_pool.admin, _pool.stakingAmount, _pool.stakingTotal); - } - - /** - * @inheritdoc IStaking - */ - function getManySelfStakings(address[] calldata _pools) external view returns (uint256[] memory _selfStakings) { - _selfStakings = new uint256[](_pools.length); - for (uint _i = 0; _i < _pools.length; _i++) { - _selfStakings[_i] = _stakingPool[_pools[_i]].stakingAmount; - } - } - - /** - * @inheritdoc IStaking - */ - function recordRewards( + function execRecordRewards( address[] calldata _consensusAddrs, uint256[] calldata _rewards, uint256 _period @@ -73,14 +46,14 @@ contract Staking is IStaking, CandidateStaking, DelegatorStaking, Initializable /** * @inheritdoc IStaking */ - function deductStakingAmount(address _consensusAddr, uint256 _amount) + function execDeductStakingAmount(address _consensusAddr, uint256 _amount) external onlyValidatorContract returns (uint256 _actualDeductingAmount) { _actualDeductingAmount = _deductStakingAmount(_stakingPool[_consensusAddr], _amount); address payable _recipientAddr = payable(validatorContract()); - if (!_unsafeSendRON(_recipientAddr, _actualDeductingAmount)) { + if (!_unsafeSendRON(_recipientAddr, _actualDeductingAmount, 3500)) { emit StakingAmountDeductFailed(_consensusAddr, _recipientAddr, _actualDeductingAmount, address(this).balance); } } diff --git a/contracts/ronin/validator/CandidateManager.sol b/contracts/ronin/validator/CandidateManager.sol index 221e52e5c..dbe3452e8 100644 --- a/contracts/ronin/validator/CandidateManager.sol +++ b/contracts/ronin/validator/CandidateManager.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.9; -import "@openzeppelin/contracts/utils/Strings.sol"; import "../../extensions/collections/HasStakingContract.sol"; import "../../extensions/consumers/PercentageConsumer.sol"; import "../../interfaces/validator/ICandidateManager.sol"; @@ -72,48 +71,15 @@ abstract contract CandidateManager is ICandidateManager, PercentageConsumer, Has uint256 _commissionRate ) external override onlyStakingContract { uint256 _length = _candidates.length; - require(_length < maxValidatorCandidate(), "CandidateManager: exceeds maximum number of candidates"); - require(!isValidatorCandidate(_consensusAddr), "CandidateManager: query for already existent candidate"); - require(_commissionRate <= _MAX_PERCENTAGE, "CandidateManager: invalid comission rate"); + if (_length >= maxValidatorCandidate()) revert ErrExceedsMaxNumberOfCandidate(); + if (isValidatorCandidate(_consensusAddr)) revert ErrExistentCandidate(); + if (_commissionRate > _MAX_PERCENTAGE) revert ErrInvalidCommissionRate(); for (uint _i = 0; _i < _candidates.length; _i++) { ValidatorCandidate storage existentInfo = _candidateInfo[_candidates[_i]]; - - if (_candidateAdmin == existentInfo.admin) { - revert( - string( - abi.encodePacked( - "CandidateManager: candidate admin address ", - Strings.toHexString(uint160(_candidateAdmin), 20), - " is already exist" - ) - ) - ); - } - - if (_treasuryAddr == existentInfo.treasuryAddr) { - revert( - string( - abi.encodePacked( - "CandidateManager: treasury address ", - Strings.toHexString(uint160(address(_treasuryAddr)), 20), - " is already exist" - ) - ) - ); - } - - if (_bridgeOperatorAddr == existentInfo.bridgeOperatorAddr) { - revert( - string( - abi.encodePacked( - "CandidateManager: bridge operator address ", - Strings.toHexString(uint160(_bridgeOperatorAddr), 20), - " is already exist" - ) - ) - ); - } + if (_candidateAdmin == existentInfo.admin) revert ErrExistentCandidateAdmin(_candidateAdmin); + if (_treasuryAddr == existentInfo.treasuryAddr) revert ErrExistentTreasury(_treasuryAddr); + if (_bridgeOperatorAddr == existentInfo.bridgeOperatorAddr) revert ErrExistentBridgeOperator(_bridgeOperatorAddr); } _candidateIndex[_consensusAddr] = ~_length; @@ -132,13 +98,9 @@ abstract contract CandidateManager is ICandidateManager, PercentageConsumer, Has * @inheritdoc ICandidateManager */ function requestRevokeCandidate(address _consensusAddr, uint256 _secsLeft) external override onlyStakingContract { - require(isValidatorCandidate(_consensusAddr), "CandidateManager: query for non-existent candidate"); ValidatorCandidate storage _info = _candidateInfo[_consensusAddr]; - require(_info.revokingTimestamp == 0, "CandidateManager: already requested before"); - - uint256 _revokingTimestamp = block.timestamp + _secsLeft; - _info.revokingTimestamp = _revokingTimestamp; - emit CandidateRevokingTimestampUpdated(_consensusAddr, _revokingTimestamp); + if (_info.revokingTimestamp != 0) revert ErrAlreadyRequestedRevokingCandidate(); + _setRevokingTimestamp(_info, block.timestamp + _secsLeft); } /** @@ -149,12 +111,11 @@ abstract contract CandidateManager is ICandidateManager, PercentageConsumer, Has uint256 _effectiveDaysOnwards, uint256 _commissionRate ) external override onlyStakingContract { - require( - _candidateCommissionChangeSchedule[_consensusAddr].effectiveTimestamp == 0, - "CandidateManager: commission change schedule exists" - ); - require(_commissionRate <= _MAX_PERCENTAGE, "CandidateManager: invalid commission rate"); - require(_effectiveDaysOnwards >= _minEffectiveDaysOnwards, "CandidateManager: invalid effective date"); + if (_candidateCommissionChangeSchedule[_consensusAddr].effectiveTimestamp != 0) { + revert ErrAlreadyRequestedUpdatingCommissionRate(); + } + if (_commissionRate > _MAX_PERCENTAGE) revert ErrInvalidCommissionRate(); + if (_effectiveDaysOnwards < _minEffectiveDaysOnwards) revert ErrInvalidEffectiveDaysOnwards(); CommissionSchedule storage _schedule = _candidateCommissionChangeSchedule[_consensusAddr]; uint256 _effectiveTimestamp = ((block.timestamp / 1 days) + _effectiveDaysOnwards) * 1 days; @@ -185,7 +146,7 @@ abstract contract CandidateManager is ICandidateManager, PercentageConsumer, Has * @inheritdoc ICandidateManager */ function getCandidateInfo(address _candidate) external view override returns (ValidatorCandidate memory) { - require(isValidatorCandidate(_candidate), "CandidateManager: query for non-existent candidate"); + if (!isValidatorCandidate(_candidate)) revert ErrNonExistentCandidate(); return _candidateInfo[_candidate]; } @@ -244,7 +205,8 @@ abstract contract CandidateManager is ICandidateManager, PercentageConsumer, Has } // Removes unsastisfied candidates - bool _revokingActivated = _info.revokingTimestamp != 0 && _info.revokingTimestamp <= block.timestamp; + bool _revokingActivated = (_info.revokingTimestamp != 0 && _info.revokingTimestamp <= block.timestamp) || + _emergencyExitLockedFundReleased(_addr); bool _topupDeadlineMissed = _info.topupDeadline != 0 && _info.topupDeadline <= block.timestamp; if (_revokingActivated || _topupDeadlineMissed) { _selfStakings[_i] = _selfStakings[--_length]; @@ -307,7 +269,7 @@ abstract contract CandidateManager is ICandidateManager, PercentageConsumer, Has * */ function _setMinEffectiveDaysOnwards(uint256 _numOfDays) internal { - require(_numOfDays >= 1, "CandidateManager: invalid min effective days onwards"); + if (_numOfDays < 1) revert ErrInvalidMinEffectiveDaysOnwards(); _minEffectiveDaysOnwards = _numOfDays; emit MinEffectiveDaysOnwardsUpdated(_numOfDays); } @@ -315,7 +277,7 @@ abstract contract CandidateManager is ICandidateManager, PercentageConsumer, Has /** * @dev Removes the candidate. */ - function _removeCandidate(address _addr) private { + function _removeCandidate(address _addr) internal virtual { uint256 _idx = _candidateIndex[_addr]; if (_idx == 0) { return; @@ -326,7 +288,6 @@ abstract contract CandidateManager is ICandidateManager, PercentageConsumer, Has delete _candidateCommissionChangeSchedule[_addr]; address _lastCandidate = _candidates[_candidates.length - 1]; - if (_lastCandidate != _addr) { _candidateIndex[_lastCandidate] = _idx; _candidates[~_idx] = _lastCandidate; @@ -334,4 +295,18 @@ abstract contract CandidateManager is ICandidateManager, PercentageConsumer, Has _candidates.pop(); } + + /** + * @dev Sets timestamp to revoke a candidate. + */ + function _setRevokingTimestamp(ValidatorCandidate storage _candidate, uint256 _timestamp) internal { + if (!isValidatorCandidate(_candidate.consensusAddr)) revert ErrNonExistentCandidate(); + _candidate.revokingTimestamp = _timestamp; + emit CandidateRevokingTimestampUpdated(_candidate.consensusAddr, _timestamp); + } + + /** + * @dev Returns a flag indicating whether the fund is unlocked. + */ + function _emergencyExitLockedFundReleased(address _consensusAddr) internal virtual returns (bool); } diff --git a/contracts/ronin/validator/CoinbaseExecution.sol b/contracts/ronin/validator/CoinbaseExecution.sol index b8ef5ab62..7abdde79e 100644 --- a/contracts/ronin/validator/CoinbaseExecution.sol +++ b/contracts/ronin/validator/CoinbaseExecution.sol @@ -10,40 +10,37 @@ import "../../extensions/RONTransferHelper.sol"; import "../../interfaces/validator/ICoinbaseExecution.sol"; import "../../libraries/EnumFlags.sol"; import "../../libraries/Math.sol"; -import "../../precompile-usages/PrecompileUsageSortValidators.sol"; -import "../../precompile-usages/PrecompileUsagePickValidatorSet.sol"; +import "../../precompile-usages/PCUSortValidators.sol"; +import "../../precompile-usages/PCUPickValidatorSet.sol"; import "./storage-fragments/CommonStorage.sol"; import "./CandidateManager.sol"; +import "./EmergencyExit.sol"; abstract contract CoinbaseExecution is ICoinbaseExecution, RONTransferHelper, - PrecompileUsageSortValidators, - PrecompileUsagePickValidatorSet, + PCUSortValidators, + PCUPickValidatorSet, HasStakingVestingContract, HasBridgeTrackingContract, HasMaintenanceContract, HasSlashIndicatorContract, - CandidateManager, - CommonStorage + EmergencyExit { using EnumFlags for EnumFlags.ValidatorFlag; modifier onlyCoinbase() { - require(msg.sender == block.coinbase, "RoninValidatorSet: method caller must be coinbase"); + if (msg.sender != block.coinbase) revert ErrCallerMustBeCoinbase(); _; } modifier whenEpochEnding() { - require(epochEndingAt(block.number), "RoninValidatorSet: only allowed at the end of epoch"); + if (!epochEndingAt(block.number)) revert ErrAtEndOfEpochOnly(); _; } modifier oncePerEpoch() { - require( - epochOf(_lastUpdatedBlock) < epochOf(block.number), - "RoninValidatorSet: query for already wrapped up epoch" - ); + if (epochOf(_lastUpdatedBlock) >= epochOf(block.number)) revert ErrAlreadyWrappedEpoch(); _lastUpdatedBlock = block.number; _; } @@ -104,6 +101,7 @@ abstract contract CoinbaseExecution is address[] memory _currentValidators = getValidators(); uint256 _epoch = epochOf(block.number); + uint256 _nextEpoch = _epoch + 1; uint256 _lastPeriod = currentPeriod(); if (_periodEnding) { @@ -113,13 +111,14 @@ abstract contract CoinbaseExecution is uint256[] memory _delegatingRewards ) = _distributeRewardToTreasuriesAndCalculateTotalDelegatingReward(_lastPeriod, _currentValidators); _settleAndTransferDelegatingRewards(_lastPeriod, _currentValidators, _totalDelegatingReward, _delegatingRewards); + _tryRecycleLockedFundsFromEmergencyExits(); _recycleDeprecatedRewards(); _slashIndicatorContract.updateCreditScores(_currentValidators, _lastPeriod); _currentValidators = _syncValidatorSet(_newPeriod); } - _revampBlockProducers(_newPeriod, _currentValidators); + _revampRoles(_newPeriod, _nextEpoch, _currentValidators); emit WrappedUpEpoch(_lastPeriod, _epoch, _periodEnding); - _periodOf[_epoch + 1] = _newPeriod; + _periodOf[_nextEpoch] = _newPeriod; _lastUpdatedPeriod = _newPeriod; } @@ -189,11 +188,14 @@ abstract contract CoinbaseExecution is if (_missedRatio > _ratioTier2) { _bridgeRewardDeprecatedAtPeriod[_validator][_period] = true; _miningRewardDeprecatedAtPeriod[_validator][_period] = true; - _jailedUntil[_validator] = Math.max(block.number + _jailDurationTier2, _jailedUntil[_validator]); - emit ValidatorPunished(_validator, _period, _jailedUntil[_validator], 0, true, true); + _blockProducerJailedBlock[_validator] = Math.max( + block.number + _jailDurationTier2, + _blockProducerJailedBlock[_validator] + ); + emit ValidatorPunished(_validator, _period, _blockProducerJailedBlock[_validator], 0, true, true); } else if (_missedRatio > _ratioTier1) { _bridgeRewardDeprecatedAtPeriod[_validator][_period] = true; - emit ValidatorPunished(_validator, _period, _jailedUntil[_validator], 0, false, true); + emit ValidatorPunished(_validator, _period, _blockProducerJailedBlock[_validator], 0, false, true); } else if (_totalBallots > 0) { _bridgeOperatingReward[_validator] = (_totalBridgeReward * _validatorBallots) / _totalBallots; } @@ -252,7 +254,7 @@ abstract contract CoinbaseExecution is function _distributeMiningReward(address _consensusAddr, address payable _treasury) private { uint256 _amount = _miningReward[_consensusAddr]; if (_amount > 0) { - if (_unsafeSendRON(_treasury, _amount)) { + if (_unsafeSendRON(_treasury, _amount, 3500)) { emit MiningRewardDistributed(_consensusAddr, _treasury, _amount); return; } @@ -277,7 +279,7 @@ abstract contract CoinbaseExecution is ) private { uint256 _amount = _bridgeOperatingReward[_consensusAddr]; if (_amount > 0) { - if (_unsafeSendRON(_treasury, _amount)) { + if (_unsafeSendRON(_treasury, _amount, 3500)) { emit BridgeOperatorRewardDistributed(_consensusAddr, _bridgeOperator, _treasury, _amount); return; } @@ -311,12 +313,17 @@ abstract contract CoinbaseExecution is IStaking _staking = _stakingContract; if (_totalDelegatingReward > 0) { if (_unsafeSendRON(payable(address(_staking)), _totalDelegatingReward)) { - _staking.recordRewards(_currentValidators, _delegatingRewards, _period); - emit StakingRewardDistributed(_totalDelegatingReward); + _staking.execRecordRewards(_currentValidators, _delegatingRewards, _period); + emit StakingRewardDistributed(_totalDelegatingReward, _currentValidators, _delegatingRewards); return; } - emit StakingRewardDistributionFailed(_totalDelegatingReward, address(this).balance); + emit StakingRewardDistributionFailed( + _totalDelegatingReward, + _currentValidators, + _delegatingRewards, + address(this).balance + ); } } @@ -350,7 +357,6 @@ abstract contract CoinbaseExecution is * @dev Updates the validator set based on the validator candidates from the Staking contract. * * Emits the `ValidatorSetUpdated` event. - * Emits the `BridgeOperatorSetUpdated` event. * * Note: This method should be called once in the end of each period. * @@ -368,7 +374,6 @@ abstract contract CoinbaseExecution is _maxPrioritizedValidatorNumber ); _setNewValidatorSet(_newValidators, _newValidatorCount, _newPeriod); - emit BridgeOperatorSetUpdated(_newPeriod, getBridgeOperators()); } /** @@ -414,43 +419,38 @@ abstract contract CoinbaseExecution is * - This method is called at the end of each epoch * * Emits the `BlockProducerSetUpdated` event. + * Emits the `BridgeOperatorSetUpdated` event. * */ - function _revampBlockProducers(uint256 _newPeriod, address[] memory _currentValidators) private { + function _revampRoles( + uint256 _newPeriod, + uint256 _nextEpoch, + address[] memory _currentValidators + ) private { bool[] memory _maintainedList = _maintenanceContract.checkManyMaintained(_candidates, block.number + 1); for (uint _i = 0; _i < _currentValidators.length; _i++) { - address _currentValidator = _currentValidators[_i]; - bool _isProducerBefore = isBlockProducer(_currentValidator); - bool _isProducerAfter = !(_jailed(_currentValidator) || _maintainedList[_i]); + address _validator = _currentValidators[_i]; + bool _emergencyExitRequested = block.timestamp <= _emergencyExitJailedTimestamp[_validator]; + bool _isProducerBefore = isBlockProducer(_validator); + bool _isProducerAfter = !(_jailed(_validator) || _maintainedList[_i] || _emergencyExitRequested); if (!_isProducerBefore && _isProducerAfter) { - _validatorMap[_currentValidator] = _validatorMap[_currentValidator].addFlag( - EnumFlags.ValidatorFlag.BlockProducer - ); - continue; + _validatorMap[_validator] = _validatorMap[_validator].addFlag(EnumFlags.ValidatorFlag.BlockProducer); + } else if (_isProducerBefore && !_isProducerAfter) { + _validatorMap[_validator] = _validatorMap[_validator].removeFlag(EnumFlags.ValidatorFlag.BlockProducer); } - if (_isProducerBefore && !_isProducerAfter) { - _validatorMap[_currentValidator] = _validatorMap[_currentValidator].removeFlag( - EnumFlags.ValidatorFlag.BlockProducer - ); + bool _isBridgeOperatorBefore = isOperatingBridge(_validator); + bool _isBridgeOperatorAfter = !_emergencyExitRequested; + if (!_isBridgeOperatorBefore && _isBridgeOperatorAfter) { + _validatorMap[_validator] = _validatorMap[_validator].addFlag(EnumFlags.ValidatorFlag.BridgeOperator); + } else if (_isBridgeOperatorBefore && !_isBridgeOperatorAfter) { + _validatorMap[_validator] = _validatorMap[_validator].removeFlag(EnumFlags.ValidatorFlag.BridgeOperator); } } - emit BlockProducerSetUpdated(_newPeriod, getBlockProducers()); - } - - /** - * @dev Override `ValidatorInfoStorage-_bridgeOperatorOf`. - */ - function _bridgeOperatorOf(address _consensusAddr) - internal - view - virtual - override(CandidateManager, ValidatorInfoStorage) - returns (address) - { - return CandidateManager._bridgeOperatorOf(_consensusAddr); + emit BlockProducerSetUpdated(_newPeriod, _nextEpoch, getBlockProducers()); + emit BridgeOperatorSetUpdated(_newPeriod, _nextEpoch, getBridgeOperators()); } } diff --git a/contracts/ronin/validator/EmergencyExit.sol b/contracts/ronin/validator/EmergencyExit.sol new file mode 100644 index 000000000..998cdb9cb --- /dev/null +++ b/contracts/ronin/validator/EmergencyExit.sol @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.9; + +import "../../extensions/RONTransferHelper.sol"; +import "../../interfaces/IRoninGovernanceAdmin.sol"; +import "../../interfaces/validator/IEmergencyExit.sol"; +import "./storage-fragments/CommonStorage.sol"; +import "./CandidateManager.sol"; + +abstract contract EmergencyExit is IEmergencyExit, RONTransferHelper, CandidateManager, CommonStorage { + /** + * @inheritdoc IEmergencyExit + */ + function emergencyExitLockedAmount() external view returns (uint256) { + return _emergencyExitLockedAmount; + } + + /** + * @inheritdoc IEmergencyExit + */ + function emergencyExpiryDuration() external view returns (uint256) { + return _emergencyExpiryDuration; + } + + /** + * @inheritdoc IEmergencyExit + */ + function execEmergencyExit(address _consensusAddr, uint256 _secLeftToRevoke) external onlyStakingContract { + EmergencyExitInfo storage _info = _exitInfo[_consensusAddr]; + if (_info.recyclingAt != 0) revert ErrAlreadyRequestedEmergencyExit(); + + uint256 _revokingTimestamp = block.timestamp + _secLeftToRevoke; + _setRevokingTimestamp(_candidateInfo[_consensusAddr], _revokingTimestamp); + _emergencyExitJailedTimestamp[_consensusAddr] = _revokingTimestamp; + + uint256 _deductedAmount = _stakingContract.execDeductStakingAmount(_consensusAddr, _emergencyExitLockedAmount); + if (_deductedAmount > 0) { + uint256 _recyclingAt = block.timestamp + _emergencyExpiryDuration; + _lockedConsensusList.push(_consensusAddr); + _info.lockedAmount = _deductedAmount; + _info.recyclingAt = _recyclingAt; + IRoninGovernanceAdmin(_getAdmin()).createEmergencyExitPoll( + _consensusAddr, + _candidateInfo[_consensusAddr].treasuryAddr, + block.timestamp, + _recyclingAt + ); + } + emit EmergencyExitRequested(_consensusAddr, _deductedAmount); + } + + /** + * @inheritdoc IEmergencyExit + */ + function setEmergencyExitLockedAmount(uint256 _emergencyExitLockedAmount) external onlyAdmin { + _setEmergencyExitLockedAmount(_emergencyExitLockedAmount); + } + + /** + * @inheritdoc IEmergencyExit + */ + function setEmergencyExpiryDuration(uint256 _emergencyExpiryDuration) external onlyAdmin { + _setEmergencyExpiryDuration(_emergencyExpiryDuration); + } + + /** + * @inheritdoc IEmergencyExit + */ + function execReleaseLockedFundForEmergencyExitRequest(address _consensusAddr, address payable _recipient) + external + onlyAdmin + { + if (_exitInfo[_consensusAddr].recyclingAt == 0) { + return; + } + + uint256 _length = _lockedConsensusList.length; + uint256 _index = _length; + + for (uint _i = 0; _i < _length; _i++) { + if (_lockedConsensusList[_i] == _consensusAddr) { + _index = _i; + break; + } + } + + // The locked amount might be recycled + if (_index == _length) { + return; + } + + uint256 _amount = _exitInfo[_consensusAddr].lockedAmount; + if (_amount > 0) { + delete _exitInfo[_consensusAddr]; + if (_length > 1) { + _lockedConsensusList[_index] = _lockedConsensusList[_length - 1]; + } + _lockedConsensusList.pop(); + + _lockedFundReleased[_consensusAddr] = true; + if (_unsafeSendRON(_recipient, _amount, 3500)) { + emit EmergencyExitLockedFundReleased(_consensusAddr, _recipient, _amount); + return; + } + + emit EmergencyExitLockedFundReleasingFailed(_consensusAddr, _recipient, _amount, address(this).balance); + } + } + + /** + * @dev Tries to recycle the locked funds from emergency exit requests. + */ + function _tryRecycleLockedFundsFromEmergencyExits() internal { + uint256 _length = _lockedConsensusList.length; + + uint256 _i; + address _addr; + EmergencyExitInfo storage _info; + + while (_i < _length) { + _addr = _lockedConsensusList[_i]; + _info = _exitInfo[_addr]; + + if (_info.recyclingAt <= block.timestamp) { + _totalDeprecatedReward += _info.lockedAmount; + + delete _exitInfo[_addr]; + if (--_length > 0) { + _lockedConsensusList[_i] = _lockedConsensusList[_length]; + } + _lockedConsensusList.pop(); + continue; + } + + _i++; + } + } + + /** + * @dev Override `CandidateManager-_emergencyExitLockedFundReleased`. + */ + function _emergencyExitLockedFundReleased(address _consensusAddr) internal virtual override returns (bool) { + return _lockedFundReleased[_consensusAddr]; + } + + /** + * @dev Override `CandidateManager-_removeCandidate`. + */ + function _removeCandidate(address _consensusAddr) internal override { + delete _lockedFundReleased[_consensusAddr]; + super._removeCandidate(_consensusAddr); + } + + /** + * @dev Override `ValidatorInfoStorage-_bridgeOperatorOf`. + */ + function _bridgeOperatorOf(address _consensusAddr) + internal + view + virtual + override(CandidateManager, ValidatorInfoStorage) + returns (address) + { + return CandidateManager._bridgeOperatorOf(_consensusAddr); + } + + /** + * @dev See `setEmergencyExitLockedAmount. + */ + function _setEmergencyExitLockedAmount(uint256 _amount) internal { + _emergencyExitLockedAmount = _amount; + emit EmergencyExitLockedAmountUpdated(_amount); + } + + /** + * @dev See `setEmergencyExpiryDuration`. + */ + function _setEmergencyExpiryDuration(uint256 _duration) internal { + _emergencyExpiryDuration = _duration; + emit EmergencyExpiryDurationUpdated(_duration); + } +} diff --git a/contracts/ronin/validator/RoninValidatorSet.sol b/contracts/ronin/validator/RoninValidatorSet.sol index dc851bf74..1d33d134e 100644 --- a/contracts/ronin/validator/RoninValidatorSet.sol +++ b/contracts/ronin/validator/RoninValidatorSet.sol @@ -34,7 +34,10 @@ contract RoninValidatorSet is Initializable, CoinbaseExecution, SlashingExecutio uint256 __maxValidatorCandidate, uint256 __maxPrioritizedValidatorNumber, uint256 __minEffectiveDaysOnwards, - uint256 __numberOfBlocksInEpoch + uint256 __numberOfBlocksInEpoch, + // __emergencyExitConfigs[0]: emergencyExitLockedAmount + // __emergencyExitConfigs[1]: emergencyExpiryDuration + uint256[2] calldata __emergencyExitConfigs ) external initializer { _setSlashIndicatorContract(__slashIndicatorContract); _setStakingContract(__stakingContract); @@ -46,18 +49,17 @@ contract RoninValidatorSet is Initializable, CoinbaseExecution, SlashingExecutio _setMaxValidatorCandidate(__maxValidatorCandidate); _setMaxPrioritizedValidatorNumber(__maxPrioritizedValidatorNumber); _setMinEffectiveDaysOnwards(__minEffectiveDaysOnwards); + _setEmergencyExitLockedAmount(__emergencyExitConfigs[0]); + _setEmergencyExpiryDuration(__emergencyExitConfigs[1]); _numberOfBlocksInEpoch = __numberOfBlocksInEpoch; } /** * @dev Only receives RON from staking vesting contract (for topping up bonus), and from staking contract (for transferring - * deducting amount when slash). + * deducting amount on slashing). */ function _fallback() internal view { - require( - msg.sender == stakingVestingContract() || msg.sender == stakingContract(), - "RoninValidatorSet: only receives RON from staking vesting contract or staking contract" - ); + if (msg.sender != stakingVestingContract() && msg.sender != stakingContract()) revert UnauthorizedReceiveRON(); } /** @@ -66,9 +68,9 @@ contract RoninValidatorSet is Initializable, CoinbaseExecution, SlashingExecutio function _bridgeOperatorOf(address _consensusAddr) internal view - override(CoinbaseExecution, ValidatorInfoStorage) + override(EmergencyExit, ValidatorInfoStorage) returns (address) { - return CoinbaseExecution._bridgeOperatorOf(_consensusAddr); + return super._bridgeOperatorOf(_consensusAddr); } } diff --git a/contracts/ronin/validator/SlashingExecution.sol b/contracts/ronin/validator/SlashingExecution.sol index 8fee33f57..fc0b36421 100644 --- a/contracts/ronin/validator/SlashingExecution.sol +++ b/contracts/ronin/validator/SlashingExecution.sol @@ -29,16 +29,23 @@ abstract contract SlashingExecution is delete _miningReward[_validatorAddr]; delete _delegatingReward[_validatorAddr]; - if (_newJailedUntil > _jailedUntil[_validatorAddr]) { - _jailedUntil[_validatorAddr] = _newJailedUntil; + if (_newJailedUntil > _blockProducerJailedBlock[_validatorAddr]) { + _blockProducerJailedBlock[_validatorAddr] = _newJailedUntil; } if (_slashAmount > 0) { - uint256 _actualAmount = _stakingContract.deductStakingAmount(_validatorAddr, _slashAmount); + uint256 _actualAmount = _stakingContract.execDeductStakingAmount(_validatorAddr, _slashAmount); _totalDeprecatedReward += _actualAmount; } - emit ValidatorPunished(_validatorAddr, _period, _jailedUntil[_validatorAddr], _slashAmount, true, false); + emit ValidatorPunished( + _validatorAddr, + _period, + _blockProducerJailedBlock[_validatorAddr], + _slashAmount, + true, + false + ); } /** @@ -49,7 +56,7 @@ abstract contract SlashingExecution is // removed previously in the `slash` function. _miningRewardBailoutCutOffAtPeriod[_validatorAddr][_period] = true; _miningRewardDeprecatedAtPeriod[_validatorAddr][_period] = false; - _jailedUntil[_validatorAddr] = block.number - 1; + _blockProducerJailedBlock[_validatorAddr] = block.number - 1; emit ValidatorUnjailed(_validatorAddr, _period); } diff --git a/contracts/ronin/validator/storage-fragments/CommonStorage.sol b/contracts/ronin/validator/storage-fragments/CommonStorage.sol index 7eee3b1a7..3b7606122 100644 --- a/contracts/ronin/validator/storage-fragments/CommonStorage.sol +++ b/contracts/ronin/validator/storage-fragments/CommonStorage.sol @@ -21,11 +21,35 @@ abstract contract CommonStorage is ICommonInfo, TimingStorage, JailingStorage, V /// @dev The deprecated reward that has not been withdrawn by admin uint256 internal _totalDeprecatedReward; + /// @dev The amount of RON to lock from a consensus address. + uint256 internal _emergencyExitLockedAmount; + /// @dev The duration that an emergency request is expired and the fund will be recycled. + uint256 internal _emergencyExpiryDuration; + /// @dev The address list of consensus addresses that being locked fund. + address[] internal _lockedConsensusList; + /// @dev Mapping from consensus => request exist info + mapping(address => EmergencyExitInfo) internal _exitInfo; + /// @dev Mapping from consensus => flag indicating whether the locked fund is released + mapping(address => bool) internal _lockedFundReleased; + /** * @dev This empty reserved space is put in place to allow future versions to add new * variables without shifting down storage in the inheritance chain. */ - uint256[49] private ______gap; + uint256[44] private ______gap; + + /** + * @inheritdoc ICommonInfo + */ + function getEmergencyExitInfo(address _consensusAddr) + external + view + override + returns (EmergencyExitInfo memory _info) + { + _info = _exitInfo[_consensusAddr]; + require(_info.recyclingAt > 0, "CommonStorage: non-existent info"); + } /** * @inheritdoc ICommonInfo diff --git a/contracts/ronin/validator/storage-fragments/JailingStorage.sol b/contracts/ronin/validator/storage-fragments/JailingStorage.sol index e6cd57ecc..54597e1a3 100644 --- a/contracts/ronin/validator/storage-fragments/JailingStorage.sol +++ b/contracts/ronin/validator/storage-fragments/JailingStorage.sol @@ -12,14 +12,17 @@ abstract contract JailingStorage is IJailingInfo { mapping(address => mapping(uint256 => bool)) internal _miningRewardBailoutCutOffAtPeriod; /// @dev Mapping from consensus address => period number => block operator has no pending reward mapping(address => mapping(uint256 => bool)) internal _bridgeRewardDeprecatedAtPeriod; - /// @dev Mapping from consensus address => the last block that the validator is jailed - mapping(address => uint256) internal _jailedUntil; + + /// @dev Mapping from consensus address => the last block that the block producer is jailed + mapping(address => uint256) internal _blockProducerJailedBlock; + /// @dev Mapping from consensus address => the last timestamp that the bridge operator is jailed + mapping(address => uint256) internal _emergencyExitJailedTimestamp; /** * @dev This empty reserved space is put in place to allow future versions to add new * variables without shifting down storage in the inheritance chain. */ - uint256[50] private ______gap; + uint256[49] private ______gap; /** * @inheritdoc IJailingInfo @@ -64,7 +67,7 @@ abstract contract JailingStorage is IJailingInfo { uint256 epochLeft_ ) { - uint256 _jailedBlock = _jailedUntil[_addr]; + uint256 _jailedBlock = _blockProducerJailedBlock[_addr]; if (_jailedBlock < _blockNum) { return (false, 0, 0); } @@ -136,7 +139,7 @@ abstract contract JailingStorage is IJailingInfo { * @dev Returns whether the reward of the validator is put in jail (cannot join the set of validators) at a specific block. */ function _jailedAtBlock(address _validatorAddr, uint256 _blockNum) internal view returns (bool) { - return _blockNum <= _jailedUntil[_validatorAddr]; + return _blockNum <= _blockProducerJailedBlock[_validatorAddr]; } /** diff --git a/contracts/ronin/validator/storage-fragments/ValidatorInfoStorage.sol b/contracts/ronin/validator/storage-fragments/ValidatorInfoStorage.sol index 54d7b5b49..d776f8ae6 100644 --- a/contracts/ronin/validator/storage-fragments/ValidatorInfoStorage.sol +++ b/contracts/ronin/validator/storage-fragments/ValidatorInfoStorage.sol @@ -82,10 +82,17 @@ abstract contract ValidatorInfoStorage is IValidatorInfo, HasRoninTrustedOrganiz /** * @inheritdoc IValidatorInfo */ - function getBridgeOperators() public view override returns (address[] memory _bridgeOperatorList) { - _bridgeOperatorList = new address[](validatorCount); - for (uint _i = 0; _i < _bridgeOperatorList.length; _i++) { - _bridgeOperatorList[_i] = _bridgeOperatorOf(_validators[_i]); + function getBridgeOperators() public view override returns (address[] memory _result) { + _result = new address[](validatorCount); + uint256 _count = 0; + for (uint _i = 0; _i < _result.length; _i++) { + if (isBlockProducer(_validators[_i])) { + _result[_count++] = _bridgeOperatorOf(_validators[_i]); + } + } + + assembly { + mstore(_result, _count) } } @@ -94,13 +101,20 @@ abstract contract ValidatorInfoStorage is IValidatorInfo, HasRoninTrustedOrganiz */ function isBridgeOperator(address _bridgeOperatorAddr) external view override returns (bool _result) { for (uint _i = 0; _i < validatorCount; _i++) { - if (_bridgeOperatorOf(_validators[_i]) == _bridgeOperatorAddr) { + if (_bridgeOperatorOf(_validators[_i]) == _bridgeOperatorAddr && isOperatingBridge(_validators[_i])) { _result = true; break; } } } + /** + * @inheritdoc IValidatorInfo + */ + function isOperatingBridge(address _consensusAddr) public view override returns (bool) { + return _validatorMap[_consensusAddr].hasFlag(EnumFlags.ValidatorFlag.BridgeOperator); + } + /** * @inheritdoc IValidatorInfo */ @@ -116,12 +130,14 @@ abstract contract ValidatorInfoStorage is IValidatorInfo, HasRoninTrustedOrganiz } /** - * Notice: A validator is always a bride operator - * * @inheritdoc IValidatorInfo */ - function totalBridgeOperators() public view returns (uint256) { - return validatorCount; + function totalBridgeOperators() public view returns (uint256 _total) { + for (uint _i = 0; _i < validatorCount; _i++) { + if (isOperatingBridge(_validators[_i])) { + _total++; + } + } } /** @@ -138,6 +154,11 @@ abstract contract ValidatorInfoStorage is IValidatorInfo, HasRoninTrustedOrganiz _setMaxPrioritizedValidatorNumber(_number); } + /** + * @dev Returns the bridge operator of a consensus address. + */ + function _bridgeOperatorOf(address _consensusAddr) internal view virtual returns (address); + /** * @dev See `IValidatorInfo-setMaxValidatorNumber` */ @@ -150,17 +171,8 @@ abstract contract ValidatorInfoStorage is IValidatorInfo, HasRoninTrustedOrganiz * @dev See `IValidatorInfo-setMaxPrioritizedValidatorNumber` */ function _setMaxPrioritizedValidatorNumber(uint256 _number) internal { - require( - _number <= _maxValidatorNumber, - "RoninValidatorSet: cannot set number of prioritized greater than number of max validators" - ); - + if (_number > _maxValidatorNumber) revert InvalidMaxPrioitizedValidatorNumber(); _maxPrioritizedValidatorNumber = _number; emit MaxPrioritizedValidatorNumberUpdated(_number); } - - /** - * @dev Returns the bridge operator of a consensus address. - */ - function _bridgeOperatorOf(address _consensusAddr) internal view virtual returns (address); } diff --git a/src/config.ts b/src/config.ts index edb126601..073c8cf00 100644 --- a/src/config.ts +++ b/src/config.ts @@ -2,8 +2,8 @@ import { BigNumber } from 'ethers'; import { ethers } from 'hardhat'; import { GeneralConfig, - GovernanceAdminArguments, - GovernanceAdminConfig, + RoninGovernanceAdminArguments, + RoninGovernanceAdminConfig, LiteralNetwork, MainchainGovernanceAdminArguments, MainchainGovernanceAdminConfig, @@ -166,6 +166,8 @@ const defaultRoninValidatorSetConf: RoninValidatorSetArguments = { maxValidatorCandidate: 100, numberOfBlocksInEpoch: 600, minEffectiveDaysOnwards: 7, + emergencyExitLockedAmount: BigNumber.from(10).pow(18).mul(50_000), // 50.000 RON + emergencyExpiryDuration: 14 * 86400, // 14 days }; // TODO: update config for testnet & mainnet @@ -264,12 +266,12 @@ export const mainchainGovernanceAdminConf: MainchainGovernanceAdminConfig = { [Network.Ethereum]: undefined, }; -const defaultGovernanceAdminConf: GovernanceAdminArguments = { +const defaultGovernanceAdminConf: RoninGovernanceAdminArguments = { proposalExpiryDuration: 60 * 60 * 24 * 14, // 14 days }; // TODO: update config for goerli, ethereum -export const governanceAdminConf: GovernanceAdminConfig = { +export const roninGovernanceAdminConf: RoninGovernanceAdminConfig = { [Network.Local]: defaultGovernanceAdminConf, [Network.Devnet]: defaultGovernanceAdminConf, [Network.Goerli]: undefined, diff --git a/src/deploy/proxy/ronin-validator-proxy.ts b/src/deploy/proxy/ronin-validator-proxy.ts index 00223c19f..03c4ddf80 100644 --- a/src/deploy/proxy/ronin-validator-proxy.ts +++ b/src/deploy/proxy/ronin-validator-proxy.ts @@ -26,6 +26,10 @@ const deploy = async ({ getNamedAccounts, deployments }: HardhatRuntimeEnvironme roninValidatorSetConf[network.name]!.maxPrioritizedValidatorNumber, roninValidatorSetConf[network.name]!.minEffectiveDaysOnwards, roninValidatorSetConf[network.name]!.numberOfBlocksInEpoch, + [ + roninValidatorSetConf[network.name]!.emergencyExitLockedAmount, + roninValidatorSetConf[network.name]!.emergencyExpiryDuration, + ], ]); const deployment = await deploy('RoninValidatorSetProxy', { diff --git a/src/deploy/ronin-governance-admin.ts b/src/deploy/ronin-governance-admin.ts index 8b752dddb..32d771740 100644 --- a/src/deploy/ronin-governance-admin.ts +++ b/src/deploy/ronin-governance-admin.ts @@ -1,7 +1,7 @@ import { network } from 'hardhat'; import { HardhatRuntimeEnvironment } from 'hardhat/types'; -import { roninchainNetworks, generalRoninConf, governanceAdminConf } from '../config'; +import { roninchainNetworks, generalRoninConf, roninGovernanceAdminConf } from '../config'; import { verifyAddress } from '../script/verify-address'; const deploy = async ({ getNamedAccounts, deployments }: HardhatRuntimeEnvironment) => { @@ -18,7 +18,8 @@ const deploy = async ({ getNamedAccounts, deployments }: HardhatRuntimeEnvironme args: [ generalRoninConf[network.name].roninTrustedOrganizationContract?.address, generalRoninConf[network.name].bridgeContract, - governanceAdminConf[network.name]?.proposalExpiryDuration, + generalRoninConf[network.name].validatorContract?.address, + roninGovernanceAdminConf[network.name]?.proposalExpiryDuration, ], nonce: generalRoninConf[network.name].governanceAdmin?.nonce, }); diff --git a/src/script/governance-admin-interface.ts b/src/script/governance-admin-interface.ts index 8143fe74d..8541c8394 100644 --- a/src/script/governance-admin-interface.ts +++ b/src/script/governance-admin-interface.ts @@ -9,7 +9,7 @@ import { BallotTypes, getProposalHash, VoteType } from './proposal'; import { RoninGovernanceAdmin, TransparentUpgradeableProxyV2__factory } from '../types'; import { ProposalDetailStruct } from '../types/GovernanceAdmin'; import { SignatureStruct } from '../types/MainchainGovernanceAdmin'; -import { GovernanceAdminArguments } from '../utils'; +import { RoninGovernanceAdminArguments } from '../utils'; import { getLastBlockTimestamp } from '../../test/helpers/utils'; import { defaultTestConfig } from '../../test/helpers/fixture'; @@ -31,10 +31,10 @@ export class GovernanceAdminInterface { contract!: RoninGovernanceAdmin; domain!: TypedDataDomain; interface!: Interface; - args!: GovernanceAdminArguments; + args!: RoninGovernanceAdminArguments; address = ethers.constants.AddressZero; - constructor(contract: RoninGovernanceAdmin, args?: GovernanceAdminArguments, ...signers: SignerWithAddress[]) { + constructor(contract: RoninGovernanceAdmin, args?: RoninGovernanceAdminArguments, ...signers: SignerWithAddress[]) { this.contract = contract; this.signers = signers; this.address = contract.address; @@ -63,11 +63,13 @@ export class GovernanceAdminInterface { return proposal; } - async generateSignatures(proposal: ProposalDetailStruct) { + async generateSignatures(proposal: ProposalDetailStruct, signers?: SignerWithAddress[], support?: VoteType) { const proposalHash = getProposalHash(proposal); const signatures = await Promise.all( - this.signers.map((v) => - v._signTypedData(this.domain, BallotTypes, { proposalHash, support: VoteType.For }).then(mapByteSigToSigStruct) + (signers ?? this.signers).map((v) => + v + ._signTypedData(this.domain, BallotTypes, { proposalHash, support: support ?? VoteType.For }) + .then(mapByteSigToSigStruct) ) ); return signatures; diff --git a/src/script/proposal.ts b/src/script/proposal.ts index 8296a2bac..d51f3cf11 100644 --- a/src/script/proposal.ts +++ b/src/script/proposal.ts @@ -10,8 +10,10 @@ const proposalTypeHash = '0xd051578048e6ff0bbc9fca3b65a42088dbde10f36ca841de5667 const globalProposalTypeHash = '0x1463f426c05aff2c1a7a0957a71c9898bc8b47142540538e79ee25ee91141350'; // keccak256("Ballot(bytes32 proposalHash,uint8 support)") const ballotTypeHash = '0xd900570327c4c0df8dd6bdd522b7da7e39145dd049d2fd4602276adcd511e3c2'; -// keccak256("BridgeOperatorsBallot(uint256 period,address[] operators)"); -const bridgeOperatorsBallotTypeHash = '0xeea5e3908ac28cbdbbce8853e49444c558a0a03597e98ef19e6ff86162ed9ae3'; +// keccak256("BridgeOperatorsBallot(uint256 period,uint256 epoch,address[] operators)"); +const bridgeOperatorsBallotTypeHash = '0xd679a49e9e099fa9ed83a5446aaec83e746b03ec6723d6f5efb29d37d7f0b78a'; +// keccak256("EmergencyExitBallot(address consensusAddress,address recipientAfterUnlockedFund,uint256 requestedAt,uint256 expiredAt)"); +const emergencyExitBallotTypehash = '0x697acba4deaf1a718d8c2d93e42860488cb7812696f28ca10eed17bac41e7027'; export enum VoteType { For = 0, @@ -38,6 +40,7 @@ export const proposalParamTypes = [ ]; export const globalProposalParamTypes = ['bytes32', 'uint256', 'uint256', 'bytes32', 'bytes32', 'bytes32', 'bytes32']; export const bridgeOperatorsBallotParamTypes = ['bytes32', 'uint256', 'bytes32']; +export const emergencyExitBallotParamTypes = ['bytes32', 'address', 'address', 'uint256', 'uint256']; export const BallotTypes = { Ballot: [ @@ -70,10 +73,20 @@ export const GlobalProposalTypes = { export const BridgeOperatorsBallotTypes = { BridgeOperatorsBallot: [ { name: 'period', type: 'uint256' }, + { name: 'epoch', type: 'uint256' }, { name: 'operators', type: 'address[]' }, ], }; +export const EmergencyExitBallotTypes = { + EmergencyExitBallot: [ + { name: 'consensusAddress', type: 'address' }, + { name: 'recipientAfterUnlockedFund', type: 'address' }, + { name: 'requestedAt', type: 'uint256' }, + { name: 'expiredAt', type: 'uint256' }, + ], +}; + export const getProposalHash = (proposal: ProposalDetailStruct) => keccak256( AbiCoder.prototype.encode(proposalParamTypes, [ @@ -152,14 +165,16 @@ export const getBallotDigest = (domainSeparator: string, proposalHash: string, s export interface BOsBallot { period: BigNumberish; + epoch: BigNumberish; operators: Address[]; } -export const getBOsBallotHash = (period: BigNumberish, operators: Address[]) => +export const getBOsBallotHash = (period: BigNumberish, epoch: BigNumberish, operators: Address[]) => keccak256( AbiCoder.prototype.encode(bridgeOperatorsBallotParamTypes, [ bridgeOperatorsBallotTypeHash, period, + epoch, keccak256( AbiCoder.prototype.encode( operators.map(() => 'address'), @@ -169,8 +184,29 @@ export const getBOsBallotHash = (period: BigNumberish, operators: Address[]) => ]) ); -export const getBOsBallotDigest = (domainSeparator: string, period: BigNumberish, operators: Address[]): string => +export const getEmergencyExitBallotHash = ( + consensusAddress: Address, + recipientAfterUnlockedFund: Address, + requestedAt: BigNumberish, + expiredAt: BigNumberish +) => + keccak256( + AbiCoder.prototype.encode(emergencyExitBallotParamTypes, [ + emergencyExitBallotTypehash, + consensusAddress, + recipientAfterUnlockedFund, + requestedAt, + expiredAt, + ]) + ); + +export const getBOsBallotDigest = ( + domainSeparator: string, + period: BigNumberish, + epoch: BigNumberish, + operators: Address[] +): string => solidityKeccak256( ['bytes1', 'bytes1', 'bytes32', 'bytes32'], - ['0x19', '0x01', domainSeparator, getBOsBallotHash(period, operators)] + ['0x19', '0x01', domainSeparator, getBOsBallotHash(period, epoch, operators)] ); diff --git a/src/utils.ts b/src/utils.ts index 1bc5f6478..de9f19354 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,11 +1,12 @@ import { BigNumber, BigNumberish } from 'ethers'; -import { ethers, network } from 'hardhat'; +import { ethers } from 'hardhat'; import { Address } from 'hardhat-deploy/dist/types'; import { TrustedOrganizationStruct } from './types/IRoninTrustedOrganization'; export const DEFAULT_ADDRESS = '0x0000000000000000000000000000000000000000'; export const DEFAULT_ADMIN_ROLE = '0x0000000000000000000000000000000000000000000000000000000000000000'; +export const MODERATOR_ROLE = '0x71f3d55856e4058ed06ee057d79ada615f65cdf5f9ee88181b914225088f834f'; export const ZERO_BYTES32 = '0x0000000000000000000000000000000000000000000000000000000000000000'; export const MAX_UINT256 = BigNumber.from( '115792089237316195423570985008687907853269984665640564039457584007913129639936' @@ -14,6 +15,10 @@ export const MAX_UINT255 = BigNumber.from( '57896044618658097711785492504343953926634992332820282019728792003956564819968' ); +export const FORWARDER_ADMIN_SLOT = '0xa8c82e6b38a127695961bbff56774712a221ab251224d4167eab01e23fcee6ca'; +export const FORWARDER_TARGET_SLOT = '0x58221d865d4bfcbfe437720ee0c958ac3269c4e9c775f643bf474ed980d61168'; +export const FORWARDER_MODERATOR_SLOT = '0xcbec2a70e8f0a52aeb8f96e02517dc497e58d9a6fa86ab4056563f1e6baf3d3e'; + export enum Network { Local = 'local', Hardhat = 'hardhat', @@ -148,18 +153,20 @@ export interface RoninValidatorSetArguments { maxPrioritizedValidatorNumber?: BigNumberish; numberOfBlocksInEpoch?: BigNumberish; minEffectiveDaysOnwards?: BigNumberish; + emergencyExitLockedAmount?: BigNumberish; + emergencyExpiryDuration?: BigNumberish; } export interface RoninValidatorSetConfig { [network: LiteralNetwork]: RoninValidatorSetArguments | undefined; } -export interface GovernanceAdminArguments { +export interface RoninGovernanceAdminArguments { proposalExpiryDuration?: BigNumberish; } -export interface GovernanceAdminConfig { - [network: LiteralNetwork]: GovernanceAdminArguments | undefined; +export interface RoninGovernanceAdminConfig { + [network: LiteralNetwork]: RoninGovernanceAdminArguments | undefined; } export interface MainchainGovernanceAdminArguments { diff --git a/test/bridge/BridgeTracking.test.ts b/test/bridge/BridgeTracking.test.ts index 76dd9e821..5b3ea44b4 100644 --- a/test/bridge/BridgeTracking.test.ts +++ b/test/bridge/BridgeTracking.test.ts @@ -20,6 +20,7 @@ import { } from '../../src/types'; import { ERC20PresetMinterPauser } from '../../src/types/ERC20PresetMinterPauser'; import { ReceiptStruct } from '../../src/types/IRoninGatewayV2'; +import { DEFAULT_ADDRESS } from '../../src/utils'; import { createManyTrustedOrganizationAddressSets, createManyValidatorCandidateAddressSets, @@ -43,7 +44,6 @@ let roninValidatorSet: MockRoninValidatorSetExtended; let governanceAdmin: RoninGovernanceAdmin; let governanceAdminInterface: GovernanceAdminInterface; let token: ERC20PresetMinterPauser; -let localEpochController: EpochController; let period: BigNumberish; let receipts: ReceiptStruct[]; @@ -86,7 +86,6 @@ describe('Bridge Tracking test', () => { ]) ); bridgeContract = RoninGatewayV2__factory.connect(proxy.address, deployer); - localEpochController = new EpochController(0, numberOfBlocksInEpoch); await token.grantRole(await token.MINTER_ROLE(), bridgeContract.address); // Deploys DPoS contracts @@ -127,6 +126,7 @@ describe('Bridge Tracking test', () => { const mockValidatorLogic = await new MockRoninValidatorSetExtended__factory(deployer).deploy(); await mockValidatorLogic.deployed(); await governanceAdminInterface.upgrade(roninValidatorSet.address, mockValidatorLogic.address); + await roninValidatorSet.initEpoch(); await TransparentUpgradeableProxyV2__factory.connect(proxy.address, deployer).changeAdmin(governanceAdmin.address); await governanceAdminInterface.functionDelegateCalls( @@ -160,6 +160,10 @@ describe('Bridge Tracking test', () => { expect(await roninValidatorSet.getBridgeOperators()).eql(candidates.map((v) => v.bridgeOperator.address)); }); + after(async () => { + await network.provider.send('hardhat_setCoinbase', [DEFAULT_ADDRESS]); + }); + it('Should be able to get contract configs correctly', async () => { expect(await bridgeTracking.bridgeContract()).eq(bridgeContract.address); expect(await bridgeContract.bridgeTrackingContract()).eq(bridgeTracking.address); @@ -250,7 +254,6 @@ describe('Bridge Tracking test', () => { submitWithdrawalSignatures.map(() => []) ); - await EpochController.setTimestampToPeriodEnding(); await mineBatchTxs(async () => { await roninValidatorSet.endEpoch(); await roninValidatorSet.connect(coinbase).wrapUpEpoch(); @@ -265,6 +268,11 @@ describe('Bridge Tracking test', () => { }); it('Should not record in the next period', async () => { + await EpochController.setTimestampToPeriodEnding(); + await mineBatchTxs(async () => { + await roninValidatorSet.endEpoch(); + await roninValidatorSet.connect(coinbase).wrapUpEpoch(); + }); const newPeriod = await roninValidatorSet.currentPeriod(); expect(newPeriod).not.eq(period); diff --git a/test/forwarder/VaultForwarder.test.ts b/test/forwarder/VaultForwarder.test.ts new file mode 100644 index 000000000..f0a9d9671 --- /dev/null +++ b/test/forwarder/VaultForwarder.test.ts @@ -0,0 +1,271 @@ +import { expect } from 'chai'; +import { BigNumber } from 'ethers'; +import { ethers, network } from 'hardhat'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; + +import { + VaultForwarder, + VaultForwarder__factory, + MockForwarderTarget, + MockForwarderTarget__factory, +} from '../../src/types'; +import { + DEFAULT_ADDRESS, + FORWARDER_ADMIN_SLOT, + FORWARDER_TARGET_SLOT, + FORWARDER_MODERATOR_SLOT, +} from '../../src/utils'; +import { calculateAddress } from '../helpers/utils'; +import { parseEther } from 'ethers/lib/utils'; + +let deployer: SignerWithAddress; +let admin: SignerWithAddress; +let moderator: SignerWithAddress; +let unauthorized: SignerWithAddress; +let forwarder: VaultForwarder; +let targetBehindForwarder: MockForwarderTarget; +let target: MockForwarderTarget; +let localData: BigNumber; + +describe('Vault forwarder', () => { + before(async () => { + [deployer, admin, moderator, unauthorized] = await ethers.getSigners(); + + let nonce = await ethers.provider.getTransactionCount(deployer.address); + let forwarderAddress = calculateAddress(deployer.address, nonce + 1).address; + + localData = BigNumber.from(0); + target = await new MockForwarderTarget__factory(deployer).deploy(forwarderAddress, localData); + forwarder = await new VaultForwarder__factory(deployer).deploy(target.address, admin.address); + + await deployer.sendTransaction({ + to: forwarderAddress, + value: parseEther('1.0'), + }); + + expect(forwarder.address).eq(forwarderAddress); + }); + + describe('Configuration test', async () => { + it('Should the forward config the target correctly', async () => { + expect(await network.provider.send('eth_getStorageAt', [forwarder.address, FORWARDER_TARGET_SLOT])).eq( + ['0x', '0'.repeat(24), target.address.toLocaleLowerCase().slice(2)].join('') + ); + }); + it('Should the forward config the admin correctly', async () => { + expect(await network.provider.send('eth_getStorageAt', [forwarder.address, FORWARDER_ADMIN_SLOT])).eq( + ['0x', '0'.repeat(24), admin.address.toLocaleLowerCase().slice(2)].join('') + ); + }); + }); + + describe('Access grant', async () => { + before(async () => { + forwarder = VaultForwarder__factory.connect(forwarder.address, admin); + }); + it('Should the admin be able to grant moderator role', async () => { + await forwarder.changeModeratorTo(moderator.address); + expect(await network.provider.send('eth_getStorageAt', [forwarder.address, FORWARDER_MODERATOR_SLOT])).eq( + ['0x', '0'.repeat(24), moderator.address.toLocaleLowerCase().slice(2)].join('') + ); + }); + }); + + describe('Calls from moderator user', async () => { + before(async () => { + forwarder = VaultForwarder__factory.connect(forwarder.address, moderator); + target = MockForwarderTarget__factory.connect(target.address, moderator); + targetBehindForwarder = MockForwarderTarget__factory.connect(forwarder.address, moderator); + }); + + it('Should be able to call non-payable foo function', async () => { + await targetBehindForwarder.foo(2); + expect(await target.data()).eq(2); + }); + + it('Should be able to call payable foo function, with fund from caller account', async () => { + expect(await targetBehindForwarder.fooPayable(3, { value: 100 })).changeEtherBalances( + [moderator.address, target.address], + [-100, 100] + ); + expect(await target.data()).eq(3); + expect(await target.getBalance()).eq(100); + }); + + it('Should be able to call payable foo function, with fund from forwarder account', async () => { + expect( + await forwarder.functionCall(target.interface.encodeFunctionData('fooPayable', [4]), 200) + ).changeEtherBalances([moderator.address, forwarder.address, target.address], [0, -200, 200]); + expect(await target.data()).eq(4); + expect(await target.getBalance()).eq(300); + }); + + it('Should the revert message is thrown from target contract', async () => { + await expect(targetBehindForwarder.fooRevert()).revertedWith('MockForwarderContract: revert intentionally'); + }); + + it('Should the silent revert message is thrown from forwarder contract', async () => { + await expect(targetBehindForwarder.fooSilentRevert()).revertedWith('Forwarder: target reverts silently'); + }); + + it('Should the custom error is thrown from forwarder contract', async () => { + await expect(targetBehindForwarder.fooCustomErrorRevert()).revertedWithCustomError(target, 'ErrIntentionally'); + }); + + it("Should be able to call function of target, that has the same name with admin's function", async () => { + await expect(targetBehindForwarder.withdrawAll()).changeEtherBalances( + [forwarder.address, target.address, moderator.address], + [300, -300, 0] + ); + }); + + it('Fallback: invokes target', async () => { + await expect( + moderator.sendTransaction({ + data: '0xdeadbeef', + to: targetBehindForwarder.address, + value: 200, + }) + ).revertedWith('MockForwardTarget: hello from fallback'); + }); + + it('Receive: invokes forwarder', async () => { + await expect( + moderator.sendTransaction({ + to: targetBehindForwarder.address, + value: 200, + }) + ).changeEtherBalances([moderator.address, forwarder.address, target.address], [-200, 200, 0]); + }); + + it('Should not be able to call the function of admin', async () => { + // overloaded method in target is invoked here + let tx; + await expect(async () => (tx = forwarder.withdrawAll())).changeEtherBalances( + [moderator.address, forwarder.address, target.address], + [0, 0, 0] + ); + + await expect(tx) + .emit(target, 'TargetWithdrawn') + .withArgs(moderator.address, forwarder.address, forwarder.address); + await expect(tx).not.emit(forwarder, 'ForwarderRONWithdrawn'); + }); + }); + + describe('Calls from admin user', async () => { + before(async () => { + forwarder = VaultForwarder__factory.connect(forwarder.address, admin); + target = MockForwarderTarget__factory.connect(target.address, admin); + targetBehindForwarder = MockForwarderTarget__factory.connect(forwarder.address, admin); + }); + + it('Should not be able to call non-payable foo function', async () => { + await expect(targetBehindForwarder.foo(12)).revertedWith('Forwarder: unauthorized call'); + expect(await target.data()).not.eq(12); + }); + + it('Should not be able to call non-payable foo function by force `functionCall`', async () => { + await expect(forwarder.functionCall(target.interface.encodeFunctionData('foo', [12]), 0)).revertedWith( + 'Forwarder: unauthorized call' + ); + }); + + it('Should not be able to call payable foo function, with fund from forwarder account', async () => { + await expect(forwarder.functionCall(target.interface.encodeFunctionData('fooPayable', [13]), 300)).rejectedWith( + 'Forwarder: unauthorized call' + ); + }); + + it('Fallback: invokes forwarder', async () => { + await expect( + admin.sendTransaction({ + data: '0xdeadbeef', + to: targetBehindForwarder.address, + value: 200, + }) + ).revertedWith('Forwarder: unauthorized call'); + }); + + it('Receive: invokes forwarder', async () => { + let tx; + await expect( + async () => + (tx = admin.sendTransaction({ + to: targetBehindForwarder.address, + value: 200, + })) + ).changeEtherBalances([admin.address, forwarder.address, target.address], [-200, 200, 0]); + }); + + it('Should be able to call the admin function of forwarder', async () => { + let forwarderBalance = await ethers.provider.getBalance(forwarder.address); + + // overloaded method in forwarder is invoked here + let tx; + await expect(async () => (tx = forwarder.withdrawAll())).changeEtherBalances( + [admin.address, forwarder.address, target.address], + [forwarderBalance, BigNumber.from(0).sub(forwarderBalance), 0] + ); + + await expect(tx).not.emit(target, 'TargetWithdrawn'); + await expect(tx).emit(forwarder, 'ForwarderRONWithdrawn').withArgs(admin.address, forwarderBalance); + }); + }); + + describe('Calls from unauthorized user', async () => { + before(async () => { + forwarder = VaultForwarder__factory.connect(forwarder.address, unauthorized); + target = MockForwarderTarget__factory.connect(target.address, unauthorized); + targetBehindForwarder = MockForwarderTarget__factory.connect(forwarder.address, unauthorized); + }); + + it('Should not be able to call foo function', async () => { + await expect(targetBehindForwarder.foo(22)).revertedWith('Forwarder: unauthorized call'); + await expect(forwarder.functionCall(target.interface.encodeFunctionData('foo', [22]), 0)).revertedWith( + 'Forwarder: unauthorized call' + ); + }); + + it('Should not be able to call payable foo function, with fund from caller account', async () => { + await expect(targetBehindForwarder.fooPayable(22, { value: 100 })).revertedWith('Forwarder: unauthorized call'); + }); + + it('Should not be able to call payable foo function, with fund from forwarder account', async () => { + await expect(forwarder.functionCall(target.interface.encodeFunctionData('fooPayable', [22]), 100)).revertedWith( + 'Forwarder: unauthorized call' + ); + }); + + it('Fallback: revert', async () => { + await expect( + unauthorized.sendTransaction({ + data: '0xdeadbeef', + to: targetBehindForwarder.address, + value: 200, + }) + ).revertedWith('Forwarder: unauthorized call'); + }); + + it('Receive: accept incoming token', async () => { + await expect( + unauthorized.sendTransaction({ + to: targetBehindForwarder.address, + value: 200, + }) + ).changeEtherBalances([unauthorized.address, forwarder.address, target.address], [-200, 200, 0]); + }); + + it('Should not be able to call the overloaded method', async () => { + await expect(forwarder.withdrawAll()).revertedWith('Forwarder: unauthorized call'); + }); + + it('Should not be able to call the function of admin', async () => { + await expect(forwarder.withdrawAll()).revertedWith('Forwarder: unauthorized call'); + }); + + it('Should not be able to call the exposed methods', async () => { + await expect(forwarder.changeTargetTo(DEFAULT_ADDRESS)).revertedWith('Forwarder: unauthorized call'); + }); + }); +}); diff --git a/test/governance-admin/GovernanceAdmin.test.ts b/test/governance-admin/GovernanceAdmin.test.ts index e6aab6050..1c2679630 100644 --- a/test/governance-admin/GovernanceAdmin.test.ts +++ b/test/governance-admin/GovernanceAdmin.test.ts @@ -1,6 +1,6 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { expect } from 'chai'; -import { BigNumber } from 'ethers'; +import { BigNumber, BigNumberish } from 'ethers'; import { ethers, network } from 'hardhat'; import { GovernanceAdminInterface, mapByteSigToSigStruct } from '../../src/script/governance-admin-interface'; @@ -27,7 +27,7 @@ import { SignatureStruct } from '../../src/types/RoninGovernanceAdmin'; import { randomAddress, ZERO_BYTES32 } from '../../src/utils'; import { createManyTrustedOrganizationAddressSets, TrustedOrganizationAddressSet } from '../helpers/address-set-types'; import { initTest } from '../helpers/fixture'; -import { getLastBlockTimestamp } from '../helpers/utils'; +import { getLastBlockTimestamp, compareAddrs } from '../helpers/utils'; let deployer: SignerWithAddress; let relayer: SignerWithAddress; @@ -48,6 +48,7 @@ let ballot: BOsBallot; let proposalExpiryDuration = 60; let numerator = 7; let denominator = 10; +let snapshotId: string; describe('Governance Admin test', () => { before(async () => { @@ -63,7 +64,7 @@ describe('Governance Admin test', () => { bridgeContract = MockBridge__factory.connect(proxy.address, deployer); const { roninGovernanceAdminAddress, mainchainGovernanceAdminAddress, stakingContractAddress } = await initTest( - 'RoninGovernanceAdmin.test' + 'RoninGovernanceAdminTest' )({ bridgeContract: bridgeContract.address, roninTrustedOrganizationArguments: { @@ -100,127 +101,234 @@ describe('Governance Admin test', () => { }); describe('General case of governance admin', async () => { - it('Should be able to propose to change staking config', async () => { - const newMinValidatorStakingAmount = 555; - const latestTimestamp = await getLastBlockTimestamp(); - proposal = await governanceAdminInterface.createProposal( - latestTimestamp + proposalExpiryDuration, - stakingContract.address, - 0, - governanceAdminInterface.interface.encodeFunctionData('functionDelegateCall', [ - stakingContract.interface.encodeFunctionData('setMinValidatorStakingAmount', [newMinValidatorStakingAmount]), - ]), - 500_000 - ); - signatures = await governanceAdminInterface.generateSignatures(proposal); - supports = signatures.map(() => VoteType.For); - - expect(await governanceAdmin.proposalVoted(proposal.chainId, proposal.nonce, trustedOrgs[0].governor.address)).to - .false; - await governanceAdmin - .connect(trustedOrgs[0].governor) - .proposeProposalStructAndCastVotes(proposal, supports, signatures); - expect(await governanceAdmin.proposalVoted(proposal.chainId, proposal.nonce, trustedOrgs[0].governor.address)).to - .true; - expect(await stakingContract.minValidatorStakingAmount()).eq(newMinValidatorStakingAmount); - }); + describe('Proposals', () => { + it('Should be able to propose to change staking config', async () => { + const newMinValidatorStakingAmount = 555; + const latestTimestamp = await getLastBlockTimestamp(); + proposal = await governanceAdminInterface.createProposal( + latestTimestamp + proposalExpiryDuration, + stakingContract.address, + 0, + governanceAdminInterface.interface.encodeFunctionData('functionDelegateCall', [ + stakingContract.interface.encodeFunctionData('setMinValidatorStakingAmount', [ + newMinValidatorStakingAmount, + ]), + ]), + 500_000 + ); + signatures = await governanceAdminInterface.generateSignatures(proposal); + supports = signatures.map(() => VoteType.For); - it('Should not be able to reuse already voted signatures or proposals', async () => { - await expect( - governanceAdmin + expect(await governanceAdmin.proposalVoted(proposal.chainId, proposal.nonce, trustedOrgs[0].governor.address)) + .to.false; + await governanceAdmin .connect(trustedOrgs[0].governor) - .proposeProposalStructAndCastVotes(proposal, supports, signatures) - ).revertedWith('CoreGovernance: invalid proposal nonce'); - }); - - it('Should be able to relay to mainchain governance admin contract', async () => { - expect(await mainchainGovernanceAdmin.proposalRelayed(proposal.chainId, proposal.nonce)).to.false; - await mainchainGovernanceAdmin.connect(relayer).relayProposal(proposal, supports, signatures); - expect(await mainchainGovernanceAdmin.proposalRelayed(proposal.chainId, proposal.nonce)).to.true; - }); - - it('Should not be able to relay again', async () => { - await expect( - mainchainGovernanceAdmin.connect(relayer).relayProposal(proposal, supports, signatures) - ).revertedWith('CoreGovernance: invalid proposal nonce'); + .proposeProposalStructAndCastVotes(proposal, supports, signatures); + expect(await governanceAdmin.proposalVoted(proposal.chainId, proposal.nonce, trustedOrgs[0].governor.address)) + .to.true; + expect(await stakingContract.minValidatorStakingAmount()).eq(newMinValidatorStakingAmount); + }); + + it('Should not be able to reuse already voted signatures or proposals', async () => { + await expect( + governanceAdmin + .connect(trustedOrgs[0].governor) + .proposeProposalStructAndCastVotes(proposal, supports, signatures) + ).revertedWith('CoreGovernance: invalid proposal nonce'); + }); + + it('Should be able to relay to mainchain governance admin contract', async () => { + expect(await mainchainGovernanceAdmin.proposalRelayed(proposal.chainId, proposal.nonce)).to.false; + await mainchainGovernanceAdmin.connect(relayer).relayProposal(proposal, supports, signatures); + expect(await mainchainGovernanceAdmin.proposalRelayed(proposal.chainId, proposal.nonce)).to.true; + }); + + it('Should not be able to relay again', async () => { + await expect( + mainchainGovernanceAdmin.connect(relayer).relayProposal(proposal, supports, signatures) + ).revertedWith('CoreGovernance: invalid proposal nonce'); + }); }); - it('Should be able to vote bridge operators', async () => { - ballot = { - period: 10, - operators: trustedOrgs.map((v) => v.bridgeVoter.address), - }; - signatures = await Promise.all( - trustedOrgs.map((g) => - g.bridgeVoter - ._signTypedData(governanceAdminInterface.domain, BridgeOperatorsBallotTypes, ballot) - .then(mapByteSigToSigStruct) - ) - ); - expect(await governanceAdmin.bridgeOperatorsVoted(ballot.period, trustedOrgs[0].bridgeVoter.address)).to.false; - await governanceAdmin.voteBridgeOperatorsBySignatures(ballot.period, ballot.operators, signatures); - expect(await governanceAdmin.bridgeOperatorsVoted(ballot.period, trustedOrgs[0].bridgeVoter.address)).to.true; - }); - - it('Should be able relay vote bridge operators', async () => { - expect(await mainchainGovernanceAdmin.bridgeOperatorsRelayed(ballot.period)).to.false; - await mainchainGovernanceAdmin.connect(relayer).relayBridgeOperators(ballot.period, ballot.operators, signatures); - expect(await mainchainGovernanceAdmin.bridgeOperatorsRelayed(ballot.period)).to.true; - expect(await bridgeContract.getBridgeOperators()).eql(trustedOrgs.map((v) => v.bridgeVoter.address)); - }); - - it('Should not able to relay again', async () => { - await expect( - mainchainGovernanceAdmin.connect(relayer).relayBridgeOperators(ballot.period, ballot.operators, signatures) - ).revertedWith('BOsGovernanceRelay: query for outdated period'); - }); - - it('Should not be able to use the signatures for another period', async () => { - ballot = { - period: 100, - operators: trustedOrgs.map((v) => v.bridgeVoter.address), - }; - await expect( - governanceAdmin.voteBridgeOperatorsBySignatures(ballot.period, ballot.operators, signatures) - ).revertedWith('BOsGovernanceProposal: invalid order'); - }); - - it('Should not be able to vote bridge operators with a smaller period', async () => { - ballot = { - period: 5, - operators: trustedOrgs.map((v) => v.bridgeVoter.address), - }; - signatures = await Promise.all( - trustedOrgs.map((g) => - g.bridgeVoter - ._signTypedData(governanceAdminInterface.domain, BridgeOperatorsBallotTypes, ballot) - .then(mapByteSigToSigStruct) - ) - ); - await expect( - governanceAdmin.voteBridgeOperatorsBySignatures(ballot.period, ballot.operators, signatures) - ).revertedWith('BOsGovernanceProposal: query for outdated period'); - }); - - it('Should be able to vote bridge operators with a larger period', async () => { - const duplicatedNumber = 11; - ballot = { - period: 100, - operators: trustedOrgs.map((v, i) => (i < duplicatedNumber ? v.bridgeVoter.address : randomAddress())), - }; - signatures = await Promise.all( - trustedOrgs.map((g) => - g.bridgeVoter - ._signTypedData(governanceAdminInterface.domain, BridgeOperatorsBallotTypes, ballot) - .then(mapByteSigToSigStruct) - ) - ); - await governanceAdmin.voteBridgeOperatorsBySignatures(ballot.period, ballot.operators, signatures); - }); - - it('Should be able relay vote bridge operators', async () => { - await mainchainGovernanceAdmin.connect(relayer).relayBridgeOperators(ballot.period, ballot.operators, signatures); - expect(await bridgeContract.getBridgeOperators()).have.same.members(ballot.operators); + describe('Bridge Operator Set Voting', () => { + before(async () => { + const latestBOset = await governanceAdmin.lastSyncedBridgeOperatorSetInfo(); + expect(latestBOset.period).eq(0); + expect(latestBOset.epoch).eq(0); + expect(latestBOset.operators).eql([]); + }); + + it('Should be able to vote bridge operators', async () => { + ballot = { + period: 10, + epoch: 10_000, + operators: trustedOrgs + .slice(0, 2) + .map((v) => v.bridgeVoter.address) + .sort(compareAddrs), + }; + signatures = await Promise.all( + trustedOrgs.map((g) => + g.bridgeVoter + ._signTypedData(governanceAdminInterface.domain, BridgeOperatorsBallotTypes, ballot) + .then(mapByteSigToSigStruct) + ) + ); + expect( + await governanceAdmin.bridgeOperatorsVoted(ballot.period, ballot.epoch, trustedOrgs[0].bridgeVoter.address) + ).to.false; + await governanceAdmin.voteBridgeOperatorsBySignatures(ballot, signatures); + expect( + await governanceAdmin.bridgeOperatorsVoted(ballot.period, ballot.epoch, trustedOrgs[0].bridgeVoter.address) + ).to.true; + + const latestBOset = await governanceAdmin.lastSyncedBridgeOperatorSetInfo(); + expect(latestBOset.period).eq(ballot.period); + expect(latestBOset.epoch).eq(ballot.epoch); + expect(latestBOset.operators).eql(ballot.operators); + }); + + it('Should be able relay vote bridge operators', async () => { + expect(await mainchainGovernanceAdmin.bridgeOperatorsRelayed(ballot.period, ballot.epoch)).to.false; + await mainchainGovernanceAdmin.connect(relayer).relayBridgeOperators(ballot, signatures); + expect(await mainchainGovernanceAdmin.bridgeOperatorsRelayed(ballot.period, ballot.epoch)).to.true; + const bridgeOperators = await bridgeContract.getBridgeOperators(); + expect([...bridgeOperators].sort(compareAddrs)).eql(ballot.operators); + const latestBOset = await mainchainGovernanceAdmin.lastSyncedBridgeOperatorSetInfo(); + expect(latestBOset.period).eq(ballot.period); + expect(latestBOset.epoch).eq(ballot.epoch); + expect(latestBOset.operators).eql(ballot.operators); + }); + + it('Should not able to relay again', async () => { + await expect(mainchainGovernanceAdmin.connect(relayer).relayBridgeOperators(ballot, signatures)).revertedWith( + 'BOsGovernanceRelay: query for outdated bridge operator set' + ); + }); + + it('Should not be able to relay using invalid period/epoch', async () => { + await expect( + mainchainGovernanceAdmin + .connect(relayer) + .relayBridgeOperators( + { ...ballot, period: BigNumber.from(ballot.period).add(1), operators: [ethers.constants.AddressZero] }, + signatures + ) + ).revertedWith('BOsGovernanceRelay: query for outdated bridge operator set'); + }); + + it('Should not be able to use the signatures for another period', async () => { + const ballot = { + period: 100, + epoch: 10_000, + operators: trustedOrgs.slice(0, 1).map((v) => v.bridgeVoter.address), + }; + await expect(governanceAdmin.voteBridgeOperatorsBySignatures(ballot, signatures)).revertedWith( + 'BOsGovernanceProposal: invalid signer order' + ); + }); + + it('Should not be able to vote for duplicated operators', async () => { + const ballot = { + period: 100, + epoch: 10_000, + operators: [ethers.constants.AddressZero, ethers.constants.AddressZero], + }; + await expect(governanceAdmin.voteBridgeOperatorsBySignatures(ballot, signatures)).revertedWith( + 'BridgeOperatorsBallot: invalid order of bridge operators' + ); + }); + + it('Should not be able to vote for the same operator set again', async () => { + ballot = { + ...ballot, + epoch: BigNumber.from(ballot.epoch).add(1), + }; + await expect(governanceAdmin.voteBridgeOperatorsBySignatures(ballot, signatures)).revertedWith( + 'BridgeOperatorsBallot: bridge operator set is already voted' + ); + }); + + it('Should not be able to vote bridge operators with a smaller epoch/period', async () => { + ballot = { + period: 100, + epoch: 100, + operators: trustedOrgs.map((v) => v.bridgeVoter.address), + }; + await expect(governanceAdmin.voteBridgeOperatorsBySignatures(ballot, signatures)).revertedWith( + 'BOsGovernanceProposal: query for outdated bridge operator set' + ); + }); + + it('Should not be able to vote invalid order of bridge operators', async () => { + const duplicatedNumber = 11; + ballot = { + period: 100, + epoch: 10_001, + operators: [ + ...trustedOrgs.map((v, i) => (i < duplicatedNumber ? v.bridgeVoter.address : randomAddress())), + ethers.constants.AddressZero, + ], + }; + await expect(governanceAdmin.voteBridgeOperatorsBySignatures(ballot, signatures)).revertedWith( + 'BridgeOperatorsBallot: invalid order of bridge operators' + ); + }); + + it('Should be able to vote for a larger number of bridge operators', async () => { + ballot.operators.pop(); + ballot = { + ...ballot, + operators: [ethers.constants.AddressZero, ...ballot.operators.sort(compareAddrs)], + }; + signatures = await Promise.all( + trustedOrgs.map((g) => + g.bridgeVoter + ._signTypedData(governanceAdminInterface.domain, BridgeOperatorsBallotTypes, ballot) + .then(mapByteSigToSigStruct) + ) + ); + const lastLength = (await governanceAdmin.lastSyncedBridgeOperatorSetInfo()).operators.length; + await governanceAdmin.voteBridgeOperatorsBySignatures(ballot, signatures); + const latestBOset = await governanceAdmin.lastSyncedBridgeOperatorSetInfo(); + expect(lastLength).not.eq(ballot.operators.length); + expect(latestBOset.period).eq(ballot.period); + expect(latestBOset.epoch).eq(ballot.epoch); + expect(latestBOset.operators).eql(ballot.operators); + }); + + it('Should be able relay vote bridge operators', async () => { + await mainchainGovernanceAdmin.connect(relayer).relayBridgeOperators(ballot, signatures); + const bridgeOperators = await bridgeContract.getBridgeOperators(); + expect([...bridgeOperators].sort(compareAddrs)).eql(ballot.operators); + const latestBOset = await mainchainGovernanceAdmin.lastSyncedBridgeOperatorSetInfo(); + expect(latestBOset.period).eq(ballot.period); + expect(latestBOset.epoch).eq(ballot.epoch); + expect(latestBOset.operators).eql(ballot.operators); + }); + + it('Should be able to vote for a same number of bridge operators', async () => { + ballot.operators.pop(); + ballot = { + ...ballot, + epoch: BigNumber.from(ballot.epoch).add(1), + operators: [...ballot.operators, randomAddress()].sort(compareAddrs), + }; + signatures = await Promise.all( + trustedOrgs.map((g) => + g.bridgeVoter + ._signTypedData(governanceAdminInterface.domain, BridgeOperatorsBallotTypes, ballot) + .then(mapByteSigToSigStruct) + ) + ); + const lastLength = (await governanceAdmin.lastSyncedBridgeOperatorSetInfo()).operators.length; + await governanceAdmin.voteBridgeOperatorsBySignatures(ballot, signatures); + const latestBOset = await governanceAdmin.lastSyncedBridgeOperatorSetInfo(); + expect(lastLength).eq(ballot.operators.length); + expect(latestBOset.period).eq(ballot.period); + expect(latestBOset.epoch).eq(ballot.epoch); + expect(latestBOset.operators).eql(ballot.operators); + }); }); }); @@ -487,4 +595,202 @@ describe('Governance Admin test', () => { expect(currentProposalVote.status).eq(VoteStatus.Executed); }); }); + + describe('Current Network Proposal Voting', () => { + let newConfig: BigNumberish; + let votedSignatures: SignatureStruct[] = []; + + before(() => { + newConfig = Math.floor(Math.random() * 1000000) + 100000; + }); + + it('Should be able to create a proposal using governor account', async () => { + const latestTimestamp = await getLastBlockTimestamp(); + const expiryTimestamp = latestTimestamp + proposalExpiryDuration; + proposal = await governanceAdminInterface.createProposal( + expiryTimestamp, + stakingContract.address, + 0, + governanceAdminInterface.interface.encodeFunctionData('functionDelegateCall', [ + stakingContract.interface.encodeFunctionData('setMinValidatorStakingAmount', [newConfig]), + ]), + 500_000 + ); + + await governanceAdmin + .connect(trustedOrgs[0].governor) + .proposeProposalForCurrentNetwork( + proposal.expiryTimestamp, + proposal.targets, + proposal.values, + proposal.calldatas, + proposal.gasAmounts, + VoteType.Against + ); + expect(await governanceAdmin.proposalVoted(proposal.chainId, proposal.nonce, trustedOrgs[0].governor.address)).to + .true; + }); + + it('Should not be able to cast vote with invalid chain id', async () => { + await expect( + governanceAdmin + .connect(trustedOrgs[1].governor) + .castProposalVoteForCurrentNetwork( + { ...proposal, chainId: BigNumber.from(proposal.chainId).add(1) }, + VoteType.Against + ) + ).revertedWith('RoninGovernanceAdmin: invalid chain id'); + }); + + it('Should not be able to cast vote with invalid data', async () => { + await expect( + governanceAdmin + .connect(trustedOrgs[1].governor) + .castProposalVoteForCurrentNetwork( + { ...proposal, values: proposal.values.map((v) => BigNumber.from(v).add(1)) }, + VoteType.Against + ) + ).revertedWith('RoninGovernanceAdmin: cast vote for invalid proposal'); + }); + + it('Should be able to cast valid vote', async () => { + await governanceAdmin.connect(trustedOrgs[1].governor).castProposalVoteForCurrentNetwork(proposal, VoteType.For); + expect(await governanceAdmin.proposalVoted(proposal.chainId, proposal.nonce, trustedOrgs[1].governor.address)).to + .true; + }); + + it('Should not be able to cast vote again with signatures', async () => { + const votedSignatures = await governanceAdminInterface.generateSignatures( + proposal, + [trustedOrgs[0].governor], + VoteType.Against + ); + await expect( + governanceAdmin + .connect(trustedOrgs[0].governor) + .castProposalBySignatures(proposal, [VoteType.Against], votedSignatures) + ).revertedWith(`CoreGovernance: ${trustedOrgs[0].governor.address.toLowerCase()} already voted`); + }); + + it('Should be able to cast vote using signatures', async () => { + votedSignatures = await governanceAdminInterface.generateSignatures( + proposal, + [trustedOrgs[2].governor], + VoteType.Against + ); + await governanceAdmin + .connect(trustedOrgs[2].governor) + .castProposalBySignatures(proposal, [VoteType.Against], votedSignatures); + expect(await governanceAdmin.proposalVoted(proposal.chainId, proposal.nonce, trustedOrgs[2].governor.address)).to + .true; + }); + + it('Should not be able to cast vote again without signatures', async () => { + expect(await governanceAdmin.proposalVoted(proposal.chainId, proposal.nonce, trustedOrgs[2].governor.address)).to + .true; + await expect( + governanceAdmin.connect(trustedOrgs[2].governor).castProposalVoteForCurrentNetwork(proposal, VoteType.For) + ).revertedWith(`CoreGovernance: ${trustedOrgs[2].governor.address.toLowerCase()} already voted`); + }); + + it('Should be able to retrieve the signatures', async () => { + const [voters, supports, signatures] = await governanceAdmin.getProposalSignatures( + proposal.chainId, + proposal.nonce + ); + expect(voters).eql([ + trustedOrgs[1].governor.address, + trustedOrgs[0].governor.address, + trustedOrgs[2].governor.address, + ]); + expect(supports).eql([VoteType.For, VoteType.Against, VoteType.Against]); + const emptySignatures = [0, ethers.constants.HashZero, ethers.constants.HashZero]; + expect(signatures).eql([ + emptySignatures, + emptySignatures, + ...votedSignatures.map((sig) => [sig.v, sig.r, sig.s]), + ]); + }); + + describe('Expired Vote', () => { + before(async () => { + snapshotId = await network.provider.send('evm_snapshot'); + await network.provider.send('evm_setNextBlockTimestamp', [ + BigNumber.from(proposal.expiryTimestamp).add(1).toNumber(), + ]); + }); + + after(async () => { + await network.provider.send('evm_revert', [snapshotId]); + }); + + it("Should be able to clear when it' is expired", async () => { + expect( + await governanceAdmin.connect(trustedOrgs[0].governor).deleteExpired(proposal.chainId, proposal.nonce) + ).emit(governanceAdmin, 'ProposalExpired'); + const [voters, supports, signatures] = await governanceAdmin.getProposalSignatures( + proposal.chainId, + proposal.nonce + ); + expect(voters.length).eq(0); + expect(supports.length).eq(0); + expect(signatures.length).eq(0); + }); + }); + + describe('Approved Vote', () => { + before(async () => { + snapshotId = await network.provider.send('evm_snapshot'); + }); + + after(async () => { + await network.provider.send('evm_revert', [snapshotId]); + }); + + it('Should be able to cast for vote', async () => { + for (let i = 3; i < trustedOrgs.length; i++) { + let vote = await governanceAdmin.vote(proposal.chainId, proposal.nonce); + if (vote.status == VoteStatus.Pending) { + await governanceAdmin + .connect(trustedOrgs[i].governor) + .castProposalVoteForCurrentNetwork(proposal, VoteType.For); + } + } + }); + + it('Should the config change after the proposal vote is approved', async () => { + expect(await stakingContract.minValidatorStakingAmount()).eq(newConfig); + }); + }); + + describe('Rejected Vote', () => { + let currentConfig: BigNumberish; + + before(async () => { + snapshotId = await network.provider.send('evm_snapshot'); + currentConfig = await stakingContract.minValidatorStakingAmount(); + }); + + after(async () => { + await network.provider.send('evm_revert', [snapshotId]); + }); + + it('Should be able to cast for vote', async () => { + for (let i = 3; i < trustedOrgs.length; i++) { + let vote = await governanceAdmin.vote(proposal.chainId, proposal.nonce); + if (vote.status == VoteStatus.Pending) { + await governanceAdmin + .connect(trustedOrgs[i].governor) + .castProposalVoteForCurrentNetwork(proposal, VoteType.Against); + } + } + }); + + it('Should the config change after the proposal vote is approved', async () => { + const latestConfig = await stakingContract.minValidatorStakingAmount(); + expect(latestConfig).not.eq(newConfig); + expect(latestConfig).eq(currentConfig); + }); + }); + }); }); diff --git a/test/helpers/fixture.ts b/test/helpers/fixture.ts index 18b383556..956a82cb8 100644 --- a/test/helpers/fixture.ts +++ b/test/helpers/fixture.ts @@ -6,7 +6,7 @@ import { EpochController } from './ronin-validator-set'; import { generalMainchainConf, generalRoninConf, - governanceAdminConf, + roninGovernanceAdminConf, mainchainGovernanceAdminConf, maintenanceConf, roninTrustedOrganizationConf, @@ -16,7 +16,7 @@ import { stakingVestingConfig, } from '../../src/config'; import { - GovernanceAdminArguments, + RoninGovernanceAdminArguments, MainchainGovernanceAdminArguments, MaintenanceArguments, Network, @@ -50,7 +50,7 @@ export interface InitTestInput { roninValidatorSetArguments?: RoninValidatorSetArguments; roninTrustedOrganizationArguments?: RoninTrustedOrganizationArguments; mainchainGovernanceAdminArguments?: MainchainGovernanceAdminArguments; - governanceAdminArguments?: GovernanceAdminArguments; + governanceAdminArguments?: RoninGovernanceAdminArguments; } export const defaultTestConfig: InitTestInput = { @@ -112,6 +112,8 @@ export const defaultTestConfig: InitTestInput = { numberOfBlocksInEpoch: 600, maxValidatorCandidate: 10, minEffectiveDaysOnwards: 7, + emergencyExitLockedAmount: 500, + emergencyExpiryDuration: 14 * 86400, // 14 days }, roninTrustedOrganizationArguments: { @@ -189,7 +191,7 @@ export const initTest = (id: string) => ...defaultTestConfig?.mainchainGovernanceAdminArguments, ...options?.mainchainGovernanceAdminArguments, }; - governanceAdminConf[network.name] = { + roninGovernanceAdminConf[network.name] = { ...defaultTestConfig?.governanceAdminArguments, ...options?.governanceAdminArguments, }; diff --git a/test/helpers/ronin-validator-set.ts b/test/helpers/ronin-validator-set.ts index 44f1d9528..e923deb49 100644 --- a/test/helpers/ronin-validator-set.ts +++ b/test/helpers/ronin-validator-set.ts @@ -123,13 +123,24 @@ export const expects = { ); }, - emitStakingRewardDistributedEvent: async function (tx: ContractTransaction, expectingAmount: BigNumberish) { + emitStakingRewardDistributedEvent: async function ( + tx: ContractTransaction, + expectingTotalAmount: BigNumberish, + expectingValidators: string[] | undefined, + expectingAmounts: BigNumberish[] | undefined + ) { await expectEvent( contractInterface, 'StakingRewardDistributed', tx, (event) => { - expect(event.args[0], 'invalid distributing reward').eq(expectingAmount); + expect(event.args[0], 'invalid total distributing reward').eq(expectingTotalAmount); + if (expectingValidators) { + expect(event.args[1], 'invalid validator list').eql(expectingValidators); + } + if (expectingAmounts) { + expect(event.args[2], 'invalid amount list').eql(expectingAmounts); + } }, 1 ); @@ -154,16 +165,37 @@ export const expects = { emitBlockProducerSetUpdatedEvent: async function ( tx: ContractTransaction, - expectingPeriod: BigNumberish, - expectingBlockProducers: string[] + expectingPeriod?: BigNumberish, + expectingEpoch?: BigNumberish, + expectingBlockProducers?: string[] ) { await expectEvent( contractInterface, 'BlockProducerSetUpdated', tx, (event) => { - expect(event.args[0], 'invalid period').eq(expectingPeriod); - expect(event.args[1], 'invalid validator set').eql(expectingBlockProducers); + !!expectingPeriod && expect(event.args[0], 'invalid period').eq(expectingPeriod); + !!expectingEpoch && expect(event.args[1], 'invalid epoch').eq(expectingEpoch); + !!expectingBlockProducers && expect(event.args[2], 'invalid block producers').eql(expectingBlockProducers); + }, + 1 + ); + }, + + emitBridgeOperatorSetUpdatedEvent: async function ( + tx: ContractTransaction, + expectingPeriod?: BigNumberish, + expectingEpoch?: BigNumberish, + expectingBridgeOperators?: string[] + ) { + await expectEvent( + contractInterface, + 'BridgeOperatorSetUpdated', + tx, + (event) => { + !!expectingPeriod && expect(event.args[0], 'invalid period').eq(expectingPeriod); + !!expectingEpoch && expect(event.args[1], 'invalid epoch').eq(expectingEpoch); + !!expectingBridgeOperators && expect(event.args[2], 'invalid bridge operators').eql(expectingBridgeOperators); }, 1 ); diff --git a/test/helpers/utils.ts b/test/helpers/utils.ts index 2debc7b25..0452c672f 100644 --- a/test/helpers/utils.ts +++ b/test/helpers/utils.ts @@ -2,6 +2,7 @@ import { expect } from 'chai'; import { ContractTransaction } from 'ethers'; import { Interface, LogDescription } from 'ethers/lib/utils'; import { ethers, network } from 'hardhat'; +import { Address } from 'hardhat-deploy/dist/types'; export const expectEvent = async ( contractInterface: Interface, @@ -26,11 +27,14 @@ export const expectEvent = async ( expect(counter, 'invalid number of emitted events').eq(eventNumbers); }; +export const mineDummyBlock = () => network.provider.send('hardhat_mine', []); + export const mineBatchTxs = async (fn: () => Promise) => { await network.provider.send('evm_setAutomine', [false]); await fn(); await network.provider.send('evm_mine'); await network.provider.send('evm_setAutomine', [true]); + await mineDummyBlock(); }; export const getLastBlockTimestamp = async (): Promise => { @@ -38,3 +42,12 @@ export const getLastBlockTimestamp = async (): Promise => { let blockBefore = await ethers.provider.getBlock(blockNumBefore); return blockBefore.timestamp; }; + + +export const calculateAddress = (from: Address, nonce: number) => ({ + nonce, + address: ethers.utils.getContractAddress({ from, nonce }), +}); + +export const compareAddrs = (firstStr: string, secondStr: string) => + firstStr.toLowerCase().localeCompare(secondStr.toLowerCase()); diff --git a/test/integration/ActionSlashValidators.test.ts b/test/integration/ActionSlashValidators.test.ts index 67266c299..11048cdd6 100644 --- a/test/integration/ActionSlashValidators.test.ts +++ b/test/integration/ActionSlashValidators.test.ts @@ -99,6 +99,7 @@ describe('[Integration] Slash validators', () => { const mockValidatorLogic = await new MockRoninValidatorSetExtended__factory(deployer).deploy(); await mockValidatorLogic.deployed(); await governanceAdminInterface.upgrade(validatorContract.address, mockValidatorLogic.address); + await validatorContract.initEpoch(); await EpochController.setTimestampToPeriodEnding(); await network.provider.send('hardhat_setCoinbase', [coinbase.address]); @@ -234,6 +235,7 @@ describe('[Integration] Slash validators', () => { await RoninValidatorSetExpects.emitBlockProducerSetUpdatedEvent( wrapUpEpochTx!, period, + await validatorContract.epochOf((await ethers.provider.getBlockNumber()) + 1), expectingBlockProducerSet ); expect(wrapUpEpochTx).not.emit(validatorContract, 'ValidatorSetUpdated'); @@ -269,6 +271,7 @@ describe('[Integration] Slash validators', () => { await RoninValidatorSetExpects.emitBlockProducerSetUpdatedEvent( wrapUpEpochTx!, period, + await validatorContract.epochOf((await ethers.provider.getBlockNumber()) + 1), expectingBlockProducerSet ); expect(wrapUpEpochTx).not.emit(validatorContract, 'ValidatorSetUpdated'); @@ -389,6 +392,7 @@ describe('[Integration] Slash validators', () => { await RoninValidatorSetExpects.emitBlockProducerSetUpdatedEvent( wrapUpEpochTx!, period, + await validatorContract.epochOf((await ethers.provider.getBlockNumber()) + 1), expectingBlockProducerSet ); }); @@ -424,6 +428,7 @@ describe('[Integration] Slash validators', () => { await RoninValidatorSetExpects.emitBlockProducerSetUpdatedEvent( wrapUpEpochTx!, period, + await validatorContract.epochOf((await ethers.provider.getBlockNumber()) + 1), expectingBlockProducerSet ); expect(wrapUpEpochTx).not.emit(validatorContract, 'ValidatorSetUpdated'); @@ -509,7 +514,9 @@ describe('[Integration] Slash validators', () => { const topUpTx = stakingContract.connect(slashee.poolAdmin).stake(slashee.consensusAddr.address, { value: slashAmountForUnavailabilityTier2Threshold, }); - await expect(topUpTx).revertedWith('BaseStaking: query for non-existent pool'); + await expect(topUpTx) + .revertedWithCustomError(stakingContract, 'ErrInactivePool') + .withArgs(slashee.consensusAddr.address); } }); diff --git a/test/integration/ActionSubmitReward.test.ts b/test/integration/ActionSubmitReward.test.ts index 92149501e..fcc3b8a58 100644 --- a/test/integration/ActionSubmitReward.test.ts +++ b/test/integration/ActionSubmitReward.test.ts @@ -95,6 +95,7 @@ describe('[Integration] Submit Block Reward', () => { const mockValidatorLogic = await new MockRoninValidatorSetExtended__factory(deployer).deploy(); await mockValidatorLogic.deployed(); await governanceAdminInterface.upgrade(validatorContract.address, mockValidatorLogic.address); + await validatorContract.initEpoch(); }); describe('Configuration check', async () => { diff --git a/test/integration/ActionWrapUpEpoch.test.ts b/test/integration/ActionWrapUpEpoch.test.ts index 50292d239..c8c2b6417 100644 --- a/test/integration/ActionWrapUpEpoch.test.ts +++ b/test/integration/ActionWrapUpEpoch.test.ts @@ -94,6 +94,7 @@ describe('[Integration] Wrap up epoch', () => { const mockValidatorLogic = await new MockRoninValidatorSetExtended__factory(deployer).deploy(); await mockValidatorLogic.deployed(); await governanceAdminInterface.upgrade(validatorContract.address, mockValidatorLogic.address); + await validatorContract.initEpoch(); }); after(async () => { @@ -172,7 +173,7 @@ describe('[Integration] Wrap up epoch', () => { await validatorContract.endEpoch(); await validatorContract.wrapUpEpoch(); let duplicatedWrapUpTx = validatorContract.wrapUpEpoch(); - await expect(duplicatedWrapUpTx).to.be.revertedWith('RoninValidatorSet: query for already wrapped up epoch'); + await expect(duplicatedWrapUpTx).to.be.revertedWithCustomError(validatorContract, 'ErrAlreadyWrappedEpoch'); }); }); @@ -290,7 +291,7 @@ describe('[Integration] Wrap up epoch', () => { it('Should the block producer set get updated (excluding the slashed validator)', async () => { const lastPeriod = await validatorContract.currentPeriod(); - const epoch = (await validatorContract.epochOf(await ethers.provider.getBlockNumber())).add(1); + const epoch = await validatorContract.epochOf(await ethers.provider.getBlockNumber()); await mineBatchTxs(async () => { await validatorContract.endEpoch(); wrapUpTx = await validatorContract.wrapUpEpoch(); @@ -299,7 +300,12 @@ describe('[Integration] Wrap up epoch', () => { let expectingBlockProducerSet = [validators[2], validators[3]].map((_) => _.consensusAddr.address).reverse(); await expect(wrapUpTx!).emit(validatorContract, 'WrappedUpEpoch').withArgs(lastPeriod, epoch, false); - await ValidatorSetExpects.emitBlockProducerSetUpdatedEvent(wrapUpTx!, lastPeriod, expectingBlockProducerSet); + await ValidatorSetExpects.emitBlockProducerSetUpdatedEvent( + wrapUpTx!, + lastPeriod, + await validatorContract.epochOf((await ethers.provider.getBlockNumber()) + 1), + expectingBlockProducerSet + ); expect(await validatorContract.getValidators()).eql( [validators[1], validators[2], validators[3]].map((_) => _.consensusAddr.address).reverse() diff --git a/test/maintainance/Maintenance.test.ts b/test/maintainance/Maintenance.test.ts index 6b68e2d8d..d2f8f65ae 100644 --- a/test/maintainance/Maintenance.test.ts +++ b/test/maintainance/Maintenance.test.ts @@ -307,6 +307,7 @@ describe('Maintenance test', () => { await ValidatorSetExpects.emitBlockProducerSetUpdatedEvent( tx!, await validatorContract.currentPeriod(), + await validatorContract.epochOf((await ethers.provider.getBlockNumber()) + 1), expectingBlockProducerSet ); expect(await validatorContract.getBlockProducers()).eql( @@ -328,6 +329,7 @@ describe('Maintenance test', () => { await ValidatorSetExpects.emitBlockProducerSetUpdatedEvent( tx!, await validatorContract.currentPeriod(), + await validatorContract.epochOf((await ethers.provider.getBlockNumber()) + 1), expectingBlockProducerSet ); expect(await validatorContract.getBlockProducers()).eql(expectingBlockProducerSet); diff --git a/test/precompile-usages/PrecompileUsageSortValidators.test.ts b/test/precompile-usages/PrecompileUsageSortValidators.test.ts index cd61960d4..bb3337b7d 100644 --- a/test/precompile-usages/PrecompileUsageSortValidators.test.ts +++ b/test/precompile-usages/PrecompileUsageSortValidators.test.ts @@ -5,22 +5,22 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { MockPrecompile, MockPrecompile__factory, - MockPrecompileUsageSortValidators, - MockPrecompileUsageSortValidators__factory, + MockPCUSortValidators, + MockPCUSortValidators__factory, } from '../../src/types'; import { randomInt } from 'crypto'; let deployer: SignerWithAddress; let validatorCandidates: SignerWithAddress[]; let precompileSorting: MockPrecompile; -let usageSorting: MockPrecompileUsageSortValidators; +let usageSorting: MockPCUSortValidators; describe('[Precompile] Sorting validators test', () => { before(async () => { [deployer, ...validatorCandidates] = await ethers.getSigners(); precompileSorting = await new MockPrecompile__factory(deployer).deploy(); - usageSorting = await new MockPrecompileUsageSortValidators__factory(deployer).deploy(precompileSorting.address); + usageSorting = await new MockPCUSortValidators__factory(deployer).deploy(precompileSorting.address); }); it('Should the usage contract correctly configs the precompile address', async () => { @@ -50,8 +50,9 @@ describe('[Precompile] Sorting validators test', () => { it('Should the usage contract revert with proper message on calling the precompile contract fails', async () => { await usageSorting.setPrecompileSortValidatorAddress(ethers.constants.AddressZero); - await expect(usageSorting.callPrecompile([validatorCandidates[0].address], [1])).revertedWith( - 'PrecompileUsageSortValidators: call to precompile fails' + await expect(usageSorting.callPrecompile([validatorCandidates[0].address], [1])).revertedWithCustomError( + usageSorting, + 'ErrCallPrecompiled' ); }); }); diff --git a/test/precompile-usages/PrecompileUsageValidateDoubleSign.test.ts b/test/precompile-usages/PrecompileUsageValidateDoubleSign.test.ts index 2b96ed026..22f1d3699 100644 --- a/test/precompile-usages/PrecompileUsageValidateDoubleSign.test.ts +++ b/test/precompile-usages/PrecompileUsageValidateDoubleSign.test.ts @@ -5,23 +5,21 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { MockPrecompile, MockPrecompile__factory, - MockPrecompileUsageValidateDoubleSign, - MockPrecompileUsageValidateDoubleSign__factory, + MockPCUValidateDoubleSign, + MockPCUValidateDoubleSign__factory, } from '../../src/types'; let deployer: SignerWithAddress; let signers: SignerWithAddress[]; let precompileValidating: MockPrecompile; -let usageValidating: MockPrecompileUsageValidateDoubleSign; +let usageValidating: MockPCUValidateDoubleSign; describe('[Precompile] Validate double sign test', () => { before(async () => { [deployer, ...signers] = await ethers.getSigners(); precompileValidating = await new MockPrecompile__factory(deployer).deploy(); - usageValidating = await new MockPrecompileUsageValidateDoubleSign__factory(deployer).deploy( - precompileValidating.address - ); + usageValidating = await new MockPCUValidateDoubleSign__factory(deployer).deploy(precompileValidating.address); }); let header1 = ethers.utils.toUtf8Bytes('sampleHeader1'); @@ -38,8 +36,9 @@ describe('[Precompile] Validate double sign test', () => { it('Should the usage contract revert with proper message on calling the precompile contract fails', async () => { await usageValidating.setPrecompileValidateDoubleSignAddress(ethers.constants.AddressZero); - await expect(usageValidating.callPrecompile(header1, header2)).revertedWith( - 'PrecompileUsageValidateDoubleSign: call to precompile fails' + await expect(usageValidating.callPrecompile(header1, header2)).revertedWithCustomError( + usageValidating, + 'ErrCallPrecompiled' ); }); }); diff --git a/test/staking/RewardCalculation.test.ts b/test/staking/RewardCalculation.test.ts index 55aafce7f..a5266dc40 100644 --- a/test/staking/RewardCalculation.test.ts +++ b/test/staking/RewardCalculation.test.ts @@ -66,16 +66,16 @@ describe('Reward Calculation test', () => { describe('Period: x+1 -> x+2', () => { it('Should not be able to record reward with invalid arguments', async () => { - tx = await stakingContract.recordRewards([poolAddr], [100, 100]); + tx = await stakingContract.execRecordRewards([poolAddr], [100, 100]); await expect(tx).emit(stakingContract, 'PoolsUpdateFailed').withArgs(period, [poolAddr], [100, 100]); }); it('Should not be able to record reward more than once for a pool', async () => { aRps = aRps.add(MASK.mul(1000 / 100)); - tx = await stakingContract.recordRewards([poolAddr], [1000]); + tx = await stakingContract.execRecordRewards([poolAddr], [1000]); await expect(tx).emit(stakingContract, 'PoolsUpdated').withArgs(period, [poolAddr], [aRps], [100]); - tx = await stakingContract.recordRewards([poolAddr], [1000]); + tx = await stakingContract.execRecordRewards([poolAddr], [1000]); await expect(tx).emit(stakingContract, 'PoolsUpdateConflicted').withArgs(period, [poolAddr]); await expect(tx).not.emit(stakingContract, 'PoolsUpdated'); await stakingContract.increasePeriod(); // period = 2 @@ -86,7 +86,7 @@ describe('Reward Calculation test', () => { it('Should not able to reward reward more than once for multiple pools', async () => { let addrList = Array.from(Array(10).keys()).map(randomAddress); let arr = addrList.map(() => 0); - tx = await stakingContract.recordRewards( + tx = await stakingContract.execRecordRewards( addrList, addrList.map(() => 1000) ); @@ -95,7 +95,7 @@ describe('Reward Calculation test', () => { const conflictNumber = 7; arr = arr.slice(conflictNumber); addrList = addrList.map((_, i) => (i >= conflictNumber ? randomAddress() : _)); - tx = await stakingContract.recordRewards( + tx = await stakingContract.execRecordRewards( addrList, addrList.map(() => 1000) ); diff --git a/test/staking/Staking.test.ts b/test/staking/Staking.test.ts index d6cd9a7f1..b637df324 100644 --- a/test/staking/Staking.test.ts +++ b/test/staking/Staking.test.ts @@ -75,7 +75,7 @@ describe('Staking test', () => { it('Should not be able to propose validator with insufficient amount', async () => { await expect( stakingContract.applyValidatorCandidate(userA.address, userA.address, userA.address, userA.address, 1) - ).revertedWith('CandidateStaking: insufficient amount'); + ).revertedWithCustomError(stakingContract, 'ErrInsufficientStakingAmount'); }); it('Should not be able to propose validator with duplicated address', async () => { @@ -91,7 +91,7 @@ describe('Staking test', () => { 1, /* 0.01% */ { value: minValidatorStakingAmount.mul(2) } ); - await expect(tx).revertedWith('CandidateStaking: three operation addresses must be distinct'); + await expect(tx).revertedWithCustomError(stakingContract, 'ErrThreeOperationAddrsNotDistinct'); }); it('Should be able to propose validator with sufficient amount', async () => { @@ -132,22 +132,25 @@ describe('Staking test', () => { value: minValidatorStakingAmount, } ) - ).revertedWith('CandidateManager: query for already existent candidate'); + ).revertedWithCustomError(validatorContract, 'ErrExistentCandidate'); }); it('Should not be able to stake with empty value', async () => { - await expect(stakingContract.stake(poolAddrSet.consensusAddr.address, { value: 0 })).revertedWith( - 'BaseStaking: query with empty value' + await expect(stakingContract.stake(poolAddrSet.consensusAddr.address, { value: 0 })).revertedWithCustomError( + stakingContract, + 'ErrZeroValue' ); }); it('Should not be able to call stake/unstake when the method is not the pool admin', async () => { - await expect(stakingContract.stake(poolAddrSet.consensusAddr.address, { value: 1 })).revertedWith( - 'BaseStaking: requester must be the pool admin' + await expect(stakingContract.stake(poolAddrSet.consensusAddr.address, { value: 1 })).revertedWithCustomError( + stakingContract, + 'ErrOnlyPoolAdminAllowed' ); - await expect(stakingContract.unstake(poolAddrSet.consensusAddr.address, 1)).revertedWith( - 'BaseStaking: requester must be the pool admin' + await expect(stakingContract.unstake(poolAddrSet.consensusAddr.address, 1)).revertedWithCustomError( + stakingContract, + 'ErrOnlyPoolAdminAllowed' ); }); @@ -164,7 +167,7 @@ describe('Staking test', () => { it('Should not be able to unstake due to cooldown restriction', async () => { await expect( stakingContract.connect(poolAddrSet.poolAdmin).unstake(poolAddrSet.consensusAddr.address, 1) - ).revertedWith('CandidateStaking: unstake too early'); + ).revertedWithCustomError(stakingContract, 'ErrUnstakeTooEarly'); }); it('Should not be able to unstake after cooldown', async () => { @@ -192,13 +195,13 @@ describe('Staking test', () => { stakingContract .connect(poolAddrSet.poolAdmin) .unstake(poolAddrSet.consensusAddr.address, minValidatorStakingAmount.add(1)) - ).revertedWith('CandidateStaking: invalid staking amount left'); + ).revertedWithCustomError(stakingContract, 'ErrStakingAmountLeft'); }); it('Should not be able to request renounce using unauthorized account', async () => { - await expect(stakingContract.connect(deployer).requestRenounce(poolAddrSet.consensusAddr.address)).revertedWith( - 'BaseStaking: requester must be the pool admin' - ); + await expect( + stakingContract.connect(deployer).requestRenounce(poolAddrSet.consensusAddr.address) + ).revertedWithCustomError(stakingContract, 'ErrOnlyPoolAdminAllowed'); }); it('Should the non-pool-admin not be able to update the commission rate', async () => { @@ -208,7 +211,7 @@ describe('Staking test', () => { minEffectiveDaysOnwards, 20_00 // 20% ) - ).revertedWith('BaseStaking: requester must be the pool admin'); + ).revertedWithCustomError(stakingContract, 'ErrOnlyPoolAdminAllowed'); }); it('Should the pool admin not be able to request updating the commission rate with invalid effective date', async () => { @@ -218,7 +221,7 @@ describe('Staking test', () => { minEffectiveDaysOnwards - 1, 20_00 // 20% ) - ).revertedWith('CandidateManager: invalid effective date'); + ).revertedWithCustomError(validatorContract, 'ErrInvalidEffectiveDaysOnwards'); }); it('Should the pool admin be able to request updating the commission rate', async () => { @@ -261,22 +264,21 @@ describe('Staking test', () => { it('Should not be able to request renounce again', async () => { await expect( stakingContract.connect(poolAddrSet.poolAdmin).requestRenounce(poolAddrSet.consensusAddr.address) - ).revertedWith('CandidateManager: already requested before'); + ).revertedWithCustomError(validatorContract, 'ErrAlreadyRequestedRevokingCandidate'); }); it('Should the consensus account is no longer be a candidate, and the staked amount is transferred back to the pool admin', async () => { await network.provider.send('evm_increaseTime', [waitingSecsToRevoke]); const stakingAmount = minValidatorStakingAmount.mul(2); - expect(await stakingContract.getStakingPool(poolAddrSet.consensusAddr.address)).eql([ + expect(await stakingContract.getPoolDetail(poolAddrSet.consensusAddr.address)).eql([ poolAddrSet.poolAdmin.address, stakingAmount, stakingAmount.add(9), ]); await expect(() => validatorContract.wrapUpEpoch()).changeEtherBalance(poolAddrSet.poolAdmin, stakingAmount); - await expect(stakingContract.getStakingPool(poolAddrSet.consensusAddr.address)).revertedWith( - 'BaseStaking: query for non-existent pool' - ); + let _poolDetail = await stakingContract.getPoolDetail(poolAddrSet.consensusAddr.address); + expect(_poolDetail._stakingAmount).eq(0); }); it('Should the exited pool admin and consensus address rejoin as a candidate', async () => { @@ -315,7 +317,9 @@ describe('Staking test', () => { 1, /* 0.01% */ { value: minValidatorStakingAmount.mul(2) } ) - ).revertedWith('CandidateStaking: pool admin is active'); + ) + .revertedWithCustomError(stakingContract, 'ErrAdminOfAnyActivePoolForbidden') + .withArgs(poolAddrSet.poolAdmin.address); await stakingContract.connect(poolAddrSet.poolAdmin).requestRenounce(poolAddrSet.consensusAddr.address); await network.provider.send('evm_increaseTime', [waitingSecsToRevoke]); @@ -335,14 +339,15 @@ describe('Staking test', () => { }); it('Should not be able to delegate to a deprecated pool', async () => { - await expect(stakingContract.delegate(poolAddrSet.consensusAddr.address, { value: 1 })).revertedWith( - 'BaseStaking: query for non-existent pool' - ); + await expect(stakingContract.delegate(poolAddrSet.consensusAddr.address, { value: 1 })) + .revertedWithCustomError(stakingContract, 'ErrInactivePool') + .withArgs(poolAddrSet.consensusAddr.address); }); it('Should not be able to delegate with empty value', async () => { - await expect(stakingContract.delegate(otherPoolAddrSet.consensusAddr.address)).revertedWith( - 'BaseStaking: query with empty value' + await expect(stakingContract.delegate(otherPoolAddrSet.consensusAddr.address)).revertedWithCustomError( + stakingContract, + 'ErrZeroValue' ); }); @@ -351,10 +356,13 @@ describe('Staking test', () => { stakingContract .connect(otherPoolAddrSet.poolAdmin) .delegate(otherPoolAddrSet.consensusAddr.address, { value: 1 }) - ).revertedWith('DelegatorStaking: admin of an active pool cannot delegate'); + ) + .revertedWithCustomError(stakingContract, 'ErrAdminOfAnyActivePoolForbidden') + .withArgs(otherPoolAddrSet.poolAdmin.address); + await expect( stakingContract.connect(otherPoolAddrSet.poolAdmin).undelegate(otherPoolAddrSet.consensusAddr.address, 1) - ).revertedWith('BaseStaking: delegator must not be the pool admin'); + ).revertedWithCustomError(stakingContract, 'ErrPoolAdminForbidden'); }); it('Should not be able to delegate when the method caller is the admin of any arbitrary pool', async () => { @@ -362,12 +370,17 @@ describe('Staking test', () => { stakingContract .connect(anotherActivePoolSet.poolAdmin) .delegate(otherPoolAddrSet.consensusAddr.address, { value: 1 }) - ).revertedWith('DelegatorStaking: admin of an active pool cannot delegate'); + ) + .revertedWithCustomError(stakingContract, 'ErrAdminOfAnyActivePoolForbidden') + .withArgs(anotherActivePoolSet.poolAdmin.address); + await expect( stakingContract .connect(otherPoolAddrSet.poolAdmin) .delegate(anotherActivePoolSet.consensusAddr.address, { value: 1 }) - ).revertedWith('DelegatorStaking: admin of an active pool cannot delegate'); + ) + .revertedWithCustomError(stakingContract, 'ErrAdminOfAnyActivePoolForbidden') + .withArgs(otherPoolAddrSet.poolAdmin.address); }); it('Should multiple accounts be able to delegate to one pool', async () => { @@ -388,9 +401,9 @@ describe('Staking test', () => { }); it('Should not be able to undelegate due to cooldown restriction', async () => { - await expect(stakingContract.connect(userA).undelegate(otherPoolAddrSet.consensusAddr.address, 1)).revertedWith( - 'DelegatorStaking: undelegate too early' - ); + await expect( + stakingContract.connect(userA).undelegate(otherPoolAddrSet.consensusAddr.address, 1) + ).revertedWithCustomError(stakingContract, 'ErrUndelegateTooEarly'); }); it('Should be able to undelegate after cooldown', async () => { @@ -405,14 +418,16 @@ describe('Staking test', () => { }); it('Should not be able to undelegate with empty amount', async () => { - await expect(stakingContract.undelegate(otherPoolAddrSet.consensusAddr.address, 0)).revertedWith( - 'DelegatorStaking: invalid amount' + await expect(stakingContract.undelegate(otherPoolAddrSet.consensusAddr.address, 0)).revertedWithCustomError( + stakingContract, + 'ErrUndelegateZeroAmount' ); }); it('Should not be able to undelegate more than the delegating amount', async () => { - await expect(stakingContract.undelegate(otherPoolAddrSet.consensusAddr.address, 1000)).revertedWith( - 'DelegatorStaking: insufficient amount to undelegate' + await expect(stakingContract.undelegate(otherPoolAddrSet.consensusAddr.address, 1000)).revertedWithCustomError( + stakingContract, + 'ErrInsufficientDelegatingAmount' ); }); @@ -427,7 +442,7 @@ describe('Staking test', () => { 2, /* 0.02% */ { value: minValidatorStakingAmount } ); - expect(await stakingContract.getStakingPool(poolAddrSet.consensusAddr.address)).eql([ + expect(await stakingContract.getPoolDetail(poolAddrSet.consensusAddr.address)).eql([ poolAddrSet.poolAdmin.address, minValidatorStakingAmount, minValidatorStakingAmount.add(8), diff --git a/test/validator/ArrangeValidators.test.ts b/test/validator/ArrangeValidators.test.ts index 16c912714..dd1bc4e9c 100644 --- a/test/validator/ArrangeValidators.test.ts +++ b/test/validator/ArrangeValidators.test.ts @@ -133,6 +133,7 @@ describe('Arrange validators', () => { const mockValidatorLogic = await new MockRoninValidatorSetExtended__factory(deployer).deploy(); await mockValidatorLogic.deployed(); await governanceAdminInterface.upgrade(validatorContract.address, mockValidatorLogic.address); + await validatorContract.initEpoch(); const mockSlashIndicator = await new MockSlashIndicatorExtended__factory(deployer).deploy(); await mockSlashIndicator.deployed(); diff --git a/test/validator/EmergencyExit.test.ts b/test/validator/EmergencyExit.test.ts new file mode 100644 index 000000000..ac2b2dbaa --- /dev/null +++ b/test/validator/EmergencyExit.test.ts @@ -0,0 +1,375 @@ +import { expect } from 'chai'; +import { BigNumber, BigNumberish, ContractTransaction, ethers as EthersType } from 'ethers'; +import { ethers, network } from 'hardhat'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; + +import { + Staking, + MockRoninValidatorSetExtended, + MockRoninValidatorSetExtended__factory, + Staking__factory, + MockSlashIndicatorExtended__factory, + MockSlashIndicatorExtended, + RoninGovernanceAdmin__factory, + RoninGovernanceAdmin, + StakingVesting__factory, + StakingVesting, +} from '../../src/types'; +import * as RoninValidatorSet from '../helpers/ronin-validator-set'; +import { mineBatchTxs } from '../helpers/utils'; +import { initTest } from '../helpers/fixture'; +import { GovernanceAdminInterface } from '../../src/script/governance-admin-interface'; +import { Address } from 'hardhat-deploy/dist/types'; +import { + createManyTrustedOrganizationAddressSets, + createManyValidatorCandidateAddressSets, + TrustedOrganizationAddressSet, + ValidatorCandidateAddressSet, +} from '../helpers/address-set-types'; +import { getEmergencyExitBallotHash } from '../../src/script/proposal'; + +let roninValidatorSet: MockRoninValidatorSetExtended; +let stakingVesting: StakingVesting; +let stakingContract: Staking; +let slashIndicator: MockSlashIndicatorExtended; +let governanceAdmin: RoninGovernanceAdmin; +let governanceAdminInterface: GovernanceAdminInterface; + +let poolAdmin: SignerWithAddress; +let coinbase: SignerWithAddress; +let bridgeOperator: SignerWithAddress; +let deployer: SignerWithAddress; +let signers: SignerWithAddress[]; +let trustedOrgs: TrustedOrganizationAddressSet[]; +let validatorCandidates: ValidatorCandidateAddressSet[]; +let compromisedValidator: ValidatorCandidateAddressSet; + +const localValidatorCandidatesLength = 5; +const slashAmountForUnavailabilityTier2Threshold = 100; +const maxValidatorNumber = 5; +const maxValidatorCandidate = 100; +const minValidatorStakingAmount = BigNumber.from(20000); +const slashDoubleSignAmount = BigNumber.from(2000); +const emergencyExitLockedAmount = BigNumber.from(500); +const waitingSecsToRevoke = 7 * 86400; // 7 days +const emergencyExpiryDuration = 14 * 86400; // 14 days + +let consensusAddr: Address; +let recipientAfterUnlockedFund: Address; +let requestedAt: BigNumberish; +let expiredAt: BigNumberish; +let voteHash: string; +let snapshotId: string; +let totalStakedAmount: BigNumber; + +describe('Emergency Exit test', () => { + let tx: ContractTransaction; + let requestBlock: EthersType.providers.Block; + + before(async () => { + [poolAdmin, coinbase, bridgeOperator, deployer, ...signers] = await ethers.getSigners(); + + trustedOrgs = createManyTrustedOrganizationAddressSets(signers.splice(0, 6)); + validatorCandidates = createManyValidatorCandidateAddressSets(signers.slice(0, localValidatorCandidatesLength * 3)); + + await network.provider.send('hardhat_setCoinbase', [coinbase.address]); + + const { + slashContractAddress, + validatorContractAddress, + stakingContractAddress, + roninGovernanceAdminAddress, + stakingVestingContractAddress, + } = await initTest('EmergencyExit')({ + slashIndicatorArguments: { + doubleSignSlashing: { + slashDoubleSignAmount, + }, + unavailabilitySlashing: { + slashAmountForUnavailabilityTier2Threshold, + }, + }, + stakingArguments: { + minValidatorStakingAmount, + waitingSecsToRevoke, + }, + roninValidatorSetArguments: { + maxValidatorNumber, + maxValidatorCandidate, + emergencyExitLockedAmount, + emergencyExpiryDuration, + }, + roninTrustedOrganizationArguments: { + trustedOrganizations: trustedOrgs.map((v) => ({ + consensusAddr: v.consensusAddr.address, + governor: v.governor.address, + bridgeVoter: v.bridgeVoter.address, + weight: 100, + addedBlock: 0, + })), + }, + }); + + roninValidatorSet = MockRoninValidatorSetExtended__factory.connect(validatorContractAddress, deployer); + stakingVesting = StakingVesting__factory.connect(stakingVestingContractAddress, deployer); + slashIndicator = MockSlashIndicatorExtended__factory.connect(slashContractAddress, deployer); + stakingContract = Staking__factory.connect(stakingContractAddress, deployer); + governanceAdmin = RoninGovernanceAdmin__factory.connect(roninGovernanceAdminAddress, deployer); + governanceAdminInterface = new GovernanceAdminInterface( + governanceAdmin, + undefined, + ...trustedOrgs.map((_) => _.governor) + ); + + const mockValidatorLogic = await new MockRoninValidatorSetExtended__factory(deployer).deploy(); + await mockValidatorLogic.deployed(); + await governanceAdminInterface.upgrade(roninValidatorSet.address, mockValidatorLogic.address); + await roninValidatorSet.initEpoch(); + + const mockSlashIndicator = await new MockSlashIndicatorExtended__factory(deployer).deploy(); + await mockSlashIndicator.deployed(); + await governanceAdminInterface.upgrade(slashIndicator.address, mockSlashIndicator.address); + + const stakedAmount = validatorCandidates.map((_, i) => + minValidatorStakingAmount.mul(2).add(validatorCandidates.length - i) + ); + for (let i = 0; i < validatorCandidates.length; i++) { + await stakingContract + .connect(validatorCandidates[i].poolAdmin) + .applyValidatorCandidate( + validatorCandidates[i].candidateAdmin.address, + validatorCandidates[i].consensusAddr.address, + validatorCandidates[i].treasuryAddr.address, + validatorCandidates[i].bridgeOperator.address, + 2_00, + { + value: stakedAmount[i], + } + ); + } + + await RoninValidatorSet.EpochController.setTimestampToPeriodEnding(); + await mineBatchTxs(async () => { + await roninValidatorSet.endEpoch(); + await roninValidatorSet.connect(coinbase).wrapUpEpoch(); + }); + compromisedValidator = validatorCandidates[validatorCandidates.length - 1]; + totalStakedAmount = stakedAmount[validatorCandidates.length - 1]; + }); + + after(async () => { + await network.provider.send('hardhat_setCoinbase', [ethers.constants.AddressZero]); + }); + + it('Should be able to get list of the validator candidates', async () => { + expect(validatorCandidates.map((v) => v.consensusAddr.address)).eql(await roninValidatorSet.getBlockProducers()); + }); + + it('Should not be able to request emergency exit using unauthorized accounts', async () => { + await expect( + stakingContract.requestEmergencyExit(compromisedValidator.consensusAddr.address) + ).revertedWithCustomError(stakingContract, 'ErrOnlyPoolAdminAllowed'); + }); + + it('Should be able to request emergency exit', async () => { + tx = await stakingContract + .connect(compromisedValidator.poolAdmin) + .requestEmergencyExit(compromisedValidator.consensusAddr.address); + }); + + it('Should not be able to request emergency exit again', async () => { + await expect( + stakingContract + .connect(compromisedValidator.poolAdmin) + .requestEmergencyExit(compromisedValidator.consensusAddr.address) + ).revertedWithCustomError(roninValidatorSet, 'ErrAlreadyRequestedEmergencyExit'); + }); + + it('Should the request tx emit event CandidateRevokingTimestampUpdated', async () => { + requestBlock = await ethers.provider.getBlock(tx.blockNumber!); + await expect(tx) + .emit(roninValidatorSet, 'CandidateRevokingTimestampUpdated') + .withArgs(compromisedValidator.consensusAddr.address, requestBlock.timestamp + waitingSecsToRevoke); + }); + + it('Should the request tx emit event EmergencyExitRequested', async () => { + await expect(tx) + .emit(roninValidatorSet, 'EmergencyExitRequested') + .withArgs(compromisedValidator.consensusAddr.address, emergencyExitLockedAmount); + }); + + it('Should the request tx emit event EmergencyExitPollCreated', async () => { + consensusAddr = compromisedValidator.consensusAddr.address; + recipientAfterUnlockedFund = compromisedValidator.treasuryAddr.address; + requestedAt = requestBlock.timestamp; + expiredAt = requestBlock.timestamp + emergencyExpiryDuration; + voteHash = getEmergencyExitBallotHash(consensusAddr, recipientAfterUnlockedFund, requestedAt, expiredAt); + + await expect(tx) + .emit(governanceAdmin, 'EmergencyExitPollCreated') + .withArgs(voteHash, consensusAddr, recipientAfterUnlockedFund, requestedAt, expiredAt); + }); + + it("Should the emergency exit's requester be still in the validator list", async () => { + expect(validatorCandidates.map((v) => v.consensusAddr.address)).eql(await roninValidatorSet.getBlockProducers()); + expect(await roninValidatorSet.isValidator(compromisedValidator.consensusAddr.address)).to.true; + expect(await roninValidatorSet.isBlockProducer(compromisedValidator.consensusAddr.address)).to.true; + expect(await roninValidatorSet.isOperatingBridge(compromisedValidator.consensusAddr.address)).to.true; + expect(await roninValidatorSet.isBridgeOperator(compromisedValidator.bridgeOperator.address)).to.true; + }); + + it("Should the exit's requester be removed in block producer and bridge operator list in next epoch", async () => { + await mineBatchTxs(async () => { + await roninValidatorSet.endEpoch(); + tx = await roninValidatorSet.connect(coinbase).wrapUpEpoch(); + }); + + expect(await roninValidatorSet.isValidatorCandidate(compromisedValidator.consensusAddr.address)).to.true; + expect(await roninValidatorSet.isValidator(compromisedValidator.consensusAddr.address)).to.false; + expect(await roninValidatorSet.isBlockProducer(compromisedValidator.consensusAddr.address)).to.false; + expect(await roninValidatorSet.isOperatingBridge(compromisedValidator.consensusAddr.address)).to.false; + expect(await roninValidatorSet.isBridgeOperator(compromisedValidator.bridgeOperator.address)).to.false; + await RoninValidatorSet.expects.emitBlockProducerSetUpdatedEvent( + tx, + undefined, + undefined, + validatorCandidates + .map((v) => v.consensusAddr.address) + .filter((v) => v != compromisedValidator.consensusAddr.address) + ); + await RoninValidatorSet.expects.emitBridgeOperatorSetUpdatedEvent( + tx, + undefined, + undefined, + validatorCandidates + .map((v) => v.bridgeOperator.address) + .filter((v) => v != compromisedValidator.bridgeOperator.address) + ); + }); + + describe('Valid emergency exit', () => { + let balance: BigNumberish; + + before(async () => { + snapshotId = await network.provider.send('evm_snapshot'); + balance = await ethers.provider.getBalance(compromisedValidator.treasuryAddr.address); + }); + + after(async () => { + await network.provider.send('evm_revert', [snapshotId]); + }); + + it('Should the governor vote for an emergency exit', async () => { + tx = await governanceAdmin + .connect(trustedOrgs[0].governor) + .voteEmergencyExit(voteHash, consensusAddr, recipientAfterUnlockedFund, requestedAt, expiredAt); + }); + + it('Should the vote tx emit event EmergencyExitPollApproved', async () => { + await expect(tx).emit(governanceAdmin, 'EmergencyExitPollApproved').withArgs(voteHash); + }); + + it('Should the vote tx emit event EmergencyExitLockedFundReleased', async () => { + await expect(tx) + .emit(roninValidatorSet, 'EmergencyExitLockedFundReleased') + .withArgs( + compromisedValidator.consensusAddr.address, + compromisedValidator.treasuryAddr.address, + emergencyExitLockedAmount + ); + }); + + it('Should the requester receive the unlocked fund', async () => { + const currentBalance = await ethers.provider.getBalance(compromisedValidator.treasuryAddr.address); + expect(currentBalance.sub(balance)).eq(emergencyExitLockedAmount); + }); + + it('Should the governor still able to vote', async () => { + tx = await governanceAdmin + .connect(trustedOrgs[1].governor) + .voteEmergencyExit(voteHash, consensusAddr, recipientAfterUnlockedFund, requestedAt, expiredAt); + await expect(tx).not.emit(roninValidatorSet, 'EmergencyExitLockedFundReleased'); + }); + + it('Should the requester not receive the unlocked fund again', async () => { + const currentBalance = await ethers.provider.getBalance(compromisedValidator.treasuryAddr.address); + expect(currentBalance.sub(balance)).eq(emergencyExitLockedAmount); + }); + + it('Should the requester receive the total staked amount at the next period ending', async () => { + await RoninValidatorSet.EpochController.setTimestampToPeriodEnding(); + await mineBatchTxs(async () => { + await roninValidatorSet.endEpoch(); + tx = await roninValidatorSet.connect(coinbase).wrapUpEpoch(); + }); + + const currentBalance = await ethers.provider.getBalance(compromisedValidator.treasuryAddr.address); + expect(currentBalance.sub(balance)).eq(totalStakedAmount); + expect(await roninValidatorSet.isValidatorCandidate(compromisedValidator.consensusAddr.address)).to.false; + }); + + it('Should the requester not receive again in the next period ending', async () => { + await RoninValidatorSet.EpochController.setTimestampToPeriodEnding(); + await mineBatchTxs(async () => { + await roninValidatorSet.endEpoch(); + tx = await roninValidatorSet.connect(coinbase).wrapUpEpoch(); + }); + + const currentBalance = await ethers.provider.getBalance(compromisedValidator.treasuryAddr.address); + expect(currentBalance.sub(balance)).eq(totalStakedAmount); + }); + }); + + describe('Expired emergency exit', () => { + let treasuryBalance: BigNumberish; + let stakingVestingBalance: BigNumberish; + + before(async () => { + snapshotId = await network.provider.send('evm_snapshot'); + treasuryBalance = await ethers.provider.getBalance(compromisedValidator.treasuryAddr.address); + stakingVestingBalance = await ethers.provider.getBalance(stakingVesting.address); + await network.provider.send('evm_increaseTime', [emergencyExpiryDuration * 2]); + }); + + after(async () => { + await network.provider.send('evm_revert', [snapshotId]); + }); + + it('Should the governor not be able to vote for an expiry emergency exit', async () => { + tx = await governanceAdmin + .connect(trustedOrgs[0].governor) + .voteEmergencyExit(voteHash, consensusAddr, recipientAfterUnlockedFund, requestedAt, expiredAt); + await expect(tx).emit(governanceAdmin, 'EmergencyExitPollExpired').withArgs(voteHash); + await expect( + governanceAdmin + .connect(trustedOrgs[1].governor) + .voteEmergencyExit(voteHash, consensusAddr, recipientAfterUnlockedFund, requestedAt, expiredAt) + ).revertedWith('RoninGovernanceAdmin: query for expired vote'); + }); + + it('Should be able to recycle the locked fund and transfer back the amount left', async () => { + await RoninValidatorSet.EpochController.setTimestampToPeriodEnding(); + await mineBatchTxs(async () => { + await roninValidatorSet.endEpoch(); + tx = await roninValidatorSet.connect(coinbase).wrapUpEpoch(); + }); + const currentTreasuryBalance = await ethers.provider.getBalance(compromisedValidator.treasuryAddr.address); + const currentStakingVestingBalance = await ethers.provider.getBalance(stakingVesting.address); + expect(currentTreasuryBalance.sub(treasuryBalance)).eq(totalStakedAmount.sub(emergencyExitLockedAmount)); + expect(currentStakingVestingBalance.sub(stakingVestingBalance)).eq(emergencyExitLockedAmount); + expect(await roninValidatorSet.isValidatorCandidate(compromisedValidator.consensusAddr.address)).to.false; + }); + + it('Should not be able to receive fund again', async () => { + await RoninValidatorSet.EpochController.setTimestampToPeriodEnding(); + await mineBatchTxs(async () => { + await roninValidatorSet.endEpoch(); + tx = await roninValidatorSet.connect(coinbase).wrapUpEpoch(); + }); + const currentTreasuryBalance = await ethers.provider.getBalance(compromisedValidator.treasuryAddr.address); + const currentStakingVestingBalance = await ethers.provider.getBalance(stakingVesting.address); + expect(currentTreasuryBalance.sub(treasuryBalance)).eq(totalStakedAmount.sub(emergencyExitLockedAmount)); + expect(currentStakingVestingBalance.sub(stakingVestingBalance)).eq(emergencyExitLockedAmount); + }); + }); +}); diff --git a/test/validator/RoninValidatorSet.test.ts b/test/validator/RoninValidatorSet.test.ts index aeb8bc3f3..44ef37768 100644 --- a/test/validator/RoninValidatorSet.test.ts +++ b/test/validator/RoninValidatorSet.test.ts @@ -126,6 +126,7 @@ describe('Ronin Validator Set test', () => { const mockValidatorLogic = await new MockRoninValidatorSetExtended__factory(deployer).deploy(); await mockValidatorLogic.deployed(); await governanceAdminInterface.upgrade(roninValidatorSet.address, mockValidatorLogic.address); + await roninValidatorSet.initEpoch(); const mockSlashIndicator = await new MockSlashIndicatorExtended__factory(deployer).deploy(); await mockSlashIndicator.deployed(); @@ -138,14 +139,16 @@ describe('Ronin Validator Set test', () => { describe('Wrapping up epoch sanity check', async () => { it('Should not be able to wrap up epoch using unauthorized account', async () => { - await expect(roninValidatorSet.connect(deployer).wrapUpEpoch()).revertedWith( - 'RoninValidatorSet: method caller must be coinbase' + await expect(roninValidatorSet.connect(deployer).wrapUpEpoch()).revertedWithCustomError( + roninValidatorSet, + 'ErrCallerMustBeCoinbase' ); }); it('Should not be able to wrap up epoch when the epoch is not ending', async () => { - await expect(roninValidatorSet.connect(consensusAddr).wrapUpEpoch()).revertedWith( - 'RoninValidatorSet: only allowed at the end of epoch' + await expect(roninValidatorSet.connect(consensusAddr).wrapUpEpoch()).revertedWithCustomError( + roninValidatorSet, + 'ErrAtEndOfEpochOnly' ); }); @@ -159,7 +162,7 @@ describe('Ronin Validator Set test', () => { }); expect(tx!).emit(roninValidatorSet, 'WrappedUpEpoch').withArgs(lastPeriod, epoch, true); lastPeriod = await roninValidatorSet.currentPeriod(); - await RoninValidatorSet.expects.emitBlockProducerSetUpdatedEvent(tx!, lastPeriod, []); + await RoninValidatorSet.expects.emitBlockProducerSetUpdatedEvent(tx!, lastPeriod, epoch, []); expect(await roninValidatorSet.getValidators()).eql([]); }); }); @@ -182,7 +185,7 @@ describe('Ronin Validator Set test', () => { } let tx: ContractTransaction; - epoch = (await roninValidatorSet.epochOf(await ethers.provider.getBlockNumber())).add(1); + epoch = await roninValidatorSet.epochOf(await ethers.provider.getBlockNumber()); await mineBatchTxs(async () => { await roninValidatorSet.endEpoch(); tx = await roninValidatorSet.connect(consensusAddr).wrapUpEpoch(); @@ -209,7 +212,9 @@ describe('Ronin Validator Set test', () => { } ); - await expect(tx).revertedWith('CandidateStaking: pool admin is active'); + await expect(tx) + .revertedWithCustomError(stakingContract, 'ErrAdminOfAnyActivePoolForbidden') + .withArgs(validatorCandidates[3].poolAdmin.address); }); it('Should not be able to apply for candidate role with existed candidate admin address', async () => { @@ -226,7 +231,7 @@ describe('Ronin Validator Set test', () => { } ); - await expect(tx).revertedWith('CandidateStaking: three interaction addresses must be of the same'); + await expect(tx).revertedWithCustomError(stakingContract, 'ErrThreeInteractionAddrsNotEqual'); }); it('Should not be able to apply for candidate role with existed treasury address', async () => { @@ -243,7 +248,7 @@ describe('Ronin Validator Set test', () => { } ); - await expect(tx).revertedWith('CandidateStaking: three interaction addresses must be of the same'); + await expect(tx).revertedWithCustomError(stakingContract, 'ErrThreeInteractionAddrsNotEqual'); }); it('Should not be able to apply for candidate role with existed bridge operator address', async () => { @@ -260,9 +265,9 @@ describe('Ronin Validator Set test', () => { } ); - await expect(tx).revertedWith( - `CandidateManager: bridge operator address ${validatorCandidates[0].bridgeOperator.address.toLocaleLowerCase()} is already exist` - ); + await expect(tx) + .revertedWithCustomError(roninValidatorSet, 'ErrExistentBridgeOperator') + .withArgs(validatorCandidates[0].bridgeOperator.address); }); }); @@ -271,7 +276,7 @@ describe('Ronin Validator Set test', () => { it('Should be able to wrap up epoch at end of period and sync validator set from staking contract', async () => { let tx: ContractTransaction; await RoninValidatorSet.EpochController.setTimestampToPeriodEnding(); - epoch = (await roninValidatorSet.epochOf(await ethers.provider.getBlockNumber())).add(1); + epoch = await roninValidatorSet.epochOf(await ethers.provider.getBlockNumber()); lastPeriod = await roninValidatorSet.currentPeriod(); await mineBatchTxs(async () => { await roninValidatorSet.endEpoch(); @@ -331,7 +336,7 @@ describe('Ronin Validator Set test', () => { let tx: ContractTransaction; await RoninValidatorSet.EpochController.setTimestampToPeriodEnding(); - epoch = (await roninValidatorSet.epochOf(await ethers.provider.getBlockNumber())).add(1); + epoch = await roninValidatorSet.epochOf(await ethers.provider.getBlockNumber()); lastPeriod = await roninValidatorSet.currentPeriod(); await mineBatchTxs(async () => { await roninValidatorSet.endEpoch(); @@ -355,8 +360,9 @@ describe('Ronin Validator Set test', () => { describe('Recording and distributing rewards', async () => { describe('Sanity check', async () => { it('Should not be able to submit block reward using unauthorized account', async () => { - await expect(roninValidatorSet.submitBlockReward()).revertedWith( - 'RoninValidatorSet: method caller must be coinbase' + await expect(roninValidatorSet.submitBlockReward()).revertedWithCustomError( + roninValidatorSet, + 'ErrCallerMustBeCoinbase' ); }); }); @@ -365,7 +371,7 @@ describe('Ronin Validator Set test', () => { it('Should be able to submit block reward using coinbase account and not receive bonuses', async () => { const tx = await roninValidatorSet.connect(consensusAddr).submitBlockReward({ value: 100 }); - epoch = (await roninValidatorSet.epochOf(await ethers.provider.getBlockNumber())).add(1); + epoch = await roninValidatorSet.epochOf(await ethers.provider.getBlockNumber()); lastPeriod = await roninValidatorSet.currentPeriod(); await RoninValidatorSet.expects.emitBlockRewardSubmittedEvent(tx, consensusAddr.address, 100, 0); @@ -415,7 +421,7 @@ describe('Ronin Validator Set test', () => { await RoninValidatorSet.EpochController.setTimestampToPeriodEnding(); lastPeriod = await roninValidatorSet.currentPeriod(); - epoch = (await roninValidatorSet.epochOf(await ethers.provider.getBlockNumber())).add(1); + epoch = await roninValidatorSet.epochOf(await ethers.provider.getBlockNumber()); await mineBatchTxs(async () => { await roninValidatorSet.endEpoch(); tx = await roninValidatorSet.connect(consensusAddr).wrapUpEpoch(); @@ -424,7 +430,12 @@ describe('Ronin Validator Set test', () => { await expect(tx!).emit(roninValidatorSet, 'WrappedUpEpoch').withArgs(lastPeriod, epoch, true); lastPeriod = await roninValidatorSet.currentPeriod(); await RoninValidatorSet.expects.emitValidatorSetUpdatedEvent(tx!, lastPeriod, currentValidatorSet); - await RoninValidatorSet.expects.emitStakingRewardDistributedEvent(tx!, 5148); // (5000 + 100 + 100) * 99% + await RoninValidatorSet.expects.emitStakingRewardDistributedEvent( + tx!, + 5148, + currentValidatorSet, + [5148, 0, 0, 0].map((_) => BigNumber.from(_)) + ); // (5000 + 100 + 100) * 99% await RoninValidatorSet.expects.emitMiningRewardDistributedEvent( tx!, consensusAddr.address, @@ -458,7 +469,7 @@ describe('Ronin Validator Set test', () => { expect(await roninValidatorSet.totalDeprecatedReward()).equal(5100); // = 0 + (5000 + 100) - epoch = (await roninValidatorSet.epochOf(await ethers.provider.getBlockNumber())).add(1); + epoch = await roninValidatorSet.epochOf(await ethers.provider.getBlockNumber()); lastPeriod = await roninValidatorSet.currentPeriod(); await mineBatchTxs(async () => { await roninValidatorSet.endEpoch(); @@ -478,7 +489,7 @@ describe('Ronin Validator Set test', () => { await roninValidatorSet.connect(consensusAddr).submitBlockReward({ value: 100 }); await RoninValidatorSet.EpochController.setTimestampToPeriodEnding(); - epoch = (await roninValidatorSet.epochOf(await ethers.provider.getBlockNumber())).add(1); + epoch = await roninValidatorSet.epochOf(await ethers.provider.getBlockNumber()); lastPeriod = await roninValidatorSet.currentPeriod(); await mineBatchTxs(async () => { await roninValidatorSet.endEpoch(); @@ -505,7 +516,7 @@ describe('Ronin Validator Set test', () => { await roninValidatorSet.connect(consensusAddr).submitBlockReward({ value: 100 }); await RoninValidatorSet.EpochController.setTimestampToPeriodEnding(); - epoch = (await roninValidatorSet.epochOf(await ethers.provider.getBlockNumber())).add(1); + epoch = await roninValidatorSet.epochOf(await ethers.provider.getBlockNumber()); lastPeriod = await roninValidatorSet.currentPeriod(); await mineBatchTxs(async () => { await roninValidatorSet.endEpoch(); @@ -517,7 +528,9 @@ describe('Ronin Validator Set test', () => { await RoninValidatorSet.expects.emitValidatorSetUpdatedEvent(tx!, lastPeriod, currentValidatorSet); await RoninValidatorSet.expects.emitStakingRewardDistributedEvent( tx!, - blockProducerBonusPerBlock.add(100).div(100).mul(99) + blockProducerBonusPerBlock.add(100).div(100).mul(99), + currentValidatorSet, + [blockProducerBonusPerBlock.add(100).div(100).mul(99), 0, 0, 0].map((_) => BigNumber.from(_)) ); const balanceDiff = (await treasury.getBalance()).sub(balance); @@ -542,10 +555,10 @@ describe('Ronin Validator Set test', () => { await expect(tx) .to.emit(roninValidatorSet, 'BlockRewardDeprecated') .withArgs(consensusAddr.address, 100, BlockRewardDeprecatedType.UNAVAILABILITY); - await expect(await roninValidatorSet.totalDeprecatedReward()).equal(100); + expect(await roninValidatorSet.totalDeprecatedReward()).equal(100); await RoninValidatorSet.EpochController.setTimestampToPeriodEnding(); - epoch = (await roninValidatorSet.epochOf(await ethers.provider.getBlockNumber())).add(1); + epoch = await roninValidatorSet.epochOf(await ethers.provider.getBlockNumber()); lastPeriod = await roninValidatorSet.currentPeriod(); await mineBatchTxs(async () => { await roninValidatorSet.endEpoch(); @@ -603,7 +616,7 @@ describe('Ronin Validator Set test', () => { currentValidatorSet.splice(-1, 1, validatorCandidates[1].consensusAddr.address); await RoninValidatorSet.EpochController.setTimestampToPeriodEnding(); - epoch = (await roninValidatorSet.epochOf(await ethers.provider.getBlockNumber())).add(1); + epoch = await roninValidatorSet.epochOf(await ethers.provider.getBlockNumber()); lastPeriod = await roninValidatorSet.currentPeriod(); await mineBatchTxs(async () => { await roninValidatorSet.endEpoch();