diff --git a/contracts/extensions/GatewayV2.sol b/contracts/extensions/GatewayV2.sol new file mode 100644 index 000000000..0dfc8625a --- /dev/null +++ b/contracts/extensions/GatewayV2.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/security/Pausable.sol"; +import "../interfaces/IQuorum.sol"; +import "./collections/HasProxyAdmin.sol"; + +abstract contract GatewayV2 is HasProxyAdmin, Pausable, IQuorum { + uint256 internal _num; + uint256 internal _denom; + + address private ______deprecated; + uint256 public nonce; + + /** + * @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; + + /** + * @dev See {IQuorum-getThreshold}. + */ + function getThreshold() external view virtual returns (uint256, uint256) { + return (_num, _denom); + } + + /** + * @dev See {IQuorum-checkThreshold}. + */ + function checkThreshold(uint256 _voteWeight) external view virtual returns (bool) { + return _voteWeight * _denom >= _num * _getTotalWeight(); + } + + /** + * @dev See {IQuorum-setThreshold}. + */ + function setThreshold(uint256 _numerator, uint256 _denominator) + external + virtual + onlyAdmin + returns (uint256, uint256) + { + return _setThreshold(_numerator, _denominator); + } + + /** + * @dev Triggers paused state. + */ + function pause() external onlyAdmin { + _pause(); + } + + /** + * @dev Triggers unpaused state. + */ + function unpause() external onlyAdmin { + _unpause(); + } + + /** + * @dev See {IQuorum-minimumVoteWeight}. + */ + function minimumVoteWeight() public view virtual returns (uint256) { + return _minimumVoteWeight(_getTotalWeight()); + } + + /** + * @dev Sets threshold and returns the old one. + * + * Emits the `ThresholdUpdated` event. + * + */ + function _setThreshold(uint256 _numerator, uint256 _denominator) + internal + virtual + returns (uint256 _previousNum, uint256 _previousDenom) + { + require(_numerator <= _denominator, "GatewayV2: invalid threshold"); + _previousNum = _num; + _previousDenom = _denom; + _num = _numerator; + _denom = _denominator; + emit ThresholdUpdated(nonce++, _numerator, _denominator, _previousNum, _previousDenom); + } + + /** + * @dev Returns minimum vote weight. + */ + function _minimumVoteWeight(uint256 _totalWeight) internal view virtual returns (uint256) { + return (_num * _totalWeight + _denom - 1) / _denom; + } + + /** + * @dev Returns the total weight. + */ + function _getTotalWeight() internal view virtual returns (uint256); +} diff --git a/contracts/extensions/MinimumWithdrawal.sol b/contracts/extensions/MinimumWithdrawal.sol new file mode 100644 index 000000000..e06eddf5a --- /dev/null +++ b/contracts/extensions/MinimumWithdrawal.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./collections/HasProxyAdmin.sol"; +import "../libraries/Transfer.sol"; + +abstract contract MinimumWithdrawal is HasProxyAdmin { + /// @dev Emitted when the minimum thresholds are updated + event MinimumThresholdsUpdated(address[] tokens, uint256[] threshold); + + /// @dev Mapping from token address => minimum thresholds + mapping(address => uint256) public minimumThreshold; + + /** + * @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; + + /** + * @dev Sets the minimum thresholds to withdraw. + * + * Requirements: + * - The method caller is admin. + * - The arrays have the same length and its length larger than 0. + * + * Emits the `MinimumThresholdsUpdated` event. + * + */ + function setMinimumThresholds(address[] calldata _tokens, uint256[] calldata _thresholds) external virtual onlyAdmin { + require(_tokens.length > 0, "MinimumWithdrawal: invalid array length"); + _setMinimumThresholds(_tokens, _thresholds); + } + + /** + * @dev Sets minimum thresholds. + * + * Requirements: + * - The array lengths are equal. + * + * Emits the `MinimumThresholdsUpdated` event. + * + */ + function _setMinimumThresholds(address[] calldata _tokens, uint256[] calldata _thresholds) internal virtual { + require(_tokens.length == _thresholds.length, "MinimumWithdrawal: invalid array length"); + for (uint256 _i; _i < _tokens.length; _i++) { + minimumThreshold[_tokens[_i]] = _thresholds[_i]; + } + emit MinimumThresholdsUpdated(_tokens, _thresholds); + } + + /** + * @dev Checks whether the request is larger than or equal to the minimum threshold. + */ + function _checkWithdrawal(Transfer.Request calldata _request) internal view { + require( + _request.info.erc != Token.Standard.ERC20 || _request.info.quantity >= minimumThreshold[_request.tokenAddr], + "MinimumWithdrawal: query for too small quantity" + ); + } +} diff --git a/contracts/extensions/WithdrawalLimitation.sol b/contracts/extensions/WithdrawalLimitation.sol new file mode 100644 index 000000000..59b1471e6 --- /dev/null +++ b/contracts/extensions/WithdrawalLimitation.sol @@ -0,0 +1,324 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./GatewayV2.sol"; + +abstract contract WithdrawalLimitation is GatewayV2 { + /// @dev Emitted when the high-tier vote weight threshold is updated + event HighTierVoteWeightThresholdUpdated( + uint256 indexed nonce, + uint256 indexed numerator, + uint256 indexed denominator, + uint256 previousNumerator, + uint256 previousDenominator + ); + /// @dev Emitted when the thresholds for high-tier withdrawals that requires high-tier vote weights are updated + event HighTierThresholdsUpdated(address[] tokens, uint256[] thresholds); + /// @dev Emitted when the thresholds for locked withdrawals are updated + event LockedThresholdsUpdated(address[] tokens, uint256[] thresholds); + /// @dev Emitted when the fee percentages to unlock withdraw are updated + event UnlockFeePercentagesUpdated(address[] tokens, uint256[] percentages); + /// @dev Emitted when the daily limit thresholds are updated + event DailyWithdrawalLimitsUpdated(address[] tokens, uint256[] limits); + + uint256 public constant _MAX_PERCENTAGE = 1_000_000; + + uint256 internal _highTierVWNum; + uint256 internal _highTierVWDenom; + + /// @dev Mapping from mainchain token => the amount thresholds for high-tier withdrawals that requires high-tier vote weights + mapping(address => uint256) public highTierThreshold; + /// @dev Mapping from mainchain token => the amount thresholds to lock withdrawal + mapping(address => uint256) public lockedThreshold; + /// @dev Mapping from mainchain token => unlock fee percentages for unlocker + /// @notice Values 0-1,000,000 map to 0%-100% + mapping(address => uint256) public unlockFeePercentages; + /// @dev Mapping from mainchain token => daily limit amount for withdrawal + mapping(address => uint256) public dailyWithdrawalLimit; + /// @dev Mapping from token address => today withdrawal amount + mapping(address => uint256) public lastSyncedWithdrawal; + /// @dev Mapping from token address => last date synced to record the `lastSyncedWithdrawal` + mapping(address => uint256) public lastDateSynced; + + /** + * @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; + + /** + * @dev Override {GatewayV2-setThreshold}. + * + * Requirements: + * - The high-tier vote weight threshold must equal to or larger than the normal threshold. + * + */ + function setThreshold(uint256 _numerator, uint256 _denominator) + external + virtual + override + onlyAdmin + returns (uint256 _previousNum, uint256 _previousDenom) + { + (_previousNum, _previousDenom) = _setThreshold(_numerator, _denominator); + _verifyThresholds(); + } + + /** + * @dev Returns the high-tier vote weight threshold. + */ + function getHighTierVoteWeightThreshold() external view virtual returns (uint256, uint256) { + return (_highTierVWNum, _highTierVWDenom); + } + + /** + * @dev Checks whether the `_voteWeight` passes the high-tier vote weight threshold. + */ + function checkHighTierVoteWeightThreshold(uint256 _voteWeight) external view virtual returns (bool) { + return _voteWeight * _highTierVWDenom >= _highTierVWNum * _getTotalWeight(); + } + + /** + * @dev Sets high-tier vote weight threshold and returns the old one. + * + * Requirements: + * - The method caller is admin. + * - The high-tier vote weight threshold must equal to or larger than the normal threshold. + * + * Emits the `HighTierVoteWeightThresholdUpdated` event. + * + */ + function setHighTierVoteWeightThreshold(uint256 _numerator, uint256 _denominator) + external + virtual + onlyAdmin + returns (uint256 _previousNum, uint256 _previousDenom) + { + (_previousNum, _previousDenom) = _setHighTierVoteWeightThreshold(_numerator, _denominator); + _verifyThresholds(); + } + + /** + * @dev Sets the thresholds for high-tier withdrawals that requires high-tier vote weights. + * + * Requirements: + * - The method caller is admin. + * - The arrays have the same length and its length larger than 0. + * + * Emits the `HighTierThresholdsUpdated` event. + * + */ + function setHighTierThresholds(address[] calldata _tokens, uint256[] calldata _thresholds) + external + virtual + onlyAdmin + { + require(_tokens.length > 0, "WithdrawalLimitation: invalid array length"); + _setHighTierThresholds(_tokens, _thresholds); + } + + /** + * @dev Sets the amount thresholds to lock withdrawal. + * + * Requirements: + * - The method caller is admin. + * - The arrays have the same length and its length larger than 0. + * + * Emits the `LockedThresholdsUpdated` event. + * + */ + function setLockedThresholds(address[] calldata _tokens, uint256[] calldata _thresholds) external virtual onlyAdmin { + require(_tokens.length > 0, "WithdrawalLimitation: invalid array length"); + _setLockedThresholds(_tokens, _thresholds); + } + + /** + * @dev Sets fee percentages to unlock withdrawal. + * + * Requirements: + * - The method caller is admin. + * - The arrays have the same length and its length larger than 0. + * + * Emits the `UnlockFeePercentagesUpdated` event. + * + */ + function setUnlockFeePercentages(address[] calldata _tokens, uint256[] calldata _percentages) + external + virtual + onlyAdmin + { + require(_tokens.length > 0, "WithdrawalLimitation: invalid array length"); + _setUnlockFeePercentages(_tokens, _percentages); + } + + /** + * @dev Sets daily limit amounts for the withdrawals. + * + * Requirements: + * - The method caller is admin. + * - The arrays have the same length and its length larger than 0. + * + * Emits the `DailyWithdrawalLimitsUpdated` event. + * + */ + function setDailyWithdrawalLimits(address[] calldata _tokens, uint256[] calldata _limits) external virtual onlyAdmin { + require(_tokens.length > 0, "WithdrawalLimitation: invalid array length"); + _setDailyWithdrawalLimits(_tokens, _limits); + } + + /** + * @dev Checks whether the withdrawal reaches the limitation. + */ + function reachedWithdrawalLimit(address _token, uint256 _quantity) external view virtual returns (bool) { + return _reachedWithdrawalLimit(_token, _quantity); + } + + /** + * @dev Sets high-tier vote weight threshold and returns the old one. + * + * Emits the `HighTierVoteWeightThresholdUpdated` event. + * + */ + function _setHighTierVoteWeightThreshold(uint256 _numerator, uint256 _denominator) + internal + returns (uint256 _previousNum, uint256 _previousDenom) + { + require(_numerator <= _denominator, "WithdrawalLimitation: invalid threshold"); + _previousNum = _highTierVWNum; + _previousDenom = _highTierVWDenom; + _highTierVWNum = _numerator; + _highTierVWDenom = _denominator; + emit HighTierVoteWeightThresholdUpdated(nonce++, _numerator, _denominator, _previousNum, _previousDenom); + } + + /** + * @dev Sets the thresholds for high-tier withdrawals that requires high-tier vote weights. + * + * Requirements: + * - The array lengths are equal. + * + * Emits the `HighTierThresholdsUpdated` event. + * + */ + function _setHighTierThresholds(address[] calldata _tokens, uint256[] calldata _thresholds) internal virtual { + require(_tokens.length == _thresholds.length, "WithdrawalLimitation: invalid array length"); + for (uint256 _i; _i < _tokens.length; _i++) { + highTierThreshold[_tokens[_i]] = _thresholds[_i]; + } + emit HighTierThresholdsUpdated(_tokens, _thresholds); + } + + /** + * @dev Sets the amount thresholds to lock withdrawal. + * + * Requirements: + * - The array lengths are equal. + * + * Emits the `LockedThresholdsUpdated` event. + * + */ + function _setLockedThresholds(address[] calldata _tokens, uint256[] calldata _thresholds) internal virtual { + require(_tokens.length == _thresholds.length, "WithdrawalLimitation: invalid array length"); + for (uint256 _i; _i < _tokens.length; _i++) { + lockedThreshold[_tokens[_i]] = _thresholds[_i]; + } + emit LockedThresholdsUpdated(_tokens, _thresholds); + } + + /** + * @dev Sets fee percentages to unlock withdrawal. + * + * Requirements: + * - The array lengths are equal. + * - The percentage is equal to or less than 100_000. + * + * Emits the `UnlockFeePercentagesUpdated` event. + * + */ + function _setUnlockFeePercentages(address[] calldata _tokens, uint256[] calldata _percentages) internal virtual { + require(_tokens.length == _percentages.length, "WithdrawalLimitation: invalid array length"); + for (uint256 _i; _i < _tokens.length; _i++) { + require(_percentages[_i] <= _MAX_PERCENTAGE, "WithdrawalLimitation: invalid percentage"); + unlockFeePercentages[_tokens[_i]] = _percentages[_i]; + } + emit UnlockFeePercentagesUpdated(_tokens, _percentages); + } + + /** + * @dev Sets daily limit amounts for the withdrawals. + * + * Requirements: + * - The array lengths are equal. + * + * Emits the `DailyWithdrawalLimitsUpdated` event. + * + */ + function _setDailyWithdrawalLimits(address[] calldata _tokens, uint256[] calldata _limits) internal virtual { + require(_tokens.length == _limits.length, "WithdrawalLimitation: invalid array length"); + for (uint256 _i; _i < _tokens.length; _i++) { + dailyWithdrawalLimit[_tokens[_i]] = _limits[_i]; + } + emit DailyWithdrawalLimitsUpdated(_tokens, _limits); + } + + /** + * @dev Checks whether the withdrawal reaches the daily limitation. + * + * Requirements: + * - The daily withdrawal threshold should not apply for locked withdrawals. + * + */ + function _reachedWithdrawalLimit(address _token, uint256 _quantity) internal view virtual returns (bool) { + if (_lockedWithdrawalRequest(_token, _quantity)) { + return false; + } + + uint256 _currentDate = block.timestamp / 1 days; + if (_currentDate > lastDateSynced[_token]) { + return dailyWithdrawalLimit[_token] <= _quantity; + } else { + return dailyWithdrawalLimit[_token] <= lastSyncedWithdrawal[_token] + _quantity; + } + } + + /** + * @dev Record withdrawal token. + */ + function _recordWithdrawal(address _token, uint256 _quantity) internal virtual { + uint256 _currentDate = block.timestamp / 1 days; + if (_currentDate > lastDateSynced[_token]) { + lastDateSynced[_token] = _currentDate; + lastSyncedWithdrawal[_token] = _quantity; + } else { + lastSyncedWithdrawal[_token] += _quantity; + } + } + + /** + * @dev Returns whether the withdrawal request is locked or not. + */ + function _lockedWithdrawalRequest(address _token, uint256 _quantity) internal view virtual returns (bool) { + return lockedThreshold[_token] <= _quantity; + } + + /** + * @dev Computes fee percentage. + */ + function _computeFeePercentage(uint256 _amount, uint256 _percentage) internal view virtual returns (uint256) { + return (_amount * _percentage) / _MAX_PERCENTAGE; + } + + /** + * @dev Returns high-tier vote weight. + */ + function _highTierVoteWeight(uint256 _totalWeight) internal view virtual returns (uint256) { + return (_highTierVWNum * _totalWeight + _highTierVWDenom - 1) / _highTierVWDenom; + } + + /** + * @dev Validates whether the high-tier vote weight threshold is larger than the normal threshold. + */ + function _verifyThresholds() internal view { + require(_num * _highTierVWDenom <= _highTierVWNum * _denom, "WithdrawalLimitation: invalid thresholds"); + } +} diff --git a/contracts/extensions/collections/HasRoninGovernanceAdminContract.sol b/contracts/extensions/collections/HasRoninGovernanceAdminContract.sol new file mode 100644 index 000000000..f53240e80 --- /dev/null +++ b/contracts/extensions/collections/HasRoninGovernanceAdminContract.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./HasProxyAdmin.sol"; +import "../../interfaces/collections/IHasRoninGovernanceAdminContract.sol"; +import "../../interfaces/IRoninGovernanceAdmin.sol"; + +contract HasRoninGovernanceAdminContract is IHasRoninGovernanceAdminContract, HasProxyAdmin { + IRoninGovernanceAdmin internal _roninGovernanceAdminContract; + + modifier onlyRoninGovernanceAdminContract() { + require( + roninGovernanceAdminContract() == msg.sender, + "HasRoninGovernanceAdminContract: method caller must be ronin governance admin contract" + ); + _; + } + + /** + * @inheritdoc IHasRoninGovernanceAdminContract + */ + function roninGovernanceAdminContract() public view override returns (address) { + return address(_roninGovernanceAdminContract); + } + + /** + * @inheritdoc IHasRoninGovernanceAdminContract + */ + function setRoninGovernanceAdminContract(address _addr) external override onlyAdmin { + _setRoninGovernanceAdminContract(_addr); + } + + /** + * @dev Sets the ronin governance admin contract. + * + * Requirements: + * - The new address is a contract. + * + * Emits the event `RoninGovernanceAdminContractUpdated`. + * + */ + function _setRoninGovernanceAdminContract(address _addr) internal { + _roninGovernanceAdminContract = IRoninGovernanceAdmin(_addr); + emit RoninGovernanceAdminContractUpdated(_addr); + } +} diff --git a/contracts/extensions/isolated-governance/bridge-operator-governance/BOsGovernanceProposal.sol b/contracts/extensions/isolated-governance/bridge-operator-governance/BOsGovernanceProposal.sol index e303dfb56..798e47589 100644 --- a/contracts/extensions/isolated-governance/bridge-operator-governance/BOsGovernanceProposal.sol +++ b/contracts/extensions/isolated-governance/bridge-operator-governance/BOsGovernanceProposal.sol @@ -2,21 +2,28 @@ pragma solidity ^0.8.0; import "../../../extensions/isolated-governance/IsolatedGovernance.sol"; -import "../../../interfaces/consumers/WeightedAddressConsumer.sol"; import "../../../interfaces/consumers/SignatureConsumer.sol"; import "../../../libraries/BridgeOperatorsBallot.sol"; +import "../../../interfaces/IRoninGovernanceAdmin.sol"; -abstract contract BOsGovernanceProposal is SignatureConsumer, WeightedAddressConsumer, IsolatedGovernance { +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 Mapping from governor address => last block that the governor voted + /// @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; + /** + * @inheritdoc IRoninGovernanceAdmin + */ + function lastVotedBlock(address _bridgeVoter) external view returns (uint256) { + return _lastVotedBlock[_bridgeVoter]; + } + /** * @dev Votes for a set of bridge operators by signatures. * @@ -27,7 +34,7 @@ abstract contract BOsGovernanceProposal is SignatureConsumer, WeightedAddressCon * */ function _castVotesBySignatures( - WeightedAddress[] calldata _operators, + address[] calldata _operators, Signature[] calldata _signatures, uint256 _period, uint256 _minimumVoteWeight, @@ -50,7 +57,7 @@ abstract contract BOsGovernanceProposal is SignatureConsumer, WeightedAddressCon require(_lastSigner < _signer, "BOsGovernanceProposal: invalid order"); _lastSigner = _signer; - uint256 _weight = _getWeight(_signer); + uint256 _weight = _getBridgeVoterWeight(_signer); if (_weight > 0) { _hasValidVotes = true; _lastVotedBlock[_signer] = block.number; @@ -65,7 +72,7 @@ abstract contract BOsGovernanceProposal is SignatureConsumer, WeightedAddressCon } /** - * @dev Returns the weight of a governor. + * @dev Returns the weight of a bridge voter. */ - function _getWeight(address _governor) internal view virtual returns (uint256); + function _getBridgeVoterWeight(address _bridgeVoter) internal view virtual returns (uint256); } diff --git a/contracts/extensions/isolated-governance/bridge-operator-governance/BOsGovernanceRelay.sol b/contracts/extensions/isolated-governance/bridge-operator-governance/BOsGovernanceRelay.sol index 3bc816e1b..4a33acccc 100644 --- a/contracts/extensions/isolated-governance/bridge-operator-governance/BOsGovernanceRelay.sol +++ b/contracts/extensions/isolated-governance/bridge-operator-governance/BOsGovernanceRelay.sol @@ -2,11 +2,10 @@ pragma solidity ^0.8.0; import "../../../extensions/isolated-governance/IsolatedGovernance.sol"; -import "../../../interfaces/consumers/WeightedAddressConsumer.sol"; import "../../../interfaces/consumers/SignatureConsumer.sol"; import "../../../libraries/BridgeOperatorsBallot.sol"; -abstract contract BOsGovernanceRelay is SignatureConsumer, WeightedAddressConsumer, IsolatedGovernance { +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 @@ -24,7 +23,7 @@ abstract contract BOsGovernanceRelay is SignatureConsumer, WeightedAddressConsum * */ function _relayVotesBySignatures( - WeightedAddress[] calldata _operators, + address[] calldata _operators, Signature[] calldata _signatures, uint256 _period, uint256 _minimumVoteWeight, @@ -47,7 +46,7 @@ abstract contract BOsGovernanceRelay is SignatureConsumer, WeightedAddressConsum } IsolatedVote storage _v = _vote[_period]; - uint256 _totalVoteWeight = _getWeights(_signers); + uint256 _totalVoteWeight = _sumBridgeVoterWeights(_signers); if (_totalVoteWeight >= _minimumVoteWeight) { require(_totalVoteWeight > 0, "BOsGovernanceRelay: invalid vote weight"); _v.status = VoteStatus.Approved; @@ -61,5 +60,5 @@ abstract contract BOsGovernanceRelay is SignatureConsumer, WeightedAddressConsum /** * @dev Returns the weight of the governor list. */ - function _getWeights(address[] memory _governors) internal view virtual returns (uint256); + function _sumBridgeVoterWeights(address[] memory _bridgeVoters) internal view virtual returns (uint256); } diff --git a/contracts/extensions/sequential-governance/GovernanceRelay.sol b/contracts/extensions/sequential-governance/GovernanceRelay.sol index 241042309..358a973f3 100644 --- a/contracts/extensions/sequential-governance/GovernanceRelay.sol +++ b/contracts/extensions/sequential-governance/GovernanceRelay.sol @@ -58,7 +58,7 @@ abstract contract GovernanceRelay is CoreGovernance { ProposalVote storage _vote = vote[_proposal.chainId][_proposal.nonce]; uint256 _minimumForVoteWeight = _getMinimumVoteWeight(); - uint256 _totalForVoteWeight = _getWeights(_forVoteSigners); + uint256 _totalForVoteWeight = _sumWeights(_forVoteSigners); if (_totalForVoteWeight >= _minimumForVoteWeight) { require(_totalForVoteWeight > 0, "GovernanceRelay: invalid vote weight"); _vote.status = VoteStatus.Approved; @@ -68,7 +68,7 @@ abstract contract GovernanceRelay is CoreGovernance { } uint256 _minimumAgainstVoteWeight = _getTotalWeights() - _minimumForVoteWeight + 1; - uint256 _totalAgainstVoteWeight = _getWeights(_againstVoteSigners); + uint256 _totalAgainstVoteWeight = _sumWeights(_againstVoteSigners); if (_totalAgainstVoteWeight >= _minimumAgainstVoteWeight) { require(_totalAgainstVoteWeight > 0, "GovernanceRelay: invalid vote weight"); _vote.status = VoteStatus.Rejected; @@ -139,5 +139,5 @@ abstract contract GovernanceRelay is CoreGovernance { /** * @dev Returns the weight of the governor list. */ - function _getWeights(address[] memory _governors) internal view virtual returns (uint256); + function _sumWeights(address[] memory _governors) internal view virtual returns (uint256); } diff --git a/contracts/interfaces/IBridge.sol b/contracts/interfaces/IBridge.sol index 71e81fd2d..75396d982 100644 --- a/contracts/interfaces/IBridge.sol +++ b/contracts/interfaces/IBridge.sol @@ -1,9 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import "./consumers/WeightedAddressConsumer.sol"; - -interface IBridge is WeightedAddressConsumer { +interface IBridge { /** * @dev Replaces the old bridge operator list by the new one. * @@ -13,10 +11,10 @@ interface IBridge is WeightedAddressConsumer { * Emitted the event `BridgeOperatorsReplaced`. * */ - function replaceBridgeOperators(WeightedAddress[] calldata) external; + function replaceBridgeOperators(address[] calldata) external; /** * @dev Returns the bridge operator list. */ - function getBridgeOperators() external view returns (WeightedAddress[] memory); + function getBridgeOperators() external view returns (address[] memory); } diff --git a/contracts/interfaces/IERC20Mintable.sol b/contracts/interfaces/IERC20Mintable.sol new file mode 100644 index 000000000..68b4a37ac --- /dev/null +++ b/contracts/interfaces/IERC20Mintable.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +interface IERC20Mintable { + function mint(address _to, uint256 _value) external returns (bool _success); +} diff --git a/contracts/interfaces/IERC721Mintable.sol b/contracts/interfaces/IERC721Mintable.sol new file mode 100644 index 000000000..a1cfa0056 --- /dev/null +++ b/contracts/interfaces/IERC721Mintable.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IERC721Mintable { + function mint(address _to, uint256 _tokenId) external returns (bool); +} diff --git a/contracts/interfaces/IMainchainGatewayV2.sol b/contracts/interfaces/IMainchainGatewayV2.sol new file mode 100644 index 000000000..05eb42628 --- /dev/null +++ b/contracts/interfaces/IMainchainGatewayV2.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./IBridge.sol"; +import "./IWETH.sol"; +import "./consumers/SignatureConsumer.sol"; +import "./consumers/MappedTokenConsumer.sol"; +import "../libraries/Transfer.sol"; + +interface IMainchainGatewayV2 is SignatureConsumer, MappedTokenConsumer, IBridge { + /// @dev Emitted when the deposit is requested + event DepositRequested(bytes32 receiptHash, Transfer.Receipt receipt); + /// @dev Emitted when the assets are withdrawn + event Withdrew(bytes32 receiptHash, Transfer.Receipt receipt); + /// @dev Emitted when the tokens are mapped + event TokenMapped(address[] mainchainTokens, address[] roninTokens, Token.Standard[] standards); + /// @dev Emitted when the wrapped native token contract is updated + event WrappedNativeTokenContractUpdated(IWETH weth); + /// @dev Emitted when the withdrawal is locked + event WithdrawalLocked(bytes32 receiptHash, Transfer.Receipt receipt); + /// @dev Emitted when the withdrawal is unlocked + event WithdrawalUnlocked(bytes32 receiptHash, Transfer.Receipt receipt); + + /** + * @dev Returns the domain seperator. + */ + function DOMAIN_SEPARATOR() external view returns (bytes32); + + /** + * @dev Returns deposit count. + */ + function depositCount() external view returns (uint256); + + /** + * @dev Sets the wrapped native token contract. + * + * Requirements: + * - The method caller is admin. + * + * Emits the `WrappedNativeTokenContractUpdated` event. + * + */ + function setWrappedNativeTokenContract(IWETH _wrappedToken) external; + + /** + * @dev Returns whether the withdrawal is locked. + */ + function withdrawalLocked(uint256 withdrawalId) external view returns (bool); + + /** + * @dev Returns the withdrawal hash. + */ + function withdrawalHash(uint256 withdrawalId) external view returns (bytes32); + + /** + * @dev Locks the assets and request deposit. + */ + function requestDepositFor(Transfer.Request calldata _request) external payable; + + /** + * @dev Withdraws based on the receipt and the validator signatures. + * Returns whether the withdrawal is locked. + * + * Emits the `Withdrew` once the assets are released. + * + */ + function submitWithdrawal(Transfer.Receipt memory _receipt, Signature[] memory _signatures) + external + returns (bool _locked); + + /** + * @dev Approves a specific withdrawal. + * + * Requirements: + * - The method caller is a validator. + * + * Emits the `Withdrew` once the assets are released. + * + */ + function unlockWithdrawal(Transfer.Receipt calldata _receipt) external; + + /** + * @dev Maps mainchain tokens to Ronin network. + * + * Requirement: + * - The method caller is admin. + * - The arrays have the same length and its length larger than 0. + * + * Emits the `TokenMapped` event. + * + */ + function mapTokens( + address[] calldata _mainchainTokens, + address[] calldata _roninTokens, + Token.Standard[] calldata _standards + ) external; + + /** + * @dev Maps mainchain tokens to Ronin network and sets thresholds. + * + * Requirement: + * - The method caller is admin. + * - The arrays have the same length and its length larger than 0. + * + * Emits the `TokenMapped` event. + * + */ + function mapTokensAndThresholds( + address[] calldata _mainchainTokens, + address[] calldata _roninTokens, + Token.Standard[] calldata _standards, + uint256[][4] calldata _thresholds + ) external; + + /** + * @dev Returns token address on Ronin network. + * Note: Reverts for unsupported token. + */ + function getRoninToken(address _mainchainToken) external view returns (MappedToken memory _token); +} diff --git a/contracts/interfaces/IRoninGatewayV2.sol b/contracts/interfaces/IRoninGatewayV2.sol new file mode 100644 index 000000000..466f1c411 --- /dev/null +++ b/contracts/interfaces/IRoninGatewayV2.sol @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "../libraries/Transfer.sol"; +import "./consumers/MappedTokenConsumer.sol"; + +interface IRoninGatewayV2 is MappedTokenConsumer { + /// @dev Emitted when the assets are depositted + event Deposited(bytes32 receiptHash, Transfer.Receipt receipt); + /// @dev Emitted when the withdrawal is requested + event WithdrawalRequested(bytes32 receiptHash, Transfer.Receipt); + /// @dev Emitted when the assets are withdrawn on mainchain + event MainchainWithdrew(bytes32 receiptHash, Transfer.Receipt receipt); + /// @dev Emitted when the withdrawal signatures is requested + event WithdrawalSignaturesRequested(bytes32 receiptHash, Transfer.Receipt); + /// @dev Emitted when the tokens are mapped + event TokenMapped(address[] roninTokens, address[] mainchainTokens, uint256[] chainIds, Token.Standard[] standards); + + /** + * @dev Returns withdrawal count. + */ + function withdrawalCount() external view returns (uint256); + + /** + * @dev Returns withdrawal signatures. + */ + function getWithdrawalSignatures(uint256 _withdrawalId, address[] calldata _validators) + external + view + returns (bytes[] memory); + + /** + * @dev Deposits based on the receipt. + * + * Requirements: + * - The method caller is a validator. + * + * Emits the `Deposited` once the assets are released. + * + * @notice The assets will be transferred whenever the valid call passes the quorum threshold. + * + */ + function depositFor(Transfer.Receipt calldata _receipt) external; + + /** + * @dev Marks the withdrawals are done on mainchain and returns the boolean array indicating whether the withdrawal + * vote is already done before. + * + * Requirements: + * - The method caller is a validator. + * + * Emits the `MainchainWithdrew` once the valid call passes the quorum threshold. + * + * @notice Not reverting to avoid unnecessary failed transactions because the validators can send transactions at the + * same time. + * + */ + function tryBulkAcknowledgeMainchainWithdrew(uint256[] calldata _withdrawalIds) external returns (bool[] memory); + + /** + * @dev Tries bulk deposits based on the receipts and returns the boolean array indicating whether the deposit vote + * is already done before. Reverts if the deposit is invalid or is voted by the validator again. + * + * Requirements: + * - The method caller is a validator. + * + * Emits the `Deposited` once the assets are released. + * + * @notice The assets will be transferred whenever the valid call for the receipt passes the quorum threshold. Not + * reverting to avoid unnecessary failed transactions because the validators can send transactions at the same time. + * + */ + function tryBulkDepositFor(Transfer.Receipt[] calldata _receipts) external returns (bool[] memory); + + /** + * @dev Locks the assets and request withdrawal. + * + * Emits the `WithdrawalRequested` event. + * + */ + function requestWithdrawalFor(Transfer.Request calldata _request, uint256 _chainId) external; + + /** + * @dev Bulk requests withdrawals. + * + * Emits the `WithdrawalRequested` events. + * + */ + function bulkRequestWithdrawalFor(Transfer.Request[] calldata _requests, uint256 _chainId) external; + + /** + * @dev Requests withdrawal signatures for a specific withdrawal. + * + * Emits the `WithdrawalSignaturesRequested` event. + * + */ + function requestWithdrawalSignatures(uint256 _withdrawalId) external; + + /** + * @dev Submits withdrawal signatures. + * + * Requirements: + * - The method caller is a validator. + * + */ + function bulkSubmitWithdrawalSignatures(uint256[] calldata _withdrawals, bytes[] calldata _signatures) external; + + /** + * @dev Maps Ronin tokens to mainchain networks. + * + * Requirement: + * - The method caller is admin. + * - The arrays have the same length and its length larger than 0. + * + * Emits the `TokenMapped` event. + * + */ + function mapTokens( + address[] calldata _roninTokens, + address[] calldata _mainchainTokens, + uint256[] calldata chainIds, + Token.Standard[] calldata _standards + ) external; + + /** + * @dev Returns whether the deposit is casted by the voter. + */ + function depositVoted( + uint256 _chainId, + uint256 _depositId, + address _voter + ) external view returns (bool); + + /** + * @dev Returns whether the mainchain withdrew is casted by the voter. + */ + function mainchainWithdrewVoted(uint256 _withdrawalId, address _voter) external view returns (bool); + + /** + * @dev Returns whether the withdrawal is done on mainchain. + */ + function mainchainWithdrew(uint256 _withdrawalId) external view returns (bool); + + /** + * @dev Returns mainchain token address. + * Reverts for unsupported token. + */ + function getMainchainToken(address _roninToken, uint256 _chainId) external view returns (MappedToken memory _token); +} diff --git a/contracts/interfaces/IRoninGovernanceAdmin.sol b/contracts/interfaces/IRoninGovernanceAdmin.sol new file mode 100644 index 000000000..01fb57581 --- /dev/null +++ b/contracts/interfaces/IRoninGovernanceAdmin.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IRoninGovernanceAdmin { + /** + * @dev Returns the last voted block of the bridge voter. + */ + function lastVotedBlock(address _bridgeVoter) external view returns (uint256); +} diff --git a/contracts/interfaces/IRoninTrustedOrganization.sol b/contracts/interfaces/IRoninTrustedOrganization.sol index 6cca8ee3b..9a9935263 100644 --- a/contracts/interfaces/IRoninTrustedOrganization.sol +++ b/contracts/interfaces/IRoninTrustedOrganization.sol @@ -2,16 +2,28 @@ pragma solidity ^0.8.9; -import "./consumers/WeightedAddressConsumer.sol"; import "./IQuorum.sol"; -interface IRoninTrustedOrganization is WeightedAddressConsumer, IQuorum { +interface IRoninTrustedOrganization is IQuorum { + struct TrustedOrganization { + // Address of the validator that produces block, e.g. block.coinbase. This is so-called validator address. + address consensusAddr; + // Address to voting proposal + address governor; + // Address to voting bridge operators + address bridgeVoter; + // Its Weight + uint256 weight; + // The block that the organization was added + uint256 addedBlock; + } + /// @dev Emitted when the trusted organization is added. - event TrustedOrganizationAdded(WeightedAddress org); + event TrustedOrganizationsAdded(TrustedOrganization[] orgs); /// @dev Emitted when the trusted organization is updated. - event TrustedOrganizationUpdated(WeightedAddress org); + event TrustedOrganizationsUpdated(TrustedOrganization[] orgs); /// @dev Emitted when the trusted organization is removed. - event TrustedOrganizationRemoved(address org); + event TrustedOrganizationsRemoved(address[] orgs); /** * @dev Adds a list of addresses into the trusted organization. @@ -19,11 +31,12 @@ interface IRoninTrustedOrganization is WeightedAddressConsumer, IQuorum { * Requirements: * - The weights should larger than 0. * - The method caller is admin. + * - The field `addedBlock` should be blank. * * Emits the event `TrustedOrganizationAdded` once an organization is added. * */ - function addTrustedOrganizations(WeightedAddress[] calldata) external; + function addTrustedOrganizations(TrustedOrganization[] calldata) external; /** * @dev Updates weights for a list of existent trusted organization. @@ -35,7 +48,7 @@ interface IRoninTrustedOrganization is WeightedAddressConsumer, IQuorum { * Emits the `TrustedOrganizationUpdated` event. * */ - function updateTrustedOrganizations(WeightedAddress[] calldata _list) external; + function updateTrustedOrganizations(TrustedOrganization[] calldata _list) external; /** * @dev Removes a list of addresses from the trusted organization. @@ -54,24 +67,54 @@ interface IRoninTrustedOrganization is WeightedAddressConsumer, IQuorum { function totalWeights() external view returns (uint256); /** - * @dev Returns the weight of an address. + * @dev Returns the weight of a consensus. + */ + function getConsensusWeight(address _consensusAddr) external view returns (uint256); + + /** + * @dev Returns the weight of a governor. + */ + function getGovernorWeight(address _governor) external view returns (uint256); + + /** + * @dev Returns the weight of a bridge voter. + */ + function getBridgeVoterWeight(address _addr) external view returns (uint256); + + /** + * @dev Returns the weights of a list of consensus addresses. */ - function getWeight(address _addr) external view returns (uint256); + function getConsensusWeights(address[] calldata _list) external view returns (uint256[] memory); /** - * @dev Returns the weights of a list of addresses. + * @dev Returns the weights of a list of governor addresses. */ - function getWeights(address[] calldata _list) external view returns (uint256[] memory); + function getGovernorWeights(address[] calldata _list) external view returns (uint256[] memory); /** - * @dev Returns total weights of the address list. + * @dev Returns the weights of a list of bridge voter addresses. */ - function sumWeights(address[] calldata _list) external view returns (uint256 _res); + function getBridgeVoterWeights(address[] calldata _list) external view returns (uint256[] memory); + + /** + * @dev Returns total weights of the consensus list. + */ + function sumConsensusWeights(address[] calldata _list) external view returns (uint256 _res); + + /** + * @dev Returns total weights of the governor list. + */ + function sumGovernorWeights(address[] calldata _list) external view returns (uint256 _res); + + /** + * @dev Returns total weights of the bridge voter list. + */ + function sumBridgeVoterWeights(address[] calldata _list) external view returns (uint256 _res); /** * @dev Returns the trusted organization at `_index`. */ - function getTrustedOrganizationAt(uint256 _index) external view returns (WeightedAddress memory); + function getTrustedOrganizationAt(uint256 _index) external view returns (TrustedOrganization memory); /** * @dev Returns the number of trusted organizations. @@ -79,7 +122,14 @@ interface IRoninTrustedOrganization is WeightedAddressConsumer, IQuorum { function countTrustedOrganizations() external view returns (uint256); /** - * @dev Returns all of the trusted organization addresses. + * @dev Returns all of the trusted organizations. + */ + function getAllTrustedOrganizations() external view returns (TrustedOrganization[] memory); + + /** + * @dev Returns the trusted organization by consensus address. + * + * Reverts once the consensus address is non-existent. */ - function getAllTrustedOrganizations() external view returns (WeightedAddress[] memory); + function getTrustedOrganization(address _consensusAddr) external view returns (TrustedOrganization memory); } diff --git a/contracts/interfaces/ISlashIndicator.sol b/contracts/interfaces/ISlashIndicator.sol index c60e9055f..62897f876 100644 --- a/contracts/interfaces/ISlashIndicator.sol +++ b/contracts/interfaces/ISlashIndicator.sol @@ -7,13 +7,18 @@ interface ISlashIndicator { UNKNOWN, MISDEMEANOR, FELONY, - DOUBLE_SIGNING + DOUBLE_SIGNING, + BRIDGE_VOTING } /// @dev Emitted when the validator is slashed for unavailability event UnavailabilitySlashed(address indexed validator, SlashType slashType, uint256 period); /// @dev Emitted when the thresholds updated event SlashThresholdsUpdated(uint256 felonyThreshold, uint256 misdemeanorThreshold); + /// @dev Emitted when the threshold to slash when trusted organization does not vote for bridge operators is updated + event BridgeVotingThresholdUpdated(uint256 threshold); + /// @dev Emitted when the amount of RON to slash bridge voting is updated + event BridgeVotingSlashAmountUpdated(uint256 amount); /// @dev Emitted when the amount of slashing felony updated event SlashFelonyAmountUpdated(uint256 slashFelonyAmount); /// @dev Emitted when the amount of slashing double sign updated @@ -51,6 +56,13 @@ interface ISlashIndicator { bytes calldata _header2 ) external; + /** + * @dev Slashes for bridge voter governance. + * + * Emits the event `UnavailabilitySlashed`. + */ + function slashBridgeVoting(address _consensusAddr) external; + /** * @dev Sets the slash thresholds * @@ -95,6 +107,28 @@ interface ISlashIndicator { */ function setFelonyJailDuration(uint256 _felonyJailDuration) external; + /** + * @dev Sets the threshold to slash when trusted organization does not vote for bridge operators. + * + * Requirements: + * - Only admin can call this method + * + * Emits the event `BridgeVotingThresholdUpdated` + * + */ + function setBridgeVotingThreshold(uint256 _threshold) external; + + /** + * @dev Sets the amount of RON to slash bridge voting. + * + * Requirements: + * - Only admin can call this method + * + * Emits the event `BridgeVotingSlashAmountUpdated` + * + */ + function setBridgeVotingSlashAmount(uint256 _amount) external; + /** * @dev Returns the current unavailability indicator of a validator. */ diff --git a/contracts/interfaces/IWETH.sol b/contracts/interfaces/IWETH.sol new file mode 100644 index 000000000..448714338 --- /dev/null +++ b/contracts/interfaces/IWETH.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IWETH { + function deposit() external payable; + + function withdraw(uint256 _wad) external; + + function balanceOf(address) external view returns (uint256); +} diff --git a/contracts/interfaces/collections/IHasRoninGovernanceAdminContract.sol b/contracts/interfaces/collections/IHasRoninGovernanceAdminContract.sol new file mode 100644 index 000000000..905cf66c9 --- /dev/null +++ b/contracts/interfaces/collections/IHasRoninGovernanceAdminContract.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.9; + +interface IHasRoninGovernanceAdminContract { + /// @dev Emitted when the ronin governance admin contract is updated. + event RoninGovernanceAdminContractUpdated(address); + + /** + * @dev Returns the ronin governance admin contract. + */ + function roninGovernanceAdminContract() external view returns (address); + + /** + * @dev Sets the ronin governance admin contract. + * + * Requirements: + * - The method caller is admin. + * - The new address is a contract. + * + * Emits the event `RoninGovernanceAdminContractUpdated`. + * + */ + function setRoninGovernanceAdminContract(address) external; +} diff --git a/contracts/interfaces/consumers/MappedTokenConsumer.sol b/contracts/interfaces/consumers/MappedTokenConsumer.sol new file mode 100644 index 000000000..ca0f57eba --- /dev/null +++ b/contracts/interfaces/consumers/MappedTokenConsumer.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "../../libraries/Token.sol"; + +interface MappedTokenConsumer { + struct MappedToken { + Token.Standard erc; + address tokenAddr; + } +} diff --git a/contracts/libraries/BridgeOperatorsBallot.sol b/contracts/libraries/BridgeOperatorsBallot.sol index 0c6bbb3a7..2b033f4a3 100644 --- a/contracts/libraries/BridgeOperatorsBallot.sol +++ b/contracts/libraries/BridgeOperatorsBallot.sol @@ -5,38 +5,19 @@ import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import "../interfaces/consumers/WeightedAddressConsumer.sol"; library BridgeOperatorsBallot { - // keccak256("BridgeOperator(address addr,uint256 weight)"); - bytes32 public constant BRIDGE_OPERATOR_TYPEHASH = 0xe71132f1797176c8456299d5325989bbf16523f1e2e3aef4554d23f982955a2c; - - /** - * @dev Returns hash of an operator struct. - */ - function hash(WeightedAddressConsumer.WeightedAddress calldata _operator) internal pure returns (bytes32) { - return keccak256(abi.encode(BRIDGE_OPERATOR_TYPEHASH, _operator.addr, _operator.weight)); - } - - // keccak256("BridgeOperatorsBallot(uint256 period,BridgeOperator[] operators)BridgeOperator(address addr,uint256 weight)"); - bytes32 public constant BRIDGE_OPERATORS_ACKNOWLEDGE_BALLOT_TYPEHASH = - 0x086d287088869477577720f66bf2a8412510e726fd1a893739cf6c2280aadcb5; + // keccak256("BridgeOperatorsBallot(uint256 period,address[] operators)"); + bytes32 public constant BRIDGE_OPERATORS_BALLOT_TYPEHASH = + 0xeea5e3908ac28cbdbbce8853e49444c558a0a03597e98ef19e6ff86162ed9ae3; /** * @dev Returns hash of the ballot. */ - function hash(uint256 _period, WeightedAddressConsumer.WeightedAddress[] calldata _operators) - internal - pure - returns (bytes32) - { - bytes32[] memory _hashArr = new bytes32[](_operators.length); - for (uint256 _i; _i < _hashArr.length; _i++) { - _hashArr[_i] = hash(_operators[_i]); - } - + function hash(uint256 _period, address[] memory _operators) internal pure returns (bytes32) { bytes32 _operatorsHash; assembly { - _operatorsHash := keccak256(add(_hashArr, 32), mul(mload(_hashArr), 32)) + _operatorsHash := keccak256(add(_operators, 32), mul(mload(_operators), 32)) } - return keccak256(abi.encode(BRIDGE_OPERATORS_ACKNOWLEDGE_BALLOT_TYPEHASH, _period, _operatorsHash)); + return keccak256(abi.encode(BRIDGE_OPERATORS_BALLOT_TYPEHASH, _period, _operatorsHash)); } } diff --git a/contracts/libraries/Token.sol b/contracts/libraries/Token.sol new file mode 100644 index 000000000..829fa578f --- /dev/null +++ b/contracts/libraries/Token.sol @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; +import "../interfaces/IWETH.sol"; + +library Token { + enum Standard { + ERC20, + ERC721 + } + struct Info { + Standard erc; + // For ERC20: the id must be 0 and the quantity is larger than 0. + // For ERC721: the quantity must be 0. + uint256 id; + uint256 quantity; + } + + // keccak256("TokenInfo(uint8 erc,uint256 id,uint256 quantity)"); + bytes32 public constant INFO_TYPE_HASH = 0x1e2b74b2a792d5c0f0b6e59b037fa9d43d84fbb759337f0112fcc15ca414fc8d; + + /** + * @dev Returns token info struct hash. + */ + function hash(Info memory _info) internal pure returns (bytes32) { + return keccak256(abi.encode(INFO_TYPE_HASH, _info.erc, _info.id, _info.quantity)); + } + + /** + * @dev Validates the token info. + */ + function validate(Info memory _info) internal pure { + require( + (_info.erc == Standard.ERC20 && _info.quantity > 0 && _info.id == 0) || + (_info.erc == Standard.ERC721 && _info.quantity == 0), + "Token: invalid info" + ); + } + + /** + * @dev Transfer asset from. + * + * Requirements: + * - The `_from` address must approve for the contract using this library. + * + */ + function transferFrom( + Info memory _info, + address _from, + address _to, + address _token + ) internal { + bool _success; + bytes memory _data; + if (_info.erc == Standard.ERC20) { + (_success, _data) = _token.call(abi.encodeWithSelector(IERC20.transferFrom.selector, _from, _to, _info.quantity)); + _success = _success && (_data.length == 0 || abi.decode(_data, (bool))); + } else if (_info.erc == Standard.ERC721) { + // bytes4(keccak256("transferFrom(address,address,uint256)")) + (_success, ) = _token.call(abi.encodeWithSelector(0x23b872dd, _from, _to, _info.id)); + } else { + revert("Token: unsupported token standard"); + } + + if (!_success) { + revert( + string( + abi.encodePacked( + "Token: could not transfer ", + toString(_info), + " from ", + Strings.toHexString(uint160(_from), 20), + " to ", + Strings.toHexString(uint160(_to), 20), + " token ", + Strings.toHexString(uint160(_token), 20) + ) + ) + ); + } + } + + /** + * @dev Transfers ERC721 token and returns the result. + */ + function tryTransferERC721( + address _token, + address _to, + uint256 _id + ) internal returns (bool _success) { + (_success, ) = _token.call(abi.encodeWithSelector(IERC721.transferFrom.selector, address(this), _to, _id)); + } + + /** + * @dev Transfers ERC20 token and returns the result. + */ + function tryTransferERC20( + address _token, + address _to, + uint256 _quantity + ) internal returns (bool _success) { + bytes memory _data; + (_success, _data) = _token.call(abi.encodeWithSelector(IERC20.transfer.selector, _to, _quantity)); + _success = _success && (_data.length == 0 || abi.decode(_data, (bool))); + } + + /** + * @dev Transfer assets from current address to `_to` address. + */ + function transfer( + Info memory _info, + address _to, + address _token + ) internal { + bool _success; + if (_info.erc == Standard.ERC20) { + _success = tryTransferERC20(_token, _to, _info.quantity); + } else if (_info.erc == Standard.ERC721) { + _success = tryTransferERC721(_token, _to, _info.id); + } else { + revert("Token: unsupported token standard"); + } + + if (!_success) { + revert( + string( + abi.encodePacked( + "Token: could not transfer ", + toString(_info), + " to ", + Strings.toHexString(uint160(_to), 20), + " token ", + Strings.toHexString(uint160(_token), 20) + ) + ) + ); + } + } + + /** + * @dev Tries minting and transfering assets. + * + * @notice Prioritizes transfer native token if the token is wrapped. + * + */ + function handleAssetTransfer( + Info memory _info, + address payable _to, + address _token, + IWETH _wrappedNativeToken + ) internal { + bool _success; + if (_token == address(_wrappedNativeToken)) { + // Try sending the native token before transferring the wrapped token + if (!_to.send(_info.quantity)) { + _wrappedNativeToken.deposit{ value: _info.quantity }(); + transfer(_info, _to, _token); + } + } else if (_info.erc == Token.Standard.ERC20) { + uint256 _balance = IERC20(_token).balanceOf(address(this)); + + if (_balance < _info.quantity) { + // bytes4(keccak256("mint(address,uint256)")) + (_success, ) = _token.call(abi.encodeWithSelector(0x40c10f19, address(this), _info.quantity - _balance)); + require(_success, "Token: ERC20 minting failed"); + } + + transfer(_info, _to, _token); + } else if (_info.erc == Token.Standard.ERC721) { + if (!tryTransferERC721(_token, _to, _info.id)) { + // bytes4(keccak256("mint(address,uint256)")) + (_success, ) = _token.call(abi.encodeWithSelector(0x40c10f19, _to, _info.id)); + require(_success, "Token: ERC721 minting failed"); + } + } else { + revert("Token: unsupported token standard"); + } + } + + /** + * @dev Returns readable string. + */ + function toString(Info memory _info) internal pure returns (string memory) { + return + string( + abi.encodePacked( + "TokenInfo(", + Strings.toHexString(uint160(_info.erc), 1), + ",", + Strings.toHexString(_info.id), + ",", + Strings.toHexString(_info.quantity), + ")" + ) + ); + } + + struct Owner { + address addr; + address tokenAddr; + uint256 chainId; + } + + // keccak256("TokenOwner(address addr,address tokenAddr,uint256 chainId)"); + bytes32 public constant OWNER_TYPE_HASH = 0x353bdd8d69b9e3185b3972e08b03845c0c14a21a390215302776a7a34b0e8764; + + /** + * @dev Returns ownership struct hash. + */ + function hash(Owner memory _owner) internal pure returns (bytes32) { + return keccak256(abi.encode(OWNER_TYPE_HASH, _owner.addr, _owner.tokenAddr, _owner.chainId)); + } +} diff --git a/contracts/libraries/Transfer.sol b/contracts/libraries/Transfer.sol new file mode 100644 index 000000000..54b768731 --- /dev/null +++ b/contracts/libraries/Transfer.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; +import "./Token.sol"; + +library Transfer { + using ECDSA for bytes32; + + enum Kind { + Deposit, + Withdrawal + } + + struct Request { + // For deposit request: Recipient address on Ronin network + // For withdrawal request: Recipient address on mainchain network + address recipientAddr; + // Token address to deposit/withdraw + // Value 0: native token + address tokenAddr; + Token.Info info; + } + + /** + * @dev Converts the transfer request into the deposit receipt. + */ + function into_deposit_receipt( + Request memory _request, + address _requester, + uint256 _id, + address _roninTokenAddr, + uint256 _roninChainId + ) internal view returns (Receipt memory _receipt) { + _receipt.id = _id; + _receipt.kind = Kind.Deposit; + _receipt.mainchain.addr = _requester; + _receipt.mainchain.tokenAddr = _request.tokenAddr; + _receipt.mainchain.chainId = block.chainid; + _receipt.ronin.addr = _request.recipientAddr; + _receipt.ronin.tokenAddr = _roninTokenAddr; + _receipt.ronin.chainId = _roninChainId; + _receipt.info = _request.info; + } + + /** + * @dev Converts the transfer request into the withdrawal receipt. + */ + function into_withdrawal_receipt( + Request memory _request, + address _requester, + uint256 _id, + address _mainchainTokenAddr, + uint256 _mainchainId + ) internal view returns (Receipt memory _receipt) { + _receipt.id = _id; + _receipt.kind = Kind.Withdrawal; + _receipt.ronin.addr = _requester; + _receipt.ronin.tokenAddr = _request.tokenAddr; + _receipt.ronin.chainId = block.chainid; + _receipt.mainchain.addr = _request.recipientAddr; + _receipt.mainchain.tokenAddr = _mainchainTokenAddr; + _receipt.mainchain.chainId = _mainchainId; + _receipt.info = _request.info; + } + + struct Receipt { + uint256 id; + Kind kind; + Token.Owner mainchain; + Token.Owner ronin; + Token.Info info; + } + + // keccak256("Receipt(uint256 id,uint8 kind,TokenOwner mainchain,TokenOwner ronin,TokenInfo info)TokenInfo(uint8 erc,uint256 id,uint256 quantity)TokenOwner(address addr,address tokenAddr,uint256 chainId)"); + bytes32 public constant TYPE_HASH = 0xb9d1fe7c9deeec5dc90a2f47ff1684239519f2545b2228d3d91fb27df3189eea; + + /** + * @dev Returns token info struct hash. + */ + function hash(Receipt memory _receipt) internal pure returns (bytes32) { + return + keccak256( + abi.encode( + TYPE_HASH, + _receipt.id, + _receipt.kind, + Token.hash(_receipt.mainchain), + Token.hash(_receipt.ronin), + Token.hash(_receipt.info) + ) + ); + } + + /** + * @dev Returns the receipt digest. + */ + function receiptDigest(bytes32 _domainSeparator, bytes32 _receiptHash) internal pure returns (bytes32) { + return _domainSeparator.toTypedDataHash(_receiptHash); + } +} diff --git a/contracts/mainchain/MainchainGatewayV2.sol b/contracts/mainchain/MainchainGatewayV2.sol new file mode 100644 index 000000000..0813abc7e --- /dev/null +++ b/contracts/mainchain/MainchainGatewayV2.sol @@ -0,0 +1,453 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/access/AccessControlEnumerable.sol"; +import "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import "../extensions/GatewayV2.sol"; +import "../extensions/WithdrawalLimitation.sol"; +import "../libraries/Transfer.sol"; +import "../interfaces/IMainchainGatewayV2.sol"; + +contract MainchainGatewayV2 is WithdrawalLimitation, Initializable, AccessControlEnumerable, IMainchainGatewayV2 { + using Token for Token.Info; + using Transfer for Transfer.Request; + using Transfer for Transfer.Receipt; + + /// @dev Emitted when the bridge operators are replaced + event BridgeOperatorsReplaced(address[] operators); + + /// @dev Withdrawal unlocker role hash + bytes32 public constant WITHDRAWAL_UNLOCKER_ROLE = keccak256("WITHDRAWAL_UNLOCKER_ROLE"); + + /// @dev Wrapped native token address + IWETH public wrappedNativeToken; + /// @dev Ronin network id + uint256 public roninChainId; + /// @dev Total deposit + uint256 public depositCount; + /// @dev Domain seperator + bytes32 internal _domainSeparator; + /// @dev Mapping from mainchain token => token address on Ronin network + mapping(address => MappedToken) internal _roninToken; + /// @dev Mapping from withdrawal id => withdrawal hash + mapping(uint256 => bytes32) public withdrawalHash; + /// @dev Mapping from withdrawal id => locked + mapping(uint256 => bool) public withdrawalLocked; + + /// @dev Mapping from validator address => last block that the bridge operator is added + mapping(address => uint256) internal _bridgeOperatorAddedBlock; + /// @dev Bridge operators array + address[] internal _bridgeOperators; + + fallback() external payable { + _fallback(); + } + + receive() external payable { + _fallback(); + } + + /** + * @dev Initializes contract storage. + */ + function initialize( + address _roleSetter, + IWETH _wrappedToken, + uint256 _roninChainId, + uint256 _numerator, + uint256 _highTierVWNumerator, + uint256 _denominator, + // _addresses[0]: mainchainTokens + // _addresses[1]: roninTokens + // _addresses[2]: withdrawalUnlockers + address[][3] calldata _addresses, + // _thresholds[0]: highTierThreshold + // _thresholds[1]: lockedThreshold + // _thresholds[2]: unlockFeePercentages + // _thresholds[3]: dailyWithdrawalLimit + uint256[][4] calldata _thresholds, + Token.Standard[] calldata _standards + ) external payable virtual initializer { + _setupRole(DEFAULT_ADMIN_ROLE, _roleSetter); + roninChainId = _roninChainId; + + _setWrappedNativeTokenContract(_wrappedToken); + _updateDomainSeparator(); + _setThreshold(_numerator, _denominator); + _setHighTierVoteWeightThreshold(_highTierVWNumerator, _denominator); + _verifyThresholds(); + + if (_addresses[0].length > 0) { + // Map mainchain tokens to ronin tokens + _mapTokens(_addresses[0], _addresses[1], _standards); + // Sets thresholds based on the mainchain tokens + _setHighTierThresholds(_addresses[0], _thresholds[0]); + _setLockedThresholds(_addresses[0], _thresholds[1]); + _setUnlockFeePercentages(_addresses[0], _thresholds[2]); + _setDailyWithdrawalLimits(_addresses[0], _thresholds[3]); + } + + // Grant role for withdrawal unlocker + for (uint256 _i; _i < _addresses[2].length; _i++) { + _grantRole(WITHDRAWAL_UNLOCKER_ROLE, _addresses[2][_i]); + } + } + + /** + * @dev See {IBridge-replaceBridgeOperators}. + */ + function replaceBridgeOperators(address[] calldata _list) external onlyAdmin { + address _addr; + for (uint256 _i = 0; _i < _list.length; _i++) { + _addr = _list[_i]; + if (_bridgeOperatorAddedBlock[_addr] == 0) { + _bridgeOperators.push(_addr); + } + _bridgeOperatorAddedBlock[_addr] = block.number; + } + + { + uint256 _i; + while (_i < _bridgeOperators.length) { + _addr = _bridgeOperators[_i]; + if (_bridgeOperatorAddedBlock[_addr] < block.number) { + delete _bridgeOperatorAddedBlock[_addr]; + _bridgeOperators[_i] = _bridgeOperators[_bridgeOperators.length - 1]; + _bridgeOperators.pop(); + continue; + } + _i++; + } + } + + emit BridgeOperatorsReplaced(_list); + } + + /** + * @dev See {IBridge-getBridgeOperators}. + */ + function getBridgeOperators() external view returns (address[] memory) { + return _bridgeOperators; + } + + /** + * @dev Receives ether without doing anything. Use this function to topup native token. + */ + function receiveEther() external payable {} + + /** + * @dev See {IMainchainGatewayV2-DOMAIN_SEPARATOR}. + */ + function DOMAIN_SEPARATOR() external view virtual returns (bytes32) { + return _domainSeparator; + } + + /** + * @dev See {IMainchainGatewayV2-setWrappedNativeTokenContract}. + */ + function setWrappedNativeTokenContract(IWETH _wrappedToken) external virtual onlyAdmin { + _setWrappedNativeTokenContract(_wrappedToken); + } + + /** + * @dev See {IMainchainGatewayV2-requestDepositFor}. + */ + function requestDepositFor(Transfer.Request calldata _request) external payable virtual whenNotPaused { + _requestDepositFor(_request, msg.sender); + } + + /** + * @dev See {IMainchainGatewayV2-submitWithdrawal}. + */ + function submitWithdrawal(Transfer.Receipt calldata _receipt, Signature[] calldata _signatures) + external + virtual + whenNotPaused + returns (bool _locked) + { + return _submitWithdrawal(_receipt, _signatures); + } + + /** + * @dev See {IMainchainGatewayV2-unlockWithdrawal}. + */ + function unlockWithdrawal(Transfer.Receipt calldata _receipt) external onlyRole(WITHDRAWAL_UNLOCKER_ROLE) { + bytes32 _receiptHash = _receipt.hash(); + require(withdrawalHash[_receipt.id] == _receipt.hash(), "MainchainGatewayV2: invalid receipt"); + require(withdrawalLocked[_receipt.id], "MainchainGatewayV2: query for approved withdrawal"); + delete withdrawalLocked[_receipt.id]; + emit WithdrawalUnlocked(_receiptHash, _receipt); + + address _token = _receipt.mainchain.tokenAddr; + if (_receipt.info.erc == Token.Standard.ERC20) { + Token.Info memory _feeInfo = _receipt.info; + _feeInfo.quantity = _computeFeePercentage(_receipt.info.quantity, unlockFeePercentages[_token]); + Token.Info memory _withdrawInfo = _receipt.info; + _withdrawInfo.quantity = _receipt.info.quantity - _feeInfo.quantity; + + _feeInfo.handleAssetTransfer(payable(msg.sender), _token, wrappedNativeToken); + _withdrawInfo.handleAssetTransfer(payable(_receipt.mainchain.addr), _token, wrappedNativeToken); + } else { + _receipt.info.handleAssetTransfer(payable(_receipt.mainchain.addr), _token, wrappedNativeToken); + } + + emit Withdrew(_receiptHash, _receipt); + } + + /** + * @dev See {IMainchainGatewayV2-mapTokens}. + */ + function mapTokens( + address[] calldata _mainchainTokens, + address[] calldata _roninTokens, + Token.Standard[] calldata _standards + ) external virtual onlyAdmin { + require(_mainchainTokens.length > 0, "MainchainGatewayV2: query for empty array"); + _mapTokens(_mainchainTokens, _roninTokens, _standards); + } + + /** + * @dev See {IMainchainGatewayV2-mapTokensAndThresholds}. + */ + function mapTokensAndThresholds( + address[] calldata _mainchainTokens, + address[] calldata _roninTokens, + Token.Standard[] calldata _standards, + // _thresholds[0]: highTierThreshold + // _thresholds[1]: lockedThreshold + // _thresholds[2]: unlockFeePercentages + // _thresholds[3]: dailyWithdrawalLimit + uint256[][4] calldata _thresholds + ) external virtual onlyAdmin { + require(_mainchainTokens.length > 0, "MainchainGatewayV2: query for empty array"); + _mapTokens(_mainchainTokens, _roninTokens, _standards); + _setHighTierThresholds(_mainchainTokens, _thresholds[0]); + _setLockedThresholds(_mainchainTokens, _thresholds[1]); + _setUnlockFeePercentages(_mainchainTokens, _thresholds[2]); + _setDailyWithdrawalLimits(_mainchainTokens, _thresholds[3]); + } + + /** + * @dev See {IMainchainGatewayV2-getRoninToken}. + */ + function getRoninToken(address _mainchainToken) public view returns (MappedToken memory _token) { + _token = _roninToken[_mainchainToken]; + require(_token.tokenAddr != address(0), "MainchainGatewayV2: unsupported token"); + } + + /** + * @dev Maps mainchain tokens to Ronin network. + * + * Requirement: + * - The arrays have the same length. + * + * Emits the `TokenMapped` event. + * + */ + function _mapTokens( + address[] calldata _mainchainTokens, + address[] calldata _roninTokens, + Token.Standard[] calldata _standards + ) internal virtual { + require( + _mainchainTokens.length == _roninTokens.length && _mainchainTokens.length == _standards.length, + "MainchainGatewayV2: invalid array length" + ); + + for (uint256 _i; _i < _mainchainTokens.length; _i++) { + _roninToken[_mainchainTokens[_i]].tokenAddr = _roninTokens[_i]; + _roninToken[_mainchainTokens[_i]].erc = _standards[_i]; + } + + emit TokenMapped(_mainchainTokens, _roninTokens, _standards); + } + + /** + * @dev Submits withdrawal receipt. + * + * Requirements: + * - The receipt kind is withdrawal. + * - The receipt is to withdraw on this chain. + * - The receipt is not used to withdraw before. + * - The withdrawal is not reached the limit threshold. + * - The signer weight total is larger than or equal to the minimum threshold. + * - The signature signers are in order. + * + * Emits the `Withdrew` once the assets are released. + * + */ + function _submitWithdrawal(Transfer.Receipt calldata _receipt, Signature[] memory _signatures) + internal + virtual + returns (bool _locked) + { + uint256 _id = _receipt.id; + uint256 _quantity = _receipt.info.quantity; + address _tokenAddr = _receipt.mainchain.tokenAddr; + + _receipt.info.validate(); + require(_receipt.kind == Transfer.Kind.Withdrawal, "MainchainGatewayV2: invalid receipt kind"); + require(_receipt.mainchain.chainId == block.chainid, "MainchainGatewayV2: invalid chain id"); + MappedToken memory _token = getRoninToken(_receipt.mainchain.tokenAddr); + require( + _token.erc == _receipt.info.erc && _token.tokenAddr == _receipt.ronin.tokenAddr, + "MainchainGatewayV2: invalid receipt" + ); + require(withdrawalHash[_id] == bytes32(0), "MainchainGatewayV2: query for processed withdrawal"); + require( + _receipt.info.erc == Token.Standard.ERC721 || !_reachedWithdrawalLimit(_tokenAddr, _quantity), + "MainchainGatewayV2: reached daily withdrawal limit" + ); + + bytes32 _receiptHash = _receipt.hash(); + bytes32 _receiptDigest = Transfer.receiptDigest(_domainSeparator, _receiptHash); + + uint256 _minimumVoteWeight; + (_minimumVoteWeight, _locked) = _computeMinVoteWeight(_receipt.info.erc, _tokenAddr, _quantity); + + { + bool _passed; + address _signer; + address _lastSigner; + Signature memory _sig; + uint256 _weight; + for (uint256 _i; _i < _signatures.length; _i++) { + _sig = _signatures[_i]; + _signer = ecrecover(_receiptDigest, _sig.v, _sig.r, _sig.s); + require(_lastSigner < _signer, "MainchainGatewayV2: invalid order"); + _lastSigner = _signer; + + _weight += _getWeight(_signer); + if (_weight >= _minimumVoteWeight) { + _passed = true; + break; + } + } + require(_passed, "MainchainGatewayV2: query for insufficient vote weight"); + withdrawalHash[_id] = _receiptHash; + } + + if (_locked) { + withdrawalLocked[_id] = true; + emit WithdrawalLocked(_receiptHash, _receipt); + return _locked; + } + + _recordWithdrawal(_tokenAddr, _quantity); + _receipt.info.handleAssetTransfer(payable(_receipt.mainchain.addr), _tokenAddr, wrappedNativeToken); + emit Withdrew(_receiptHash, _receipt); + } + + /** + * @dev Requests deposit made by `_requester` address. + * + * Requirements: + * - The token info is valid. + * - The `msg.value` is 0 while depositing ERC20 token. + * - The `msg.value` is equal to deposit quantity while depositing native token. + * + * Emits the `DepositRequested` event. + * + */ + function _requestDepositFor(Transfer.Request memory _request, address _requester) internal virtual { + MappedToken memory _token; + address _weth = address(wrappedNativeToken); + + _request.info.validate(); + if (_request.tokenAddr == address(0)) { + require(_request.info.quantity == msg.value, "MainchainGatewayV2: invalid request"); + _token = getRoninToken(_weth); + require(_token.erc == _request.info.erc, "MainchainGatewayV2: invalid token standard"); + _request.tokenAddr = _weth; + } else { + require(msg.value == 0, "MainchainGatewayV2: invalid request"); + _token = getRoninToken(_request.tokenAddr); + require(_token.erc == _request.info.erc, "MainchainGatewayV2: invalid token standard"); + _request.info.transferFrom(_requester, address(this), _request.tokenAddr); + // Withdraw if token is WETH + if (_weth == _request.tokenAddr) { + IWETH(_weth).withdraw(_request.info.quantity); + } + } + + uint256 _depositId = depositCount++; + Transfer.Receipt memory _receipt = _request.into_deposit_receipt( + _requester, + _depositId, + _token.tokenAddr, + roninChainId + ); + + emit DepositRequested(_receipt.hash(), _receipt); + } + + /** + * @dev Returns the minimum vote weight for the token. + */ + function _computeMinVoteWeight( + Token.Standard _erc, + address _token, + uint256 _quantity + ) internal virtual returns (uint256 _weight, bool _locked) { + uint256 _totalWeight = _getTotalWeight(); + _weight = _minimumVoteWeight(_totalWeight); + if (_erc == Token.Standard.ERC20) { + if (highTierThreshold[_token] <= _quantity) { + _weight = _highTierVoteWeight(_totalWeight); + } + _locked = _lockedWithdrawalRequest(_token, _quantity); + } + } + + /** + * @dev Update domain seperator. + */ + function _updateDomainSeparator() internal { + _domainSeparator = keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256("MainchainGatewayV2"), + keccak256("2"), + block.chainid, + address(this) + ) + ); + } + + /** + * @dev Sets the WETH contract. + * + * Emits the `WrappedNativeTokenContractUpdated` event. + * + */ + function _setWrappedNativeTokenContract(IWETH _wrapedToken) internal { + wrappedNativeToken = _wrapedToken; + emit WrappedNativeTokenContractUpdated(_wrapedToken); + } + + /** + * @dev Receives ETH from WETH or creates deposit request. + */ + function _fallback() internal virtual whenNotPaused { + if (msg.sender != address(wrappedNativeToken)) { + Transfer.Request memory _request; + _request.recipientAddr = msg.sender; + _request.info.quantity = msg.value; + _requestDepositFor(_request, _request.recipientAddr); + } + } + + /** + * @inheritdoc GatewayV2 + */ + function _getTotalWeight() internal view override returns (uint256) { + return _bridgeOperators.length; + } + + /** + * @dev Returns the weight of an address. + */ + function _getWeight(address _addr) internal view returns (uint256) { + return _bridgeOperatorAddedBlock[_addr] > 0 ? 1 : 0; + } +} diff --git a/contracts/mainchain/MainchainGovernanceAdmin.sol b/contracts/mainchain/MainchainGovernanceAdmin.sol index 764b5e439..a39a8b5a4 100644 --- a/contracts/mainchain/MainchainGovernanceAdmin.sol +++ b/contracts/mainchain/MainchainGovernanceAdmin.sol @@ -69,7 +69,7 @@ contract MainchainGovernanceAdmin is AccessControlEnumerable, GovernanceRelay, G */ function relayBridgeOperators( uint256 _period, - WeightedAddress[] calldata _operators, + address[] calldata _operators, Signature[] calldata _signatures ) external onlyRole(RELAYER_ROLE) { _relayVotesBySignatures(_operators, _signatures, _period, _getMinimumVoteWeight(), DOMAIN_SEPARATOR); @@ -77,23 +77,32 @@ contract MainchainGovernanceAdmin is AccessControlEnumerable, GovernanceRelay, G } /** - * @dev Override {CoreGovernance-_getWeights}. + * @inheritdoc GovernanceRelay */ - function _getWeights(address[] memory _governors) - internal - view - virtual - override(BOsGovernanceRelay, GovernanceRelay) - returns (uint256) - { + function _sumWeights(address[] memory _governors) internal view virtual override returns (uint256) { (bool _success, bytes memory _returndata) = roninTrustedOrganizationContract().staticcall( abi.encodeWithSelector( // TransparentUpgradeableProxyV2.functionDelegateCall.selector, 0x4bb5274a, - abi.encodeWithSelector(IRoninTrustedOrganization.sumWeights.selector, _governors) + abi.encodeWithSelector(IRoninTrustedOrganization.sumGovernorWeights.selector, _governors) ) ); - require(_success, "GovernanceAdmin: proxy call `sumWeights(address[])` failed"); + require(_success, "MainchainGovernanceAdmin: proxy call `sumGovernorWeights(address[])` failed"); + return abi.decode(_returndata, (uint256)); + } + + /** + * @inheritdoc BOsGovernanceRelay + */ + function _sumBridgeVoterWeights(address[] memory _governors) internal view virtual override returns (uint256) { + (bool _success, bytes memory _returndata) = roninTrustedOrganizationContract().staticcall( + abi.encodeWithSelector( + // TransparentUpgradeableProxyV2.functionDelegateCall.selector, + 0x4bb5274a, + abi.encodeWithSelector(IRoninTrustedOrganization.sumBridgeVoterWeights.selector, _governors) + ) + ); + require(_success, "MainchainGovernanceAdmin: proxy call `sumBridgeVoterWeights(address[])` failed"); return abi.decode(_returndata, (uint256)); } } diff --git a/contracts/mocks/MockBridge.sol b/contracts/mocks/MockBridge.sol index 7e18ae5ae..67ac0bd62 100644 --- a/contracts/mocks/MockBridge.sol +++ b/contracts/mocks/MockBridge.sol @@ -5,9 +5,9 @@ pragma solidity ^0.8.9; import "../interfaces/IBridge.sol"; contract MockBridge is IBridge { - WeightedAddress[] public bridgeOperators; + address[] public bridgeOperators; - function replaceBridgeOperators(WeightedAddress[] calldata _list) external override { + function replaceBridgeOperators(address[] calldata _list) external override { while (bridgeOperators.length > 0) { bridgeOperators.pop(); } @@ -16,7 +16,7 @@ contract MockBridge is IBridge { } } - function getBridgeOperators() external view override returns (WeightedAddress[] memory) { + function getBridgeOperators() external view override returns (address[] memory) { return bridgeOperators; } } diff --git a/contracts/multi-chains/RoninTrustedOrganization.sol b/contracts/multi-chains/RoninTrustedOrganization.sol index d4803c178..744879295 100644 --- a/contracts/multi-chains/RoninTrustedOrganization.sol +++ b/contracts/multi-chains/RoninTrustedOrganization.sol @@ -2,33 +2,45 @@ pragma solidity ^0.8.9; +import "@openzeppelin/contracts/utils/Strings.sol"; import "@openzeppelin/contracts/proxy/utils/Initializable.sol"; -import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import "../interfaces/IRoninTrustedOrganization.sol"; import "../extensions/collections/HasProxyAdmin.sol"; contract RoninTrustedOrganization is IRoninTrustedOrganization, HasProxyAdmin, Initializable { - using EnumerableSet for EnumerableSet.AddressSet; - uint256 internal _num; uint256 internal _denom; - uint256 internal _totalWeights; + uint256 internal _totalWeight; uint256 internal _nonce; - /// @dev Address set of the trusted organizations - EnumerableSet.AddressSet internal _orgs; - /// @dev Mapping from trusted organization address => its weight - mapping(address => uint256) internal _weight; + /// @dev Mapping from consensus address => weight + mapping(address => uint256) internal _consensusWeight; + /// @dev Mapping from governor address => weight + mapping(address => uint256) internal _governorWeight; + /// @dev Mapping from bridge voter address => weight + mapping(address => uint256) internal _bridgeVoterWeight; + + /// @dev Mapping from consensus address => added block + mapping(address => uint256) internal _addedBlock; + + /// @dev Consensus array + address[] internal _consensusList; + /// @dev Governors array + address[] internal _governorList; + /// @dev Bridge voters array + address[] internal _bridgeVoterList; /** * @dev Initializes the contract storage. */ function initialize( - WeightedAddress[] calldata _trustedOrgs, + TrustedOrganization[] calldata _trustedOrgs, uint256 __num, uint256 __denom ) external initializer { - _addTrustedOrganizations(_trustedOrgs); + if (_trustedOrgs.length > 0) { + _addTrustedOrganizations(_trustedOrgs); + } _setThreshold(__num, __denom); } @@ -43,14 +55,14 @@ contract RoninTrustedOrganization is IRoninTrustedOrganization, HasProxyAdmin, I * @inheritdoc IQuorum */ function checkThreshold(uint256 _voteWeight) external view virtual returns (bool) { - return _voteWeight * _denom >= _num * _totalWeights; + return _voteWeight * _denom >= _num * _totalWeight; } /** * @inheritdoc IQuorum */ function minimumVoteWeight() external view virtual returns (uint256) { - return (_num * _totalWeights + _denom - 1) / _denom; + return (_num * _totalWeight + _denom - 1) / _denom; } /** @@ -68,115 +80,339 @@ contract RoninTrustedOrganization is IRoninTrustedOrganization, HasProxyAdmin, I /** * @inheritdoc IRoninTrustedOrganization */ - function addTrustedOrganizations(WeightedAddress[] calldata _list) external override onlyAdmin { + function addTrustedOrganizations(TrustedOrganization[] calldata _list) external override onlyAdmin { _addTrustedOrganizations(_list); } /** * @inheritdoc IRoninTrustedOrganization */ - function updateTrustedOrganizations(WeightedAddress[] calldata _list) external override onlyAdmin { - WeightedAddress memory _item; - for (uint _i = 0; _i < _list.length; _i++) { - _item = _list[_i]; - - if (_orgs.contains(_item.addr) && _item.weight > 0) { - _totalWeights -= _weight[_item.addr]; - _totalWeights += _item.weight; - _weight[_item.addr] = _item.weight; - emit TrustedOrganizationUpdated(_item); - } + function updateTrustedOrganizations(TrustedOrganization[] calldata _list) external override onlyAdmin { + require(_list.length > 0, "RoninTrustedOrganization: invalid array length"); + for (uint256 _i; _i < _list.length; _i++) { + _updateTrustedOrganization(_list[_i]); } + emit TrustedOrganizationsUpdated(_list); } /** * @inheritdoc IRoninTrustedOrganization */ function removeTrustedOrganizations(address[] calldata _list) external override onlyAdmin { + require(_list.length > 0, "RoninTrustedOrganization: invalid array length"); for (uint _i = 0; _i < _list.length; _i++) { - if (_orgs.remove(_list[_i])) { - _totalWeights -= _weight[_list[_i]]; - delete _weight[_list[_i]]; - emit TrustedOrganizationRemoved(_list[_i]); - } + _removeTrustedOrganization(_list[_i]); } + emit TrustedOrganizationsRemoved(_list); } /** * @inheritdoc IRoninTrustedOrganization */ function totalWeights() external view virtual returns (uint256) { - return _totalWeights; + return _totalWeight; + } + + /** + * @inheritdoc IRoninTrustedOrganization + */ + function getConsensusWeight(address _consensusAddr) external view returns (uint256) { + return _consensusWeight[_consensusAddr]; } /** * @inheritdoc IRoninTrustedOrganization */ - function getWeight(address _addr) external view override returns (uint256) { - return _weight[_addr]; + function getGovernorWeight(address _governor) external view returns (uint256) { + return _governorWeight[_governor]; + } + + /** + * @inheritdoc IRoninTrustedOrganization + */ + function getBridgeVoterWeight(address _addr) external view returns (uint256) { + return _bridgeVoterWeight[_addr]; + } + + /** + * @inheritdoc IRoninTrustedOrganization + */ + function getConsensusWeights(address[] calldata _list) external view returns (uint256[] memory _res) { + _res = new uint256[](_list.length); + for (uint _i = 0; _i < _res.length; _i++) { + _res[_i] = _consensusWeight[_list[_i]]; + } } /** * @inheritdoc IRoninTrustedOrganization */ - function getWeights(address[] calldata _list) external view override returns (uint256[] memory _res) { + function getGovernorWeights(address[] calldata _list) external view returns (uint256[] memory _res) { _res = new uint256[](_list.length); for (uint _i = 0; _i < _res.length; _i++) { - _res[_i] = _weight[_list[_i]]; + _res[_i] = _governorWeight[_list[_i]]; } } /** * @inheritdoc IRoninTrustedOrganization */ - function sumWeights(address[] calldata _list) external view override returns (uint256 _res) { + function getBridgeVoterWeights(address[] calldata _list) external view returns (uint256[] memory _res) { + _res = new uint256[](_list.length); + for (uint _i = 0; _i < _res.length; _i++) { + _res[_i] = _bridgeVoterWeight[_list[_i]]; + } + } + + /** + * @inheritdoc IRoninTrustedOrganization + */ + function sumConsensusWeights(address[] calldata _list) external view returns (uint256 _res) { for (uint _i = 0; _i < _list.length; _i++) { - _res += _weight[_list[_i]]; + _res += _consensusWeight[_list[_i]]; } } /** * @inheritdoc IRoninTrustedOrganization */ - function getTrustedOrganizationAt(uint256 _idx) external view override returns (WeightedAddress memory _res) { - _res.addr = _orgs.at(_idx); - _res.weight = _weight[_res.addr]; + function sumGovernorWeights(address[] calldata _list) external view returns (uint256 _res) { + for (uint _i = 0; _i < _list.length; _i++) { + _res += _governorWeight[_list[_i]]; + } + } + + /** + * @inheritdoc IRoninTrustedOrganization + */ + function sumBridgeVoterWeights(address[] calldata _list) external view returns (uint256 _res) { + for (uint _i = 0; _i < _list.length; _i++) { + _res += _bridgeVoterWeight[_list[_i]]; + } } /** * @inheritdoc IRoninTrustedOrganization */ function countTrustedOrganizations() external view override returns (uint256) { - return _orgs.length(); + return _consensusList.length; } /** * @inheritdoc IRoninTrustedOrganization */ - function getAllTrustedOrganizations() external view override returns (WeightedAddress[] memory _res) { - address[] memory _list = _orgs.values(); - _res = new WeightedAddress[](_list.length); - for (uint _i = 0; _i < _res.length; _i++) { - _res[_i].addr = _list[_i]; - _res[_i].weight = _weight[_list[_i]]; + function getAllTrustedOrganizations() external view override returns (TrustedOrganization[] memory _list) { + _list = new TrustedOrganization[](_consensusList.length); + address _addr; + for (uint256 _i; _i < _list.length; _i++) { + _addr = _consensusList[_i]; + _list[_i].consensusAddr = _addr; + _list[_i].governor = _governorList[_i]; + _list[_i].bridgeVoter = _bridgeVoterList[_i]; + _list[_i].weight = _consensusWeight[_addr]; } } /** - * @dev Adds a list of addresses into the trusted organization. + * @inheritdoc IRoninTrustedOrganization */ - function _addTrustedOrganizations(WeightedAddress[] calldata _list) internal { - for (uint _i = 0; _i < _list.length; _i++) { - if (_list[_i].weight > 0) { - if (_orgs.add(_list[_i].addr)) { - _totalWeights += _list[_i].weight; - _weight[_list[_i].addr] = _list[_i].weight; - emit TrustedOrganizationAdded(_list[_i]); + function getTrustedOrganization(address _consensusAddr) external view returns (TrustedOrganization memory) { + for (uint _i = 0; _i < _consensusList.length; _i++) { + if (_consensusList[_i] == _consensusAddr) { + return getTrustedOrganizationAt(_i); + } + } + revert("RoninTrustedOrganization: query for non-existent consensus address"); + } + + /** + * @inheritdoc IRoninTrustedOrganization + */ + function getTrustedOrganizationAt(uint256 _idx) public view override returns (TrustedOrganization memory) { + address _addr = _consensusList[_idx]; + return + TrustedOrganization( + _addr, + _governorList[_idx], + _bridgeVoterList[_idx], + _consensusWeight[_addr], + _addedBlock[_addr] + ); + } + + /** + * @dev Adds a list of trusted organizations. + */ + function _addTrustedOrganizations(TrustedOrganization[] calldata _list) internal virtual { + for (uint256 _i; _i < _list.length; _i++) { + _addTrustedOrganization(_list[_i]); + } + emit TrustedOrganizationsAdded(_list); + } + + /** + * @dev Adds a trusted organization. + * + * Requirements: + * - The weight is larger than 0. + * - The consensus address is not added. + * - The govenor address is not added. + * - The bridge voter address is not added. + * + */ + function _addTrustedOrganization(TrustedOrganization memory _v) internal virtual { + require(_v.addedBlock == 0, "RoninTrustedOrganization: invalid request"); + require(_v.weight > 0, "RoninTrustedOrganization: invalid weight"); + + if (_consensusWeight[_v.consensusAddr] > 0) { + revert( + string( + abi.encodePacked( + "RoninTrustedOrganization: consensus address ", + Strings.toHexString(uint160(_v.consensusAddr), 20), + " is added already" + ) + ) + ); + } + + if (_governorWeight[_v.governor] > 0) { + revert( + string( + abi.encodePacked( + "RoninTrustedOrganization: govenor address ", + Strings.toHexString(uint160(_v.governor), 20), + " is added already" + ) + ) + ); + } + + if (_bridgeVoterWeight[_v.bridgeVoter] > 0) { + revert( + string( + abi.encodePacked( + "RoninTrustedOrganization: bridge voter address ", + Strings.toHexString(uint160(_v.bridgeVoter), 20), + " is added already" + ) + ) + ); + } + + _consensusList.push(_v.consensusAddr); + _consensusWeight[_v.consensusAddr] = _v.weight; + + _governorList.push(_v.governor); + _governorWeight[_v.governor] = _v.weight; + + _bridgeVoterList.push(_v.bridgeVoter); + _bridgeVoterWeight[_v.bridgeVoter] = _v.weight; + + _addedBlock[_v.consensusAddr] = block.number; + + _totalWeight += _v.weight; + } + + /** + * @dev Updates a trusted organization. + * + * Requirements: + * - The weight is larger than 0. + * - The consensus address is already added. + * + */ + function _updateTrustedOrganization(TrustedOrganization memory _v) internal virtual { + require(_v.weight > 0, "RoninTrustedOrganization: invalid weight"); + + uint256 _weight = _consensusWeight[_v.consensusAddr]; + if (_weight == 0) { + revert( + string( + abi.encodePacked( + "RoninTrustedOrganization: consensus address ", + Strings.toHexString(uint160(_v.consensusAddr), 20), + " is not added" + ) + ) + ); + } + + uint256 _count = _consensusList.length; + for (uint256 _i = 0; _i < _count; _i++) { + if (_consensusList[_i] == _v.consensusAddr) { + _totalWeight -= _weight; + _totalWeight += _v.weight; + + if (_governorList[_i] != _v.governor) { + require(_governorWeight[_v.governor] == 0, "RoninTrustedOrganization: query for duplicated governor"); + delete _governorWeight[_governorList[_i]]; + _governorList[_i] = _v.governor; + } + + if (_bridgeVoterList[_i] != _v.bridgeVoter) { + require( + _bridgeVoterWeight[_v.bridgeVoter] == 0, + "RoninTrustedOrganization: query for duplicated bridge voter" + ); + delete _bridgeVoterWeight[_bridgeVoterList[_i]]; + _bridgeVoterList[_i] = _v.governor; } + + _consensusWeight[_v.consensusAddr] = _v.weight; + _governorWeight[_v.governor] = _v.weight; + _bridgeVoterWeight[_v.bridgeVoter] = _v.weight; + return; } } } + /** + * @dev Removes a trusted organization. + * + * Requirements: + * - The consensus address is added. + * + */ + function _removeTrustedOrganization(address _addr) internal virtual { + uint256 _weight = _consensusWeight[_addr]; + if (_weight == 0) { + revert( + string( + abi.encodePacked( + "RoninTrustedOrganization: consensus address ", + Strings.toHexString(uint160(_addr), 20), + " is not added" + ) + ) + ); + } + + uint256 _index; + uint256 _count = _consensusList.length; + for (uint256 _i = 0; _i < _count; _i++) { + if (_consensusList[_i] == _addr) { + _index = _i; + break; + } + } + + _totalWeight -= _weight; + + delete _addedBlock[_addr]; + delete _consensusWeight[_addr]; + _consensusList[_index] = _consensusList[_count - 1]; + _consensusList.pop(); + + delete _governorWeight[_governorList[_index]]; + _governorList[_index] = _governorList[_count - 1]; + _governorList.pop(); + + delete _bridgeVoterWeight[_bridgeVoterList[_index]]; + _bridgeVoterList[_index] = _bridgeVoterList[_count - 1]; + _bridgeVoterList.pop(); + } + /** * @dev Sets threshold and returns the old one. * diff --git a/contracts/ronin/RoninGatewayV2.sol b/contracts/ronin/RoninGatewayV2.sol new file mode 100644 index 000000000..3b007242a --- /dev/null +++ b/contracts/ronin/RoninGatewayV2.sol @@ -0,0 +1,438 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/access/AccessControlEnumerable.sol"; +import "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import "../extensions/GatewayV2.sol"; +import "../extensions/isolated-governance/IsolatedGovernance.sol"; +import "../extensions/MinimumWithdrawal.sol"; +import "../interfaces/IERC20Mintable.sol"; +import "../interfaces/IERC721Mintable.sol"; +import "../interfaces/IRoninGatewayV2.sol"; +import "../interfaces/IRoninValidatorSet.sol"; +import "../interfaces/collections/IHasValidatorContract.sol"; + +contract RoninGatewayV2 is + GatewayV2, + IsolatedGovernance, + Initializable, + MinimumWithdrawal, + AccessControlEnumerable, + IRoninGatewayV2, + IHasValidatorContract +{ + using Token for Token.Info; + using Transfer for Transfer.Request; + using Transfer for Transfer.Receipt; + + /// @dev Withdrawal unlocker role hash + bytes32 public constant WITHDRAWAL_MIGRATOR = keccak256("WITHDRAWAL_MIGRATOR"); + + /// @dev Flag indicating whether the withdrawal migrate progress is done + bool public withdrawalMigrated; + /// @dev Total withdrawal + uint256 public withdrawalCount; + /// @dev Mapping from chain id => deposit id => deposit vote + mapping(uint256 => mapping(uint256 => IsolatedVote)) public depositVote; + /// @dev Mapping from withdrawal id => mainchain withdrew vote + mapping(uint256 => IsolatedVote) public mainchainWithdrewVote; + /// @dev Mapping from withdrawal id => withdrawal receipt + mapping(uint256 => Transfer.Receipt) public withdrawal; + /// @dev Mapping from withdrawal id => validator address => signatures + mapping(uint256 => mapping(address => bytes)) internal _withdrawalSig; + /// @dev Mapping from token address => chain id => mainchain token address + mapping(address => mapping(uint256 => MappedToken)) internal _mainchainToken; + + IRoninValidatorSet internal _validatorContract; + + fallback() external payable { + _fallback(); + } + + receive() external payable { + _fallback(); + } + + /** + * @dev Initializes contract storage. + */ + function initialize( + address _roleSetter, + uint256 _numerator, + uint256 _denominator, + address[] calldata _withdrawalMigrators, + // _packedAddresses[0]: roninTokens + // _packedAddresses[1]: mainchainTokens + address[][2] calldata _packedAddresses, + // _packedNumbers[0]: chainIds + // _packedNumbers[1]: minimumThresholds + uint256[][2] calldata _packedNumbers, + Token.Standard[] calldata _standards + ) external virtual initializer { + _setupRole(DEFAULT_ADMIN_ROLE, _roleSetter); + _setThreshold(_numerator, _denominator); + if (_packedAddresses[0].length > 0) { + _mapTokens(_packedAddresses[0], _packedAddresses[1], _packedNumbers[0], _standards); + _setMinimumThresholds(_packedAddresses[0], _packedNumbers[1]); + } + + for (uint256 _i; _i < _withdrawalMigrators.length; _i++) { + _grantRole(WITHDRAWAL_MIGRATOR, _withdrawalMigrators[_i]); + } + } + + /** + * @inheritdoc IHasValidatorContract + */ + function validatorContract() external view returns (address) { + return address(_validatorContract); + } + + /** + * @inheritdoc IHasValidatorContract + */ + function setValidatorContract(address _addr) external override onlyAdmin { + _setValidatorContract(_addr); + } + + /** + * @dev Sets the validator contract. + * + * Requirements: + * - The new address is a contract. + * + * Emits the event `ValidatorContractUpdated`. + * + */ + function _setValidatorContract(address _addr) internal { + _validatorContract = IRoninValidatorSet(_addr); + emit ValidatorContractUpdated(_addr); + } + + /** + * @dev Migrates withdrawals. + * + * Requirements: + * - The method caller is the migrator. + * - The arrays have the same length and its length larger than 0. + * + */ + function migrateWithdrawals(Transfer.Request[] calldata _requests, address[] calldata _requesters) + external + onlyRole(WITHDRAWAL_MIGRATOR) + { + require(!withdrawalMigrated, "RoninGatewayV2: withdrawals migrated"); + require(_requesters.length == _requests.length && _requests.length > 0, "RoninGatewayV2: invalid array lengths"); + for (uint256 _i; _i < _requests.length; _i++) { + MappedToken memory _token = getMainchainToken(_requests[_i].tokenAddr, 1); + require(_requests[_i].info.erc == _token.erc, "RoninGatewayV2: invalid token standard"); + _storeAsReceipt(_requests[_i], 1, _requesters[_i], _token.tokenAddr); + } + } + + /** + * @dev Mark the migration as done. + */ + function markWithdrawalMigrated() external { + require( + hasRole(DEFAULT_ADMIN_ROLE, msg.sender) || hasRole(WITHDRAWAL_MIGRATOR, msg.sender), + "RoninGatewayV2: unauthorized sender" + ); + require(!withdrawalMigrated, "RoninGatewayV2: withdrawals migrated"); + withdrawalMigrated = true; + } + + /** + * @dev {IRoninGatewayV2-getWithdrawalSignatures}. + */ + function getWithdrawalSignatures(uint256 _withdrawalId, address[] calldata _validators) + external + view + returns (bytes[] memory _signatures) + { + _signatures = new bytes[](_validators.length); + for (uint256 _i = 0; _i < _validators.length; _i++) { + _signatures[_i] = _withdrawalSig[_withdrawalId][_validators[_i]]; + } + } + + /** + * @dev {IRoninGatewayV2-depositFor}. + */ + function depositFor(Transfer.Receipt calldata _receipt) external { + address _sender = msg.sender; + uint256 _weight = _getValidatorWeight(_sender); + _depositFor(_receipt, _sender, _weight, minimumVoteWeight()); + } + + /** + * @dev {IRoninGatewayV2-tryBulkAcknowledgeMainchainWithdrew}. + */ + function tryBulkAcknowledgeMainchainWithdrew(uint256[] calldata _withdrawalIds) + external + returns (bool[] memory _executedReceipts) + { + address _governor = msg.sender; + uint256 _weight = _getValidatorWeight(_governor); + uint256 _minVoteWeight = minimumVoteWeight(); + + uint256 _withdrawalId; + _executedReceipts = new bool[](_withdrawalIds.length); + for (uint256 _i; _i < _withdrawalIds.length; _i++) { + _withdrawalId = _withdrawalIds[_i]; + if (mainchainWithdrew(_withdrawalId)) { + _executedReceipts[_i] = true; + } else { + IsolatedVote storage _proposal = mainchainWithdrewVote[_withdrawalId]; + Transfer.Receipt memory _withdrawal = withdrawal[_withdrawalId]; + bytes32 _hash = _withdrawal.hash(); + VoteStatus _status = _castVote(_proposal, _governor, _weight, _minVoteWeight, _hash); + if (_status == VoteStatus.Approved) { + _proposal.status = VoteStatus.Executed; + emit MainchainWithdrew(_hash, _withdrawal); + } + } + } + } + + /** + * @dev {IRoninGatewayV2-tryBulkDepositFor}. + */ + function tryBulkDepositFor(Transfer.Receipt[] calldata _receipts) external returns (bool[] memory _executedReceipts) { + address _sender = msg.sender; + uint256 _weight = _getValidatorWeight(_sender); + + Transfer.Receipt memory _receipt; + _executedReceipts = new bool[](_receipts.length); + uint256 _minVoteWeight = minimumVoteWeight(); + for (uint256 _i; _i < _receipts.length; _i++) { + _receipt = _receipts[_i]; + if (depositVote[_receipt.mainchain.chainId][_receipt.id].status == VoteStatus.Executed) { + _executedReceipts[_i] = true; + } else { + _depositFor(_receipt, _sender, _weight, _minVoteWeight); + } + } + } + + /** + * @dev {IRoninGatewayV2-requestWithdrawalFor}. + */ + function requestWithdrawalFor(Transfer.Request calldata _request, uint256 _chainId) external whenNotPaused { + _requestWithdrawalFor(_request, msg.sender, _chainId); + } + + /** + * @dev {IRoninGatewayV2-bulkRequestWithdrawalFor}. + */ + function bulkRequestWithdrawalFor(Transfer.Request[] calldata _requests, uint256 _chainId) external whenNotPaused { + require(_requests.length > 0, "RoninGatewayV2: empty array"); + for (uint256 _i; _i < _requests.length; _i++) { + _requestWithdrawalFor(_requests[_i], msg.sender, _chainId); + } + } + + /** + * @dev {IRoninGatewayV2-requestWithdrawalSignatures}. + */ + function requestWithdrawalSignatures(uint256 _withdrawalId) external whenNotPaused { + require(!mainchainWithdrew(_withdrawalId), "RoninGatewayV2: withdrew on mainchain already"); + Transfer.Receipt memory _receipt = withdrawal[_withdrawalId]; + require(_receipt.ronin.chainId == block.chainid, "RoninGatewayV2: query for invalid withdrawal"); + emit WithdrawalSignaturesRequested(_receipt.hash(), _receipt); + } + + /** + * @dev {IRoninGatewayV2-bulkSubmitWithdrawalSignatures}. + */ + function bulkSubmitWithdrawalSignatures(uint256[] calldata _withdrawals, bytes[] calldata _signatures) external { + address _validator = msg.sender; + // This checks method caller already + _getValidatorWeight(_validator); + + require( + _withdrawals.length > 0 && _withdrawals.length == _signatures.length, + "RoninGatewayV2: invalid array length" + ); + for (uint256 _i; _i < _withdrawals.length; _i++) { + _withdrawalSig[_withdrawals[_i]][_validator] = _signatures[_i]; + } + } + + /** + * @dev {IRoninGatewayV2-mapTokens}. + */ + function mapTokens( + address[] calldata _roninTokens, + address[] calldata _mainchainTokens, + uint256[] calldata _chainIds, + Token.Standard[] calldata _standards + ) external onlyAdmin { + require(_roninTokens.length > 0, "RoninGatewayV2: invalid array length"); + _mapTokens(_roninTokens, _mainchainTokens, _chainIds, _standards); + } + + /** + * @dev {IRoninGatewayV2-depositVoted}. + */ + function depositVoted( + uint256 _chainId, + uint256 _depositId, + address _voter + ) external view returns (bool) { + return _voted(depositVote[_chainId][_depositId], _voter); + } + + /** + * @dev {IRoninGatewayV2-mainchainWithdrewVoted}. + */ + function mainchainWithdrewVoted(uint256 _withdrawalId, address _voter) external view returns (bool) { + return _voted(mainchainWithdrewVote[_withdrawalId], _voter); + } + + /** + * @dev {IRoninGatewayV2-mainchainWithdrew}. + */ + function mainchainWithdrew(uint256 _withdrawalId) public view returns (bool) { + return mainchainWithdrewVote[_withdrawalId].status == VoteStatus.Executed; + } + + /** + * @dev {IRoninGatewayV2-getMainchainToken}. + */ + function getMainchainToken(address _roninToken, uint256 _chainId) public view returns (MappedToken memory _token) { + _token = _mainchainToken[_roninToken][_chainId]; + require(_token.tokenAddr != address(0), "RoninGatewayV2: unsupported token"); + } + + /** + * @dev Maps Ronin tokens to mainchain networks. + * + * Requirement: + * - The arrays have the same length. + * + * Emits the `TokenMapped` event. + * + */ + function _mapTokens( + address[] calldata _roninTokens, + address[] calldata _mainchainTokens, + uint256[] calldata _chainIds, + Token.Standard[] calldata _standards + ) internal { + require( + _roninTokens.length == _mainchainTokens.length && _roninTokens.length == _chainIds.length, + "RoninGatewayV2: invalid array length" + ); + + for (uint256 _i; _i < _roninTokens.length; _i++) { + _mainchainToken[_roninTokens[_i]][_chainIds[_i]].tokenAddr = _mainchainTokens[_i]; + _mainchainToken[_roninTokens[_i]][_chainIds[_i]].erc = _standards[_i]; + } + + emit TokenMapped(_roninTokens, _mainchainTokens, _chainIds, _standards); + } + + /** + * @dev Deposits based on the receipt. + * + * Emits the `Deposited` once the assets are released. + * + */ + function _depositFor( + Transfer.Receipt memory _receipt, + address _validator, + uint256 _weight, + uint256 _minVoteWeight + ) internal { + uint256 _id = _receipt.id; + _receipt.info.validate(); + require(_receipt.kind == Transfer.Kind.Deposit, "RoninGatewayV2: invalid receipt kind"); + require(_receipt.ronin.chainId == block.chainid, "RoninGatewayV2: invalid chain id"); + MappedToken memory _token = getMainchainToken(_receipt.ronin.tokenAddr, _receipt.mainchain.chainId); + require( + _token.erc == _receipt.info.erc && _token.tokenAddr == _receipt.mainchain.tokenAddr, + "RoninGatewayV2: invalid receipt" + ); + + IsolatedVote storage _proposal = depositVote[_receipt.mainchain.chainId][_id]; + bytes32 _receiptHash = _receipt.hash(); + VoteStatus _status = _castVote(_proposal, _validator, _weight, _minVoteWeight, _receiptHash); + if (_status == VoteStatus.Approved) { + _proposal.status = VoteStatus.Executed; + _receipt.info.handleAssetTransfer(payable(_receipt.ronin.addr), _receipt.ronin.tokenAddr, IWETH(address(0))); + emit Deposited(_receiptHash, _receipt); + } + } + + /** + * @dev Returns the validator weight. + * + * Requirements: + * - The `_addr` weight is larger than 0. + * + */ + function _getValidatorWeight(address _addr) internal view returns (uint256 _weight) { + _weight = _validatorContract.isBridgeOperator(_addr) ? 1 : 0; + require(_weight > 0, "RoninGatewayV2: unauthorized sender"); + } + + /** + * @dev Locks the assets and request withdrawal. + * + * Requirements: + * - The token info is valid. + * + * Emits the `WithdrawalRequested` event. + * + */ + function _requestWithdrawalFor( + Transfer.Request calldata _request, + address _requester, + uint256 _chainId + ) internal { + _request.info.validate(); + _checkWithdrawal(_request); + MappedToken memory _token = getMainchainToken(_request.tokenAddr, _chainId); + require(_request.info.erc == _token.erc, "RoninGatewayV2: invalid token standard"); + _request.info.transferFrom(_requester, address(this), _request.tokenAddr); + _storeAsReceipt(_request, _chainId, _requester, _token.tokenAddr); + } + + /** + * @dev Stores the withdrawal request as a receipt. + * + * Emits the `WithdrawalRequested` event. + * + */ + function _storeAsReceipt( + Transfer.Request calldata _request, + uint256 _chainId, + address _requester, + address _mainchainTokenAddr + ) internal { + uint256 _withdrawalId = withdrawalCount++; + Transfer.Receipt memory _receipt = _request.into_withdrawal_receipt( + _requester, + _withdrawalId, + _mainchainTokenAddr, + _chainId + ); + withdrawal[_withdrawalId] = _receipt; + emit WithdrawalRequested(_receipt.hash(), _receipt); + } + + /** + * @dev Don't send me RON. + */ + function _fallback() internal virtual { + revert("RoninGatewayV2: invalid request"); + } + + /** + * @inheritdoc GatewayV2 + */ + function _getTotalWeight() internal view virtual override returns (uint256) { + return _validatorContract.totalBridgeOperators(); + } +} diff --git a/contracts/ronin/RoninGovernanceAdmin.sol b/contracts/ronin/RoninGovernanceAdmin.sol index 23d422dd6..a47dca42b 100644 --- a/contracts/ronin/RoninGovernanceAdmin.sol +++ b/contracts/ronin/RoninGovernanceAdmin.sol @@ -7,6 +7,9 @@ import "../extensions/GovernanceAdmin.sol"; import "../interfaces/IBridge.sol"; contract RoninGovernanceAdmin is GovernanceAdmin, GovernanceProposal, BOsGovernanceProposal { + /// @dev Emitted when the bridge operators are approved. + event BridgeOperatorsApproved(uint256 _period, address[] _operators); + modifier onlyGovernor() { require(_getWeight(msg.sender) > 0, "GovernanceAdmin: sender is not governor"); _; @@ -175,36 +178,45 @@ contract RoninGovernanceAdmin is GovernanceAdmin, GovernanceProposal, BOsGoverna */ function voteBridgeOperatorsBySignatures( uint256 _period, - WeightedAddress[] calldata _operators, + address[] calldata _operators, Signature[] calldata _signatures ) external { _castVotesBySignatures(_operators, _signatures, _period, _getMinimumVoteWeight(), DOMAIN_SEPARATOR); IsolatedVote storage _v = _vote[_period]; if (_v.status == VoteStatus.Approved) { _lastSyncedPeriod = _period; + emit BridgeOperatorsApproved(_period, _operators); _v.status = VoteStatus.Executed; - _bridgeContract.replaceBridgeOperators(_operators); } } /** - * @dev Override {CoreGovernance-_getWeight}. + * @inheritdoc GovernanceProposal */ - function _getWeight(address _governor) - internal - view - virtual - override(BOsGovernanceProposal, GovernanceProposal) - returns (uint256) - { + function _getWeight(address _governor) internal view virtual override returns (uint256) { + (bool _success, bytes memory _returndata) = roninTrustedOrganizationContract().staticcall( + abi.encodeWithSelector( + // TransparentUpgradeableProxyV2.functionDelegateCall.selector, + 0x4bb5274a, + abi.encodeWithSelector(IRoninTrustedOrganization.getGovernorWeight.selector, _governor) + ) + ); + require(_success, "GovernanceAdmin: proxy call `getGovernorWeight(address)` failed"); + return abi.decode(_returndata, (uint256)); + } + + /** + * @inheritdoc BOsGovernanceProposal + */ + function _getBridgeVoterWeight(address _governor) internal view virtual override returns (uint256) { (bool _success, bytes memory _returndata) = roninTrustedOrganizationContract().staticcall( abi.encodeWithSelector( // TransparentUpgradeableProxyV2.functionDelegateCall.selector, 0x4bb5274a, - abi.encodeWithSelector(IRoninTrustedOrganization.getWeight.selector, _governor) + abi.encodeWithSelector(IRoninTrustedOrganization.getBridgeVoterWeight.selector, _governor) ) ); - require(_success, "GovernanceAdmin: proxy call `getWeight(address)` failed"); + require(_success, "GovernanceAdmin: proxy call `getBridgeVoterWeight(address)` failed"); return abi.decode(_returndata, (uint256)); } } diff --git a/contracts/ronin/SlashIndicator.sol b/contracts/ronin/SlashIndicator.sol index 6a220f5e8..5b5367a36 100644 --- a/contracts/ronin/SlashIndicator.sol +++ b/contracts/ronin/SlashIndicator.sol @@ -6,6 +6,8 @@ import "@openzeppelin/contracts/proxy/utils/Initializable.sol"; import "../interfaces/ISlashIndicator.sol"; import "../extensions/collections/HasValidatorContract.sol"; import "../extensions/collections/HasMaintenanceContract.sol"; +import "../extensions/collections/HasRoninTrustedOrganizationContract.sol"; +import "../extensions/collections/HasRoninGovernanceAdminContract.sol"; import "../libraries/Math.sol"; import "../precompile-usages/PrecompileUsageValidateDoubleSign.sol"; @@ -14,6 +16,8 @@ contract SlashIndicator is PrecompileUsageValidateDoubleSign, HasValidatorContract, HasMaintenanceContract, + HasRoninTrustedOrganizationContract, + HasRoninGovernanceAdminContract, Initializable { using Math for uint256; @@ -33,11 +37,15 @@ contract SlashIndicator is uint256 public misdemeanorThreshold; /// @dev The threshold to slash when validator is unavailability reaches felony uint256 public felonyThreshold; + /// @dev The threshold to slash when a trusted organization does not vote for bridge operators + uint256 public bridgeVotingThreshold; /// @dev The amount of RON to slash felony. uint256 public slashFelonyAmount; /// @dev The amount of RON to slash double sign. uint256 public slashDoubleSignAmount; + /// @dev The amount of RON to slash bridge voting. + uint256 public bridgeVotingSlashAmount; /// @dev The block duration to jail a validator that reaches felony thresold. uint256 public felonyJailDuration; /// @dev The block number that the punished validator will be jailed until, due to double signing. @@ -67,18 +75,26 @@ contract SlashIndicator is function initialize( address __validatorContract, address __maintenanceContract, + address __roninTrustedOrganizationContract, + address __roninGovernanceAdminContract, uint256 _misdemeanorThreshold, uint256 _felonyThreshold, + uint256 _bridgeVotingThreshold, uint256 _slashFelonyAmount, uint256 _slashDoubleSignAmount, + uint256 _bridgeVotingSlashAmount, uint256 _felonyJailBlocks, uint256 _doubleSigningConstrainBlocks ) external initializer { _setValidatorContract(__validatorContract); _setMaintenanceContract(__maintenanceContract); + _setRoninTrustedOrganizationContract(__roninTrustedOrganizationContract); + _setRoninGovernanceAdminContract(__roninGovernanceAdminContract); _setSlashThresholds(_felonyThreshold, _misdemeanorThreshold); + _setBridgeVotingThreshold(_bridgeVotingThreshold); _setSlashFelonyAmount(_slashFelonyAmount); _setSlashDoubleSignAmount(_slashDoubleSignAmount); + _setBridgeVotingSlashAmount(_bridgeVotingSlashAmount); _setFelonyJailDuration(_felonyJailBlocks); _setDoubleSigningConstrainBlocks(_doubleSigningConstrainBlocks); _setDoubleSigningJailUntilBlock(type(uint256).max); @@ -140,6 +156,24 @@ contract SlashIndicator is } } + /** + * @inheritdoc ISlashIndicator + */ + function slashBridgeVoting(address _consensusAddr) external { + IRoninTrustedOrganization.TrustedOrganization memory _org = _roninTrustedOrganizationContract + .getTrustedOrganization(_consensusAddr); + uint256 _lastVotedBlock = Math.max(_roninGovernanceAdminContract.lastVotedBlock(_org.bridgeVoter), _org.addedBlock); + uint256 _period = _validatorContract.periodOf(block.number); + if ( + block.number - _lastVotedBlock > bridgeVotingThreshold && + _unavailabilitySlashed[_consensusAddr][_period] != SlashType.BRIDGE_VOTING + ) { + _unavailabilitySlashed[_consensusAddr][_period] = SlashType.BRIDGE_VOTING; + emit UnavailabilitySlashed(_consensusAddr, SlashType.BRIDGE_VOTING, _period); + _validatorContract.slash(_consensusAddr, 0, bridgeVotingSlashAmount); + } + } + /////////////////////////////////////////////////////////////////////////////////////// // GOVERNANCE FUNCTIONS // /////////////////////////////////////////////////////////////////////////////////////// @@ -172,6 +206,20 @@ contract SlashIndicator is _setFelonyJailDuration(_felonyJailDuration); } + /** + * @inheritdoc ISlashIndicator + */ + function setBridgeVotingThreshold(uint256 _threshold) external override onlyAdmin { + _setBridgeVotingThreshold(_threshold); + } + + /** + * @inheritdoc ISlashIndicator + */ + function setBridgeVotingSlashAmount(uint256 _amount) external override onlyAdmin { + _setBridgeVotingSlashAmount(_amount); + } + /////////////////////////////////////////////////////////////////////////////////////// // QUERY FUNCTIONS // /////////////////////////////////////////////////////////////////////////////////////// @@ -285,6 +333,22 @@ contract SlashIndicator is emit DoubleSigningJailUntilBlockUpdated(_doubleSigningJailUntilBlock); } + /** + * @dev Sets the threshold to slash when trusted organization does not vote for bridge operators. + */ + function _setBridgeVotingThreshold(uint256 _threshold) internal { + bridgeVotingThreshold = _threshold; + emit BridgeVotingThresholdUpdated(_threshold); + } + + /** + * @dev Sets the amount of RON to slash bridge voting. + */ + function _setBridgeVotingSlashAmount(uint256 _amount) internal { + bridgeVotingSlashAmount = _amount; + emit BridgeVotingSlashAmountUpdated(_amount); + } + /** * @dev Sanity check the address to be slashed */ diff --git a/contracts/ronin/validator/RoninValidatorSet.sol b/contracts/ronin/validator/RoninValidatorSet.sol index d63687bb1..0d09a6bce 100644 --- a/contracts/ronin/validator/RoninValidatorSet.sol +++ b/contracts/ronin/validator/RoninValidatorSet.sol @@ -518,7 +518,7 @@ contract RoninValidatorSet is // Read more about slashing issue at: https://www.notion.so/skymavis/Slashing-Issue-9610ae1452434faca1213ab2e1d7d944 uint256 _minBalance = _stakingContract.minValidatorBalance(); _balanceWeights = _filterUnsatisfiedCandidates(_minBalance); - uint256[] memory _trustedWeights = _roninTrustedOrganizationContract.getWeights(_candidates); + uint256[] memory _trustedWeights = _roninTrustedOrganizationContract.getConsensusWeights(_candidates); uint256 _newValidatorCount; (_newValidators, _newValidatorCount) = _pcPickValidatorSet( _candidates, diff --git a/src/config.ts b/src/config.ts index d5988d129..39042f063 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,7 +1,7 @@ import { BigNumber, BigNumberish } from 'ethers'; import { ethers } from 'hardhat'; import { Address } from 'hardhat-deploy/dist/types'; -import { WeightedAddressStruct } from './types/IBridge'; +import { TrustedOrganizationStruct } from './types/IRoninTrustedOrganization'; export enum Network { Hardhat = 'hardhat', @@ -50,7 +50,7 @@ export interface MaintenanceArguments { } export interface RoninTrustedOrganizationArguments { - trustedOrganizations?: WeightedAddressStruct[]; + trustedOrganizations?: TrustedOrganizationStruct[]; numerator?: BigNumberish; denominator?: BigNumberish; } @@ -84,8 +84,10 @@ export interface StakingVestingConfig { export interface SlashIndicatorArguments { misdemeanorThreshold?: BigNumberish; felonyThreshold?: BigNumberish; + bridgeVotingThreshold?: BigNumberish; slashFelonyAmount?: BigNumberish; slashDoubleSignAmount?: BigNumberish; + bridgeVotingSlashAmount?: BigNumberish; felonyJailBlocks?: BigNumberish; doubleSigningConstrainBlocks?: BigNumberish; } @@ -167,8 +169,10 @@ export const slashIndicatorConf: SlashIndicatorConfig = { [Network.Devnet]: { misdemeanorThreshold: 50, felonyThreshold: 150, + bridgeVotingThreshold: 28800 * 3, // ~3 days slashFelonyAmount: BigNumber.from(10).pow(18).mul(1), // 1 RON slashDoubleSignAmount: BigNumber.from(10).pow(18).mul(10), // 10 RON + bridgeVotingSlashAmount: BigNumber.from(10).pow(18).mul(10_000), // 10.000 RON felonyJailBlocks: 28800 * 2, // jails for 2 days doubleSigningConstrainBlocks: 28800, }, @@ -194,8 +198,14 @@ export const roninValidatorSetConf: RoninValidatorSetConfig = { export const roninTrustedOrganizationConf: RoninTrustedOrganizationConfig = { [Network.Hardhat]: undefined, [Network.Devnet]: { - trustedOrganizations: ['0x93b8eed0a1e082ae2f478fd7f8c14b1fc0261bb1'].map((addr) => ({ addr, weight: 100 })), - numerator: 1, + trustedOrganizations: ['0x93b8eed0a1e082ae2f478fd7f8c14b1fc0261bb1'].map((addr) => ({ + consensusAddr: addr, + governor: addr, + bridgeVoter: addr, + weight: 100, + addedBlock: 0, + })), + numerator: 0, denominator: 1, }, [Network.Testnet]: undefined, diff --git a/src/deploy/proxy/slash-indicator-proxy.ts b/src/deploy/proxy/slash-indicator-proxy.ts index 24989e6e7..006febc2f 100644 --- a/src/deploy/proxy/slash-indicator-proxy.ts +++ b/src/deploy/proxy/slash-indicator-proxy.ts @@ -18,10 +18,14 @@ const deploy = async ({ getNamedAccounts, deployments }: HardhatRuntimeEnvironme const data = new SlashIndicator__factory().interface.encodeFunctionData('initialize', [ roninInitAddress[network.name]!.validatorContract?.address, roninInitAddress[network.name]!.maintenanceContract?.address, + roninInitAddress[network.name]!.roninTrustedOrganizationContract?.address, + roninInitAddress[network.name]!.governanceAdmin?.address, slashIndicatorConf[network.name]!.misdemeanorThreshold, slashIndicatorConf[network.name]!.felonyThreshold, + slashIndicatorConf[network.name]!.bridgeVotingThreshold, slashIndicatorConf[network.name]!.slashFelonyAmount, slashIndicatorConf[network.name]!.slashDoubleSignAmount, + slashIndicatorConf[network.name]!.bridgeVotingSlashAmount, slashIndicatorConf[network.name]!.felonyJailBlocks, slashIndicatorConf[network.name]!.doubleSigningConstrainBlocks, ]); diff --git a/src/script/governance-admin-interface.ts b/src/script/governance-admin-interface.ts index 662117661..0df032ee2 100644 --- a/src/script/governance-admin-interface.ts +++ b/src/script/governance-admin-interface.ts @@ -65,13 +65,32 @@ export class GovernanceAdminInterface { to, 0, this.interface.encodeFunctionData('functionDelegateCall', [data]), - 1_000_000 + 2_000_000 ); const signatures = await this.generateSignatures(proposal); const supports = signatures.map(() => VoteType.For); return this.contract.connect(this.signers[0]).proposeProposalStructAndCastVotes(proposal, supports, signatures); } + async functionDelegateCalls(toList: Address[], dataList: BytesLike[]) { + if (toList.length != dataList.length || toList.length == 0) { + throw Error('invalid array length'); + } + + const proposal = { + chainId: network.config.chainId!, + nonce: (await this.contract.round(network.config.chainId!)).add(1), + targets: toList, + values: toList.map(() => 0), + calldatas: dataList.map((v) => this.interface.encodeFunctionData('functionDelegateCall', [v])), + gasAmounts: toList.map(() => 2_000_000), + }; + + const signatures = await this.generateSignatures(proposal); + const supports = signatures.map(() => VoteType.For); + return this.contract.connect(this.signers[0]).proposeProposalStructAndCastVotes(proposal, supports, signatures); + } + async upgrade(from: Address, to: Address) { const proposal = await this.createProposal(from, 0, this.interface.encodeFunctionData('upgradeTo', [to]), 500_000); const signatures = await this.generateSignatures(proposal); diff --git a/src/script/proposal.ts b/src/script/proposal.ts index 23988c771..9aad7ee95 100644 --- a/src/script/proposal.ts +++ b/src/script/proposal.ts @@ -1,7 +1,8 @@ import { BigNumberish } from 'ethers'; import { AbiCoder, keccak256, solidityKeccak256 } from 'ethers/lib/utils'; +import { Address } from 'hardhat-deploy/dist/types'; + import { GlobalProposalDetailStruct, ProposalDetailStruct } from '../types/GovernanceAdmin'; -import { WeightedAddressStruct } from '../types/IBridge'; // keccak256("ProposalDetail(uint256 nonce,uint256 chainId,address[] targets,uint256[] values,bytes[] calldatas,uint256[] gasAmounts)") const proposalTypeHash = '0x65526afa953b4e935ecd640e6905741252eedae157e79c37331ee8103c70019d'; @@ -9,10 +10,8 @@ const proposalTypeHash = '0x65526afa953b4e935ecd640e6905741252eedae157e79c37331e const globalProposalTypeHash = '0xdb316eb400de2ddff92ab4255c0cd3cba634cd5236b93386ed9328b7d822d1c7'; // keccak256("Ballot(bytes32 proposalHash,uint8 support)") const ballotTypeHash = '0xd900570327c4c0df8dd6bdd522b7da7e39145dd049d2fd4602276adcd511e3c2'; -// keccak256("BridgeOperatorsBallot(uint256 period,BridgeOperator[] operators)BridgeOperator(address addr,uint256 weight)"); -const bridgeOperatorsBallotTypeHash = '0x086d287088869477577720f66bf2a8412510e726fd1a893739cf6c2280aadcb5'; -// keccak256("BridgeOperator(address addr,uint256 weight)"); -const bridgeOperatorTypeHash = '0xe71132f1797176c8456299d5325989bbf16523f1e2e3aef4554d23f982955a2c'; +// keccak256("BridgeOperatorsBallot(uint256 period,address[] operators)"); +const bridgeOperatorsBallotTypeHash = '0xeea5e3908ac28cbdbbce8853e49444c558a0a03597e98ef19e6ff86162ed9ae3'; export enum VoteType { For = 0, @@ -30,7 +29,6 @@ export const ballotParamTypes = ['bytes32', 'bytes32', 'uint8']; export const proposalParamTypes = ['bytes32', 'uint256', 'uint256', 'bytes32', 'bytes32', 'bytes32', 'bytes32']; export const globalProposalParamTypes = ['bytes32', 'uint256', 'bytes32', 'bytes32', 'bytes32', 'bytes32']; export const bridgeOperatorsBallotParamTypes = ['bytes32', 'uint256', 'bytes32']; -export const bridgeOperatorParamTypes = ['bytes32', 'address', 'uint256']; export const BallotTypes = { Ballot: [ @@ -61,11 +59,7 @@ export const GlobalProposalTypes = { export const BridgeOperatorsBallotTypes = { BridgeOperatorsBallot: [ { name: 'period', type: 'uint256' }, - { name: 'operators', type: 'BridgeOperator[]' }, - ], - BridgeOperator: [ - { name: 'addr', type: 'address' }, - { name: 'weight', type: 'uint256' }, + { name: 'operators', type: 'address[]' }, ], }; @@ -145,30 +139,24 @@ export const getBallotDigest = (domainSeparator: string, proposalHash: string, s export interface BOsBallot { period: BigNumberish; - operators: WeightedAddressStruct[]; + operators: Address[]; } -export const getBOsBallotHash = (period: BigNumberish, operators: WeightedAddressStruct[]) => +export const getBOsBallotHash = (period: BigNumberish, operators: Address[]) => keccak256( AbiCoder.prototype.encode(bridgeOperatorsBallotParamTypes, [ bridgeOperatorsBallotTypeHash, period, keccak256( AbiCoder.prototype.encode( - operators.map(() => 'bytes32'), - operators.map(({ addr, weight }) => - keccak256(AbiCoder.prototype.encode(bridgeOperatorParamTypes, [bridgeOperatorTypeHash, addr, weight])) - ) + operators.map(() => 'address'), + operators ) ), ]) ); -export const getBOsBallotDigest = ( - domainSeparator: string, - period: BigNumberish, - operators: WeightedAddressStruct[] -): string => +export const getBOsBallotDigest = (domainSeparator: string, period: BigNumberish, operators: Address[]): string => solidityKeccak256( ['bytes1', 'bytes1', 'bytes32', 'bytes32'], ['0x19', '0x01', domainSeparator, getBOsBallotHash(period, operators)] diff --git a/test/governance-admin/GovernanceAdmin.test.ts b/test/governance-admin/GovernanceAdmin.test.ts index 7cdedeb35..f02c414bb 100644 --- a/test/governance-admin/GovernanceAdmin.test.ts +++ b/test/governance-admin/GovernanceAdmin.test.ts @@ -45,7 +45,13 @@ describe('Governance Admin test', () => { const { roninGovernanceAdminAddress, mainchainGovernanceAdminAddress, stakingContractAddress } = await initTest( 'RoninGovernanceAdmin.test' )({ - trustedOrganizations: governors.map((v) => ({ addr: v.address, weight: 100 })), + trustedOrganizations: governors.map((v) => ({ + consensusAddr: v.address, + governor: v.address, + bridgeVoter: v.address, + weight: 100, + addedBlock: 0, + })), numerator: 1, denominator: 2, relayers: [relayer.address], @@ -93,7 +99,7 @@ describe('Governance Admin test', () => { it('Should be able to vote bridge operators', async () => { ballot = { period: 10, - operators: governors.map((v) => ({ addr: v.address, weight: 100 })), + operators: governors.map((v) => v.address), }; signatures = await Promise.all( governors.map((g) => @@ -103,11 +109,11 @@ describe('Governance Admin test', () => { ) ); await governanceAdmin.voteBridgeOperatorsBySignatures(ballot.period, ballot.operators, signatures); - expect(await bridgeContract.getBridgeOperators()).eql(governors.map((v) => [v.address, BigNumber.from(100)])); }); it('Should be able relay vote bridge operators', async () => { await mainchainGovernanceAdmin.connect(relayer).relayBridgeOperators(ballot.period, ballot.operators, signatures); + expect(await bridgeContract.getBridgeOperators()).eql(governors.map((v) => v.address)); }); it('Should not able to relay again', async () => { @@ -119,7 +125,7 @@ describe('Governance Admin test', () => { it('Should not be able to use the signatures for another period', async () => { ballot = { period: 100, - operators: governors.map((v) => ({ addr: v.address, weight: 100 })), + operators: governors.map((v) => v.address), }; await expect( governanceAdmin.voteBridgeOperatorsBySignatures(ballot.period, ballot.operators, signatures) @@ -129,7 +135,7 @@ describe('Governance Admin test', () => { it('Should not be able to vote bridge operators with a smaller period', async () => { ballot = { period: 5, - operators: governors.map((v) => ({ addr: v.address, weight: 100 })), + operators: governors.map((v) => v.address), }; signatures = await Promise.all( governors.map((g) => diff --git a/test/helpers/fixture.ts b/test/helpers/fixture.ts index 7e71d3b0f..cbd1ac276 100644 --- a/test/helpers/fixture.ts +++ b/test/helpers/fixture.ts @@ -40,6 +40,8 @@ export const defaultTestConfig = { slashFelonyAmount: BigNumber.from(10).pow(18).mul(1), slashDoubleSignAmount: BigNumber.from(10).pow(18).mul(10), doubleSigningConstrainBlocks: 28800, + bridgeVotingThreshold: 28800 * 3, + bridgeVotingSlashAmount: BigNumber.from(10).pow(18).mul(10_000), maxValidatorNumber: 4, maxPrioritizedValidatorNumber: 0, @@ -86,6 +88,8 @@ export const initTest = (id: string) => maxSchedules: options?.maxSchedules ?? defaultTestConfig.maxSchedules, }; slashIndicatorConf[network.name] = { + bridgeVotingThreshold: options?.bridgeVotingThreshold ?? defaultTestConfig.bridgeVotingThreshold, + bridgeVotingSlashAmount: options?.bridgeVotingSlashAmount ?? defaultTestConfig.bridgeVotingSlashAmount, misdemeanorThreshold: options?.misdemeanorThreshold ?? defaultTestConfig.misdemeanorThreshold, felonyThreshold: options?.felonyThreshold ?? defaultTestConfig.felonyThreshold, slashFelonyAmount: options?.slashFelonyAmount ?? defaultTestConfig.slashFelonyAmount, diff --git a/test/integration/ActionSlashValidators.test.ts b/test/integration/ActionSlashValidators.test.ts index f3ed66ea1..e946fb5cc 100644 --- a/test/integration/ActionSlashValidators.test.ts +++ b/test/integration/ActionSlashValidators.test.ts @@ -55,7 +55,15 @@ describe('[Integration] Slash validators', () => { slashFelonyAmount, slashDoubleSignAmount, minValidatorBalance, - trustedOrganizations: [{ addr: governor.address, weight: 100 }], + trustedOrganizations: [ + { + consensusAddr: governor.address, + governor: governor.address, + bridgeVoter: governor.address, + weight: 100, + addedBlock: 0, + }, + ], }); slashContract = SlashIndicator__factory.connect(slashContractAddress, deployer); diff --git a/test/integration/ActionSubmitReward.test.ts b/test/integration/ActionSubmitReward.test.ts index bd157fa67..bd526ad04 100644 --- a/test/integration/ActionSubmitReward.test.ts +++ b/test/integration/ActionSubmitReward.test.ts @@ -49,7 +49,13 @@ describe('[Integration] Submit Block Reward', () => { validatorBonusPerBlock, slashFelonyAmount, slashDoubleSignAmount, - trustedOrganizations: [governor.address].map((addr) => ({ addr, weight: 100 })), + trustedOrganizations: [governor].map((v) => ({ + consensusAddr: v.address, + governor: v.address, + bridgeVoter: v.address, + weight: 100, + addedBlock: 0, + })), }); slashContract = SlashIndicator__factory.connect(slashContractAddress, deployer); diff --git a/test/integration/ActionWrapUpEpoch.test.ts b/test/integration/ActionWrapUpEpoch.test.ts index fac5ebbc4..4cd213c2f 100644 --- a/test/integration/ActionWrapUpEpoch.test.ts +++ b/test/integration/ActionWrapUpEpoch.test.ts @@ -51,7 +51,13 @@ describe('[Integration] Wrap up epoch', () => { slashDoubleSignAmount, minValidatorBalance, maxValidatorNumber, - trustedOrganizations: [governor.address].map((addr) => ({ addr, weight: 100 })), + trustedOrganizations: [governor].map((v) => ({ + consensusAddr: v.address, + governor: v.address, + bridgeVoter: v.address, + weight: 100, + addedBlock: 0, + })), }); slashContract = SlashIndicator__factory.connect(slashContractAddress, deployer); stakingContract = Staking__factory.connect(stakingContractAddress, deployer); diff --git a/test/integration/Configuration.test.ts b/test/integration/Configuration.test.ts index 148943117..ced8701f5 100644 --- a/test/integration/Configuration.test.ts +++ b/test/integration/Configuration.test.ts @@ -76,7 +76,13 @@ describe('[Integration] Configuration check', () => { maxMaintenanceBlockPeriod, minOffset, maxSchedules, - trustedOrganizations: [governor.address].map((addr) => ({ addr, weight: 100 })), + trustedOrganizations: [governor].map((v) => ({ + consensusAddr: v.address, + governor: v.address, + bridgeVoter: v.address, + weight: 100, + addedBlock: 0, + })), }); stakingVestingContract = StakingVesting__factory.connect(stakingVestingContractAddress, deployer); diff --git a/test/maintainance/Maintenance.test.ts b/test/maintainance/Maintenance.test.ts index 414bef497..db5d9bee5 100644 --- a/test/maintainance/Maintenance.test.ts +++ b/test/maintainance/Maintenance.test.ts @@ -57,7 +57,13 @@ describe('Maintenance test', () => { validatorContractAddress, roninGovernanceAdminAddress, } = await initTest('Maintenance')({ - trustedOrganizations: [governor.address].map((addr) => ({ addr, weight: 100 })), + trustedOrganizations: [governor].map((v) => ({ + consensusAddr: v.address, + governor: v.address, + bridgeVoter: v.address, + weight: 100, + addedBlock: 0, + })), misdemeanorThreshold, felonyThreshold, }); diff --git a/test/slash/SlashIndicator.test.ts b/test/slash/SlashIndicator.test.ts index a35ba1c1d..b9eef71f3 100644 --- a/test/slash/SlashIndicator.test.ts +++ b/test/slash/SlashIndicator.test.ts @@ -73,7 +73,13 @@ describe('Slash indicator test', () => { const { slashContractAddress, stakingContractAddress, validatorContractAddress, roninGovernanceAdminAddress } = await initTest('SlashIndicator')({ - trustedOrganizations: [governor.address].map((addr) => ({ addr, weight: 100 })), + trustedOrganizations: [governor].map((v) => ({ + consensusAddr: v.address, + governor: v.address, + bridgeVoter: v.address, + weight: 100, + addedBlock: 0, + })), misdemeanorThreshold, felonyThreshold, maxValidatorNumber, diff --git a/test/validator/ArrangeValidators.test.ts b/test/validator/ArrangeValidators.test.ts index d497ea48b..7d547b381 100644 --- a/test/validator/ArrangeValidators.test.ts +++ b/test/validator/ArrangeValidators.test.ts @@ -37,16 +37,24 @@ const setPriorityStatus = async (addrs: Address[], statuses: boolean[]): Promise const arr = statuses.map((stt, i) => ({ address: addrs[i], stt })); const addingTrustedOrgs = arr.filter(({ stt }) => stt).map(({ address }) => address); const removingTrustedOrgs = arr.filter(({ stt }) => !stt).map(({ address }) => address); - await governanceAdminInterface.functionDelegateCall( - roninTrustedOrganization.address, - roninTrustedOrganization.interface.encodeFunctionData('addTrustedOrganizations', [ - addingTrustedOrgs.map((v) => ({ addr: v, weight: defaultTrustedWeight })), - ]) - ); - await governanceAdminInterface.functionDelegateCall( - roninTrustedOrganization.address, - roninTrustedOrganization.interface.encodeFunctionData('removeTrustedOrganizations', [removingTrustedOrgs]) - ); + if (addingTrustedOrgs.length > 0) { + await governanceAdminInterface.functionDelegateCalls( + addingTrustedOrgs.map(() => roninTrustedOrganization.address), + addingTrustedOrgs.map((v) => + roninTrustedOrganization.interface.encodeFunctionData('addTrustedOrganizations', [ + [{ consensusAddr: v, governor: v, bridgeVoter: v, weight: 100, addedBlock: 0 }], + ]) + ) + ); + } + if (removingTrustedOrgs.length > 0) { + await governanceAdminInterface.functionDelegateCalls( + removingTrustedOrgs.map(() => roninTrustedOrganization.address), + removingTrustedOrgs.map((v) => + roninTrustedOrganization.interface.encodeFunctionData('removeTrustedOrganizations', [[v]]) + ) + ); + } return statuses.map((stt) => (stt ? defaultTrustedWeight : 0)); }; @@ -91,7 +99,13 @@ describe('Arrange validators', () => { maxValidatorCandidate, maxPrioritizedValidatorNumber, slashFelonyAmount, - trustedOrganizations: [governor.address].map((addr) => ({ addr, weight: defaultTrustedWeight })), + trustedOrganizations: [governor].map((v) => ({ + consensusAddr: v.address, + governor: v.address, + bridgeVoter: v.address, + weight: 100, + addedBlock: 0, + })), }); validatorContract = MockRoninValidatorSetExtended__factory.connect(validatorContractAddress, deployer); @@ -409,7 +423,6 @@ describe('Arrange validators', () => { }); it('Shuffled: Actual(prioritized) == MaxNum(prioritized); Actual(regular) == MaxNum(regular)', async () => { - // prettier-ignore let indexes = [0, 1, 2, 3, 4, 5, 6]; let statuses = [true, false, true, true, false, true, false]; @@ -430,7 +443,6 @@ describe('Arrange validators', () => { }); it('Shuffled: Actual(prioritized) > MaxNum(prioritized); Actual(regular) < MaxNum(regular)', async () => { - // prettier-ignore let indexes = [0, 1, 2, 3, 4, 5, 6]; let statuses = [true, false, true, true, false, true, true]; @@ -450,8 +462,7 @@ describe('Arrange validators', () => { }); it('Shuffled: Actual(prioritized) < MaxNum(prioritized); Actual(regular) > MaxNum(regular)', async () => { - // prettier-ignore - let indexes = [0, 1, 2, 3, 4, 5, 6, 7 ]; + let indexes = [0, 1, 2, 3, 4, 5, 6, 7]; let statuses = [true, false, false, false, false, true, true, false]; let inputTrustedWeights = await setPriorityStatusByIndexes(indexes, statuses); diff --git a/test/validator/RoninValidatorSet.test.ts b/test/validator/RoninValidatorSet.test.ts index 1a76df345..a9b7658ee 100644 --- a/test/validator/RoninValidatorSet.test.ts +++ b/test/validator/RoninValidatorSet.test.ts @@ -50,7 +50,13 @@ describe('Ronin Validator Set test', () => { const { slashContractAddress, validatorContractAddress, stakingContractAddress, roninGovernanceAdminAddress } = await initTest('RoninValidatorSet')({ - trustedOrganizations: [governor.address].map((addr) => ({ addr, weight: 100 })), + trustedOrganizations: [governor].map((v) => ({ + consensusAddr: v.address, + governor: v.address, + bridgeVoter: v.address, + weight: 100, + addedBlock: 0, + })), minValidatorBalance, maxValidatorNumber, maxValidatorCandidate,