diff --git a/script/20231218-maptoken/20231218-maptoken-mainchain.s.sol b/script/20231218-maptoken/20231218-maptoken-mainchain.s.sol index e7a72225..abc8901e 100644 --- a/script/20231218-maptoken/20231218-maptoken-mainchain.s.sol +++ b/script/20231218-maptoken/20231218-maptoken-mainchain.s.sol @@ -83,6 +83,6 @@ contract Migration__20231215_MapTokenMainchain is BridgeMigration { uint256 chainId = _config.getCompanionNetwork(_config.getNetworkByChainId(block.chainid)).chainId(); vm.broadcast(sender()); - _roninBridgeManager.propose(chainId, expiredTime, targets, values, calldatas, gasAmounts); + _roninBridgeManager.propose(chainId, expiredTime, address(0), false, targets, values, calldatas, gasAmounts); } } diff --git a/script/20231218-maptoken/20231218-maptoken-roninchain.s.sol b/script/20231218-maptoken/20231218-maptoken-roninchain.s.sol index 5621946c..4e3b9150 100644 --- a/script/20231218-maptoken/20231218-maptoken-roninchain.s.sol +++ b/script/20231218-maptoken/20231218-maptoken-roninchain.s.sol @@ -58,6 +58,6 @@ contract Migration__20231215_MapTokenRoninchain is BridgeMigration { _verifyRoninProposalGasAmount(targets, values, calldatas, gasAmounts); vm.broadcast(sender()); - _roninBridgeManager.propose(block.chainid, expiredTime, targets, values, calldatas, gasAmounts); + _roninBridgeManager.propose(block.chainid, expiredTime, address(0), false, targets, values, calldatas, gasAmounts); } } diff --git a/script/20240115-mappixeltoken/20240115-maptoken-mainchain.s.sol b/script/20240115-mappixeltoken/20240115-maptoken-mainchain.s.sol index 649d8e6a..f433067c 100644 --- a/script/20240115-mappixeltoken/20240115-maptoken-mainchain.s.sol +++ b/script/20240115-mappixeltoken/20240115-maptoken-mainchain.s.sol @@ -175,6 +175,8 @@ contract Migration__MapTokenMainchain is BridgeMigration { _roninBridgeManager.propose( chainId, expiredTime, + address(0), + false, targets, values, calldatas, diff --git a/script/20240115-mappixeltoken/20240115-maptoken-roninchain.s.sol b/script/20240115-mappixeltoken/20240115-maptoken-roninchain.s.sol index 3b7d548b..ffd75cf7 100644 --- a/script/20240115-mappixeltoken/20240115-maptoken-roninchain.s.sol +++ b/script/20240115-mappixeltoken/20240115-maptoken-roninchain.s.sol @@ -131,6 +131,8 @@ contract Migration__MapTokenRoninchain is BridgeMigration { _roninBridgeManager.propose( block.chainid, expiredTime, + address(0), + false, targets, values, calldatas, diff --git a/script/20240131-maptoken-pixel/20240131-maptoken-pixel-mainchain.s.sol b/script/20240131-maptoken-pixel/20240131-maptoken-pixel-mainchain.s.sol index 1101126e..0603d4a2 100644 --- a/script/20240131-maptoken-pixel/20240131-maptoken-pixel-mainchain.s.sol +++ b/script/20240131-maptoken-pixel/20240131-maptoken-pixel-mainchain.s.sol @@ -133,6 +133,8 @@ contract Migration__20240131_MapTokenPixelMainchain is BridgeMigration, Migratio _roninBridgeManager.propose( chainId, expiredTime, + address(0), + false, targets, values, calldatas, diff --git a/script/20240131-maptoken-pixel/20240131-maptoken-pixel-roninchain.s.sol b/script/20240131-maptoken-pixel/20240131-maptoken-pixel-roninchain.s.sol index 607a728f..129f589b 100644 --- a/script/20240131-maptoken-pixel/20240131-maptoken-pixel-roninchain.s.sol +++ b/script/20240131-maptoken-pixel/20240131-maptoken-pixel-roninchain.s.sol @@ -131,6 +131,8 @@ contract Migration__20240131_MapTokenPixelRoninchain is BridgeMigration, Migrati _roninBridgeManager.propose( block.chainid, expiredTime, + address(0), + false, targets, values, calldatas, diff --git a/script/20240206-maptoken-banana/20240206-maptoken-banana-mainchain.s.sol b/script/20240206-maptoken-banana/20240206-maptoken-banana-mainchain.s.sol index 4a7da82a..9714c9bd 100644 --- a/script/20240206-maptoken-banana/20240206-maptoken-banana-mainchain.s.sol +++ b/script/20240206-maptoken-banana/20240206-maptoken-banana-mainchain.s.sol @@ -146,7 +146,7 @@ contract Migration__20240206_MapTokenBananaMainchain is console2.log("Nonce:", vm.getNonce(_governor)); vm.broadcast(_governor); - _roninBridgeManager.propose(chainId, expiredTime, targets, values, calldatas, gasAmounts); + _roninBridgeManager.propose(chainId, expiredTime, address(0), false, targets, values, calldatas, gasAmounts); } } diff --git a/script/20240206-maptoken-banana/20240206-maptoken-banana-roninchain.s.sol b/script/20240206-maptoken-banana/20240206-maptoken-banana-roninchain.s.sol index 375fc17b..2e6ab1dc 100644 --- a/script/20240206-maptoken-banana/20240206-maptoken-banana-roninchain.s.sol +++ b/script/20240206-maptoken-banana/20240206-maptoken-banana-roninchain.s.sol @@ -117,7 +117,7 @@ contract Migration__20240206_MapTokenBananaRoninChain is roninTokensToSetMinThreshold[1] = pixelRoninToken; minThresholds[1] = pixelMinThreshold; - + roninTokensToSetMinThreshold[2] = pixelMainchainToken; minThresholds[2] = 0; @@ -152,7 +152,7 @@ contract Migration__20240206_MapTokenBananaRoninChain is console2.log("Nonce:", vm.getNonce(_governor)); vm.broadcast(_governor); - _roninBridgeManager.propose(block.chainid, expiredTime, targets, values, calldatas, gasAmounts); + _roninBridgeManager.propose(block.chainid, expiredTime, address(0), false, targets, values, calldatas, gasAmounts); // ============= LOCAL SIMULATION ================== _cheatWeightOperator(_governor); diff --git a/src/extensions/bridge-operator-governance/BridgeManager.sol b/src/extensions/bridge-operator-governance/BridgeManager.sol index 2335b023..f3fa0c9c 100644 --- a/src/extensions/bridge-operator-governance/BridgeManager.sol +++ b/src/extensions/bridge-operator-governance/BridgeManager.sol @@ -77,9 +77,9 @@ abstract contract BridgeManager is IBridgeManager, Initializable, HasContracts, DOMAIN_SEPARATOR = keccak256( abi.encode( keccak256("EIP712Domain(string name,string version,bytes32 salt)"), - keccak256("BridgeAdmin"), // name hash - keccak256("2"), // version hash - keccak256(abi.encode("BRIDGE_ADMIN", roninChainId)) // salt + keccak256("BridgeManager"), // name hash + keccak256("3"), // version hash + keccak256(abi.encode("BRIDGE_MANAGER", roninChainId)) // salt ) ); diff --git a/src/extensions/sequential-governance/CoreGovernance.sol b/src/extensions/sequential-governance/CoreGovernance.sol index 944905f7..1415abb2 100644 --- a/src/extensions/sequential-governance/CoreGovernance.sol +++ b/src/extensions/sequential-governance/CoreGovernance.sol @@ -109,6 +109,8 @@ abstract contract CoreGovernance is Initializable, SignatureConsumer, VoteStatus function _proposeProposal( uint256 chainId, uint256 expiryTimestamp, + address executor, + bool loose, address[] memory targets, uint256[] memory values, bytes[] memory calldatas, @@ -118,7 +120,7 @@ abstract contract CoreGovernance is Initializable, SignatureConsumer, VoteStatus if (chainId == 0) revert ErrInvalidChainId(msg.sig, 0, block.chainid); uint256 round_ = _createVotingRound(chainId); - proposal = Proposal.ProposalDetail(round_, chainId, expiryTimestamp, targets, values, calldatas, gasAmounts); + proposal = Proposal.ProposalDetail(round_, chainId, expiryTimestamp, executor, loose, targets, values, calldatas, gasAmounts); proposal.validate(_proposalExpiryDuration); bytes32 proposalHash = proposal.hash(); @@ -204,7 +206,9 @@ abstract contract CoreGovernance is Initializable, SignatureConsumer, VoteStatus done = true; _vote.status = VoteStatus.Approved; emit ProposalApproved(_vote.hash); - _tryExecute(_vote, proposal); + if (proposal.isAutoExecute()) { + _tryExecute(_vote, proposal); + } } else if (_againstVoteWeight >= minimumAgainstVoteWeight) { done = true; _vote.status = VoteStatus.Rejected; @@ -212,6 +216,23 @@ abstract contract CoreGovernance is Initializable, SignatureConsumer, VoteStatus } } + /** + * @dev The specified executor executes the proposal on an approved proposal. + */ + function _executeWithCaller(Proposal.ProposalDetail memory proposal, address caller) internal { + bytes32 proposalHash = proposal.hash(); + ProposalVote storage _vote = vote[proposal.chainId][proposal.nonce]; + + if (_vote.hash != proposalHash) { + revert ErrInvalidProposal(proposalHash, _vote.hash); + } + + if (_vote.status != VoteStatus.Approved) revert ErrProposalNotApproved(); + if (caller != proposal.executor) revert ErrInvalidExecutor(); + + _tryExecute(_vote, proposal); + } + /** * @dev When the contract is on Ronin chain, checks whether the proposal is expired and delete it if is expired. * diff --git a/src/extensions/sequential-governance/GlobalCoreGovernance.sol b/src/extensions/sequential-governance/GlobalCoreGovernance.sol index b5ead07b..585fe6a6 100644 --- a/src/extensions/sequential-governance/GlobalCoreGovernance.sol +++ b/src/extensions/sequential-governance/GlobalCoreGovernance.sol @@ -42,6 +42,8 @@ abstract contract GlobalCoreGovernance is CoreGovernance { function _proposeGlobal( uint256 expiryTimestamp, GlobalProposal.TargetOption[] calldata targetOptions, + address executor, + bool loose, uint256[] memory values, bytes[] memory calldatas, uint256[] memory gasAmounts, @@ -49,7 +51,7 @@ abstract contract GlobalCoreGovernance is CoreGovernance { ) internal virtual { uint256 round_ = _createVotingRound(0); GlobalProposal.GlobalProposalDetail memory globalProposal = - GlobalProposal.GlobalProposalDetail(round_, expiryTimestamp, targetOptions, values, calldatas, gasAmounts); + GlobalProposal.GlobalProposalDetail(round_, expiryTimestamp, executor, loose, targetOptions, values, calldatas, gasAmounts); Proposal.ProposalDetail memory proposal = globalProposal.intoProposalDetail(_resolveTargets({ targetOptions: targetOptions, strict: true })); proposal.validate(_proposalExpiryDuration); @@ -82,6 +84,11 @@ abstract contract GlobalCoreGovernance is CoreGovernance { emit GlobalProposalCreated(round_, proposalHash, proposal, globalProposal.hash(), globalProposal, creator); } + function _executeGlobalWithCaller(GlobalProposal.GlobalProposalDetail memory globalProposal, address caller) internal { + Proposal.ProposalDetail memory proposal = globalProposal.intoProposalDetail(_resolveTargets({ targetOptions: globalProposal.targetOptions, strict: true })); + _executeWithCaller(proposal, caller); + } + /** * @dev Returns corresponding address of target options. Return address(0) on non-existent target. */ diff --git a/src/extensions/sequential-governance/governance-proposal/CommonGovernanceProposal.sol b/src/extensions/sequential-governance/governance-proposal/CommonGovernanceProposal.sol index bfa07fd7..469391f1 100644 --- a/src/extensions/sequential-governance/governance-proposal/CommonGovernanceProposal.sol +++ b/src/extensions/sequential-governance/governance-proposal/CommonGovernanceProposal.sol @@ -6,13 +6,6 @@ import "../CoreGovernance.sol"; abstract contract CommonGovernanceProposal is CoreGovernance { using Proposal for Proposal.ProposalDetail; - /** - * @dev Error thrown when an invalid proposal is encountered. - * @param actual The actual value of the proposal. - * @param expected The expected value of the proposal. - */ - error ErrInvalidProposal(bytes32 actual, bytes32 expected); - /** * @dev Casts votes by signatures. * diff --git a/src/libraries/GlobalProposal.sol b/src/libraries/GlobalProposal.sol index 5fd9dd3e..9d26debf 100644 --- a/src/libraries/GlobalProposal.sol +++ b/src/libraries/GlobalProposal.sol @@ -21,14 +21,16 @@ library GlobalProposal { // Nonce to make sure proposals are executed in order uint256 nonce; uint256 expiryTimestamp; + address executor; + bool loose; TargetOption[] targetOptions; uint256[] values; bytes[] calldatas; uint256[] gasAmounts; } - // keccak256("GlobalProposalDetail(uint256 nonce,uint256 expiryTimestamp,uint8[] targetOptions,uint256[] values,bytes[] calldatas,uint256[] gasAmounts)"); - bytes32 public constant TYPE_HASH = 0x1463f426c05aff2c1a7a0957a71c9898bc8b47142540538e79ee25ee91141350; + // keccak256("GlobalProposalDetail(uint256 nonce,uint256 expiryTimestamp,address executor,bool loose,uint8[] targetOptions,uint256[] values,bytes[] calldatas,uint256[] gasAmounts)"); + bytes32 internal constant TYPE_HASH = 0x8fdb3bc7211cb44f39a2cae84127672c4570a00720dfbf2bb58285070faa28da; /** * @dev Returns struct hash of the proposal. @@ -39,7 +41,7 @@ library GlobalProposal { bytes32[] memory calldataHashList = new bytes32[](self.calldatas.length); uint256[] memory gasAmounts = self.gasAmounts; - for (uint256 i; i < calldataHashList.length; ) { + for (uint256 i; i < calldataHashList.length;) { calldataHashList[i] = keccak256(self.calldatas[i]); unchecked { @@ -52,54 +54,55 @@ library GlobalProposal { * keccak256( * abi.encode( * TYPE_HASH, - * _proposal.nonce, - * _proposal.expiryTimestamp, - * _targetsHash, - * _valuesHash, - * _calldatasHash, - * _gasAmountsHash + * proposal.nonce, + * proposal.expiryTimestamp, + * proposal.executor, + * proposal.loose, + * targetsHash, + * valuesHash, + * calldatasHash, + * gasAmountsHash * ) * ); */ assembly { let ptr := mload(0x40) mstore(ptr, TYPE_HASH) - mstore(add(ptr, 0x20), mload(self)) // _proposal.nonce - mstore(add(ptr, 0x40), mload(add(self, 0x20))) // _proposal.expiryTimestamp + mstore(add(ptr, 0x20), mload(self)) // proposal.nonce + mstore(add(ptr, 0x40), mload(add(self, 0x20))) // proposal.expiryTimestamp + mstore(add(ptr, 0x60), mload(add(self, 0x40))) // proposal.executor + mstore(add(ptr, 0x80), mload(add(self, 0x60))) // proposal.loose let arrayHashed arrayHashed := keccak256(add(targets, 32), mul(mload(targets), 32)) // targetsHash - mstore(add(ptr, 0x60), arrayHashed) - arrayHashed := keccak256(add(values, 32), mul(mload(values), 32)) // _valuesHash - mstore(add(ptr, 0x80), arrayHashed) - arrayHashed := keccak256(add(calldataHashList, 32), mul(mload(calldataHashList), 32)) // _calldatasHash mstore(add(ptr, 0xa0), arrayHashed) - arrayHashed := keccak256(add(gasAmounts, 32), mul(mload(gasAmounts), 32)) // _gasAmountsHash + arrayHashed := keccak256(add(values, 32), mul(mload(values), 32)) // valuesHash mstore(add(ptr, 0xc0), arrayHashed) - digest_ := keccak256(ptr, 0xe0) + arrayHashed := keccak256(add(calldataHashList, 32), mul(mload(calldataHashList), 32)) // calldatasHash + mstore(add(ptr, 0xe0), arrayHashed) + arrayHashed := keccak256(add(gasAmounts, 32), mul(mload(gasAmounts), 32)) // gasAmountsHash + mstore(add(ptr, 0x100), arrayHashed) + digest_ := keccak256(ptr, 0x120) } } /** * @dev Converts into the normal proposal. */ - function intoProposalDetail( - GlobalProposalDetail memory self, - address[] memory targets - ) internal pure returns (Proposal.ProposalDetail memory detail_) { + function intoProposalDetail(GlobalProposalDetail memory self, address[] memory targets) internal pure returns (Proposal.ProposalDetail memory detail_) { detail_.nonce = self.nonce; - detail_.expiryTimestamp = self.expiryTimestamp; detail_.chainId = 0; + detail_.expiryTimestamp = self.expiryTimestamp; + detail_.executor = self.executor; + detail_.loose = self.loose; + detail_.targets = new address[](self.targetOptions.length); detail_.values = self.values; detail_.calldatas = self.calldatas; detail_.gasAmounts = self.gasAmounts; - for (uint256 i; i < self.targetOptions.length; ) { + for (uint256 i; i < self.targetOptions.length; ++i) { detail_.targets[i] = targets[i]; - unchecked { - ++i; - } } } } diff --git a/src/libraries/Proposal.sol b/src/libraries/Proposal.sol index 06e36db3..c5ede054 100644 --- a/src/libraries/Proposal.sol +++ b/src/libraries/Proposal.sol @@ -14,36 +14,47 @@ library Proposal { */ error ErrInvalidExpiryTimestamp(); + /** + * @dev Error thrown when the loose proposal reverts when execute the internal call no. `callIndex` with revert message is `revertMsg`. + */ + error ErrLooseProposalInternallyRevert(uint256 callIndex, bytes revertMsg); + struct ProposalDetail { // Nonce to make sure proposals are executed in order uint256 nonce; // Value 0: all chain should run this proposal - // Other values: only specifc chain has to execute + // Other values: only specific chain has to execute uint256 chainId; uint256 expiryTimestamp; + // The address that execute the proposal after the proposal passes. + // Leave this address as address(0) to auto-execute by the last valid vote. + address executor; + // A `loose` proposal will revert the whole proposal if encounter one internal failed. + // A non-`loose` proposal will ignore the failed internal calls. + bool loose; address[] targets; uint256[] values; bytes[] calldatas; uint256[] gasAmounts; } - // keccak256("ProposalDetail(uint256 nonce,uint256 chainId,uint256 expiryTimestamp,address[] targets,uint256[] values,bytes[] calldatas,uint256[] gasAmounts)"); - bytes32 public constant TYPE_HASH = 0xd051578048e6ff0bbc9fca3b65a42088dbde10f36ca841de566711087ad9b08a; + // keccak256("ProposalDetail(uint256 nonce,uint256 chainId,uint256 expiryTimestamp,address executor,bool loose,address[] targets,uint256[] values,bytes[] calldatas,uint256[] gasAmounts)"); + bytes32 internal constant TYPE_HASH = 0x98e2bc443e89d620038081eb862bc4dd7a26e2eba7a2a87201642f9419340a57; /** * @dev Validates the proposal. */ - function validate(ProposalDetail memory _proposal, uint256 _maxExpiryDuration) internal view { + function validate(ProposalDetail memory proposal, uint256 maxExpiryDuration) internal view { if ( - !(_proposal.targets.length > 0 && - _proposal.targets.length == _proposal.values.length && - _proposal.targets.length == _proposal.calldatas.length && - _proposal.targets.length == _proposal.gasAmounts.length) + !( + proposal.targets.length > 0 && proposal.targets.length == proposal.values.length && proposal.targets.length == proposal.calldatas.length + && proposal.targets.length == proposal.gasAmounts.length + ) ) { revert ErrLengthMismatch(msg.sig); } - if (_proposal.expiryTimestamp > block.timestamp + _maxExpiryDuration) { + if (proposal.expiryTimestamp > block.timestamp + maxExpiryDuration) { revert ErrInvalidExpiryTimestamp(); } } @@ -51,83 +62,86 @@ library Proposal { /** * @dev Returns struct hash of the proposal. */ - function hash(ProposalDetail memory _proposal) internal pure returns (bytes32 digest_) { - uint256[] memory _values = _proposal.values; - address[] memory _targets = _proposal.targets; - bytes32[] memory _calldataHashList = new bytes32[](_proposal.calldatas.length); - uint256[] memory _gasAmounts = _proposal.gasAmounts; - - for (uint256 _i; _i < _calldataHashList.length; ) { - _calldataHashList[_i] = keccak256(_proposal.calldatas[_i]); - - unchecked { - ++_i; - } + function hash(ProposalDetail memory proposal) internal pure returns (bytes32 digest_) { + uint256[] memory values = proposal.values; + address[] memory targets = proposal.targets; + bytes32[] memory calldataHashList = new bytes32[](proposal.calldatas.length); + uint256[] memory gasAmounts = proposal.gasAmounts; + + for (uint256 i; i < calldataHashList.length; ++i) { + calldataHashList[i] = keccak256(proposal.calldatas[i]); } // return // keccak256( // abi.encode( // TYPE_HASH, - // _proposal.nonce, - // _proposal.chainId, - // _targetsHash, - // _valuesHash, - // _calldatasHash, - // _gasAmountsHash + // proposal.nonce, + // proposal.chainId, + // proposal.expiryTimestamp + // proposal.executor + // proposal.loose + // targetsHash, + // valuesHash, + // calldatasHash, + // gasAmountsHash // ) // ); // / assembly { let ptr := mload(0x40) mstore(ptr, TYPE_HASH) - mstore(add(ptr, 0x20), mload(_proposal)) // _proposal.nonce - mstore(add(ptr, 0x40), mload(add(_proposal, 0x20))) // _proposal.chainId - mstore(add(ptr, 0x60), mload(add(_proposal, 0x40))) // expiry timestamp + mstore(add(ptr, 0x20), mload(proposal)) // proposal.nonce + mstore(add(ptr, 0x40), mload(add(proposal, 0x20))) // proposal.chainId + mstore(add(ptr, 0x60), mload(add(proposal, 0x40))) // proposal.expiryTimestamp + mstore(add(ptr, 0x80), mload(add(proposal, 0x60))) // proposal.executor + mstore(add(ptr, 0xa0), mload(add(proposal, 0x80))) // proposal.loose let arrayHashed - arrayHashed := keccak256(add(_targets, 32), mul(mload(_targets), 32)) // targetsHash - mstore(add(ptr, 0x80), arrayHashed) - arrayHashed := keccak256(add(_values, 32), mul(mload(_values), 32)) // _valuesHash - mstore(add(ptr, 0xa0), arrayHashed) - arrayHashed := keccak256(add(_calldataHashList, 32), mul(mload(_calldataHashList), 32)) // _calldatasHash + arrayHashed := keccak256(add(targets, 32), mul(mload(targets), 32)) // targetsHash mstore(add(ptr, 0xc0), arrayHashed) - arrayHashed := keccak256(add(_gasAmounts, 32), mul(mload(_gasAmounts), 32)) // _gasAmountsHash + arrayHashed := keccak256(add(values, 32), mul(mload(values), 32)) // valuesHash mstore(add(ptr, 0xe0), arrayHashed) - digest_ := keccak256(ptr, 0x100) + arrayHashed := keccak256(add(calldataHashList, 32), mul(mload(calldataHashList), 32)) // calldatasHash + mstore(add(ptr, 0x100), arrayHashed) + arrayHashed := keccak256(add(gasAmounts, 32), mul(mload(gasAmounts), 32)) // gasAmountsHash + mstore(add(ptr, 0x120), arrayHashed) + digest_ := keccak256(ptr, 0x140) } } + /** + * @dev Returns whether the proposal is auto-executed on the last valid vote. + */ + function isAutoExecute(ProposalDetail memory proposal) internal pure returns (bool) { + return proposal.executor == address(0); + } + /** * @dev Returns whether the proposal is executable for the current chain. * * @notice Does not check whether the call result is successful or not. Please use `execute` instead. * */ - function executable(ProposalDetail memory _proposal) internal view returns (bool _result) { - return _proposal.chainId == 0 || _proposal.chainId == block.chainid; + function executable(ProposalDetail memory proposal) internal view returns (bool result) { + return proposal.chainId == 0 || proposal.chainId == block.chainid; } /** * @dev Executes the proposal. */ - function execute( - ProposalDetail memory _proposal - ) internal returns (bool[] memory _successCalls, bytes[] memory _returnDatas) { - if (!executable(_proposal)) revert ErrInvalidChainId(msg.sig, _proposal.chainId, block.chainid); - - _successCalls = new bool[](_proposal.targets.length); - _returnDatas = new bytes[](_proposal.targets.length); - for (uint256 _i = 0; _i < _proposal.targets.length; ) { - if (gasleft() <= _proposal.gasAmounts[_i]) revert ErrInsufficientGas(hash(_proposal)); - - (_successCalls[_i], _returnDatas[_i]) = _proposal.targets[_i].call{ - value: _proposal.values[_i], - gas: _proposal.gasAmounts[_i] - }(_proposal.calldatas[_i]); - - unchecked { - ++_i; + function execute(ProposalDetail memory proposal) internal returns (bool[] memory successCalls, bytes[] memory returnDatas) { + if (!executable(proposal)) revert ErrInvalidChainId(msg.sig, proposal.chainId, block.chainid); + + successCalls = new bool[](proposal.targets.length); + returnDatas = new bytes[](proposal.targets.length); + for (uint256 i = 0; i < proposal.targets.length; ++i) { + if (gasleft() <= proposal.gasAmounts[i]) revert ErrInsufficientGas(hash(proposal)); + + (successCalls[i], returnDatas[i]) = proposal.targets[i].call{ value: proposal.values[i], gas: proposal.gasAmounts[i] }(proposal.calldatas[i]); + + if (!proposal.loose && !successCalls[i]) { + revert ErrLooseProposalInternallyRevert(i, returnDatas[i]); } } } diff --git a/src/mainchain/MainchainBridgeManager.sol b/src/mainchain/MainchainBridgeManager.sol index 7c63d831..60f4bb7e 100644 --- a/src/mainchain/MainchainBridgeManager.sol +++ b/src/mainchain/MainchainBridgeManager.sol @@ -41,6 +41,7 @@ contract MainchainBridgeManager is BridgeManager, GovernanceRelay, GlobalGoverna Ballot.VoteType[] calldata supports_, Signature[] calldata signatures ) external onlyGovernor { + _requireExecutor(proposal.executor, msg.sender); _relayProposal(proposal, supports_, signatures, DOMAIN_SEPARATOR, msg.sender); } @@ -55,9 +56,16 @@ contract MainchainBridgeManager is BridgeManager, GovernanceRelay, GlobalGoverna Ballot.VoteType[] calldata supports_, Signature[] calldata signatures ) external onlyGovernor { + _requireExecutor(globalProposal.executor, msg.sender); _relayGlobalProposal({ globalProposal: globalProposal, supports_: supports_, signatures: signatures, domainSeparator: DOMAIN_SEPARATOR, creator: msg.sender }); } + function _requireExecutor(address executor, address caller) internal pure { + if (executor != address(0) && caller != executor) { + revert ErrNonExecutorCannotRelay(executor, caller); + } + } + /** * @dev Internal function to retrieve the minimum vote weight required for governance actions. * @return minimumVoteWeight The minimum vote weight required for governance actions. diff --git a/src/ronin/gateway/RoninBridgeManager.sol b/src/ronin/gateway/RoninBridgeManager.sol index 5ca59ab1..4a4dac4c 100644 --- a/src/ronin/gateway/RoninBridgeManager.sol +++ b/src/ronin/gateway/RoninBridgeManager.sol @@ -9,9 +9,11 @@ import { GlobalGovernanceProposal } from "../../extensions/sequential-governance/governance-proposal/GlobalGovernanceProposal.sol"; import { VoteStatusConsumer } from "../../interfaces/consumers/VoteStatusConsumer.sol"; -import { ErrQueryForEmptyVote } from "../../utils/CommonErrors.sol"; +import "../../utils/CommonErrors.sol"; contract RoninBridgeManager is BridgeManager, GovernanceProposal, GlobalGovernanceProposal { + using Proposal for Proposal.ProposalDetail; + function initialize( uint256 num, uint256 denom, @@ -42,14 +44,16 @@ contract RoninBridgeManager is BridgeManager, GovernanceProposal, GlobalGovernan * */ function propose( - uint256 _chainId, - uint256 _expiryTimestamp, - address[] calldata _targets, - uint256[] calldata _values, - bytes[] calldata _calldatas, - uint256[] calldata _gasAmounts + uint256 chainId, + uint256 expiryTimestamp, + address executor, + bool loose, + address[] calldata targets, + uint256[] calldata values, + bytes[] calldata calldatas, + uint256[] calldata gasAmounts ) external onlyGovernor { - _proposeProposal(_chainId, _expiryTimestamp, _targets, _values, _calldatas, _gasAmounts, msg.sender); + _proposeProposal(chainId, expiryTimestamp, executor, loose, targets, values, calldatas, gasAmounts, msg.sender); } /** @@ -78,6 +82,8 @@ contract RoninBridgeManager is BridgeManager, GovernanceProposal, GlobalGovernan */ function proposeProposalForCurrentNetwork( uint256 expiryTimestamp, + address executor, + bool loose, address[] calldata targets, uint256[] calldata values, bytes[] calldata calldatas, @@ -88,6 +94,8 @@ contract RoninBridgeManager is BridgeManager, GovernanceProposal, GlobalGovernan Proposal.ProposalDetail memory _proposal = _proposeProposal({ chainId: block.chainid, expiryTimestamp: expiryTimestamp, + executor: executor, + loose: loose, targets: targets, values: values, calldatas: calldatas, @@ -128,6 +136,8 @@ contract RoninBridgeManager is BridgeManager, GovernanceProposal, GlobalGovernan */ function proposeGlobal( uint256 expiryTimestamp, + address executor, + bool loose, GlobalProposal.TargetOption[] calldata targetOptions, uint256[] calldata values, bytes[] calldata calldatas, @@ -135,6 +145,8 @@ contract RoninBridgeManager is BridgeManager, GovernanceProposal, GlobalGovernan ) external onlyGovernor { _proposeGlobal({ expiryTimestamp: expiryTimestamp, + executor: executor, + loose: loose, targetOptions: targetOptions, values: values, calldatas: calldatas, @@ -179,6 +191,20 @@ contract RoninBridgeManager is BridgeManager, GovernanceProposal, GlobalGovernan * COMMON METHODS */ + /** + * @dev See {CoreGovernance-_executeWithCaller}. + */ + function execute(Proposal.ProposalDetail calldata proposal) external { + _executeWithCaller(proposal, msg.sender); + } + + /** + * @dev See {GlobalCoreGovernance-_executeWithCaller}. + */ + function executeGlobal(GlobalProposal.GlobalProposalDetail calldata globalProposal) external { + _executeGlobalWithCaller(globalProposal, msg.sender); + } + /** * @dev Deletes the expired proposal by its chainId and nonce, without creating a new proposal. * diff --git a/src/utils/CommonErrors.sol b/src/utils/CommonErrors.sol index 0dd843f5..0aedf12e 100644 --- a/src/utils/CommonErrors.sol +++ b/src/utils/CommonErrors.sol @@ -226,3 +226,25 @@ error ErrOncePerBlock(); * @dev Error of method caller must be coinbase */ error ErrCallerMustBeCoinbase(); + +/** + * @dev Error thrown when an invalid proposal is encountered. + * @param actual The actual value of the proposal. + * @param expected The expected value of the proposal. + */ +error ErrInvalidProposal(bytes32 actual, bytes32 expected); + +/** + * @dev Error of proposal is not approved for executing. + */ +error ErrProposalNotApproved(); + +/** + * @dev Error of the caller is not the specified executor. + */ +error ErrInvalidExecutor(); + +/** + * @dev Error of the `caller` to relay is not the specified `executor`. + */ +error ErrNonExecutorCannotRelay(address executor, address caller); \ No newline at end of file diff --git a/test/bridge/integration/BaseIntegration.t.sol b/test/bridge/integration/BaseIntegration.t.sol index 79bb2828..672a4a2c 100644 --- a/test/bridge/integration/BaseIntegration.t.sol +++ b/test/bridge/integration/BaseIntegration.t.sol @@ -317,6 +317,8 @@ contract BaseIntegration_Test is Base_Test { // set targets GlobalProposal.GlobalProposalDetail memory globalProposal = _roninProposalUtils.createGlobalProposal({ expiryTimestamp: block.timestamp + 10, + executor: address(0), + loose: false, targetOption: GlobalProposal.TargetOption.BridgeManager, value: 0, calldata_: abi.encodeCall(GlobalCoreGovernance.updateManyTargetOption, (param.targetOptions, param.targets)), @@ -333,6 +335,8 @@ contract BaseIntegration_Test is Base_Test { // set bridge contract GlobalProposal.GlobalProposalDetail memory globalProposal = _roninProposalUtils.createGlobalProposal({ expiryTimestamp: block.timestamp + 10, + executor: address(0), + loose: false, targetOption: GlobalProposal.TargetOption.BridgeManager, value: 0, calldata_: abi.encodeCall(IHasContracts.setContract, (ContractType.BRIDGE, param.bridgeContract)), @@ -351,6 +355,8 @@ contract BaseIntegration_Test is Base_Test { bytes memory calldata_ = abi.encodeCall(IBridgeManagerCallbackRegister.registerCallbacks, (param.callbackRegisters)); GlobalProposal.GlobalProposalDetail memory globalProposal = _roninProposalUtils.createGlobalProposal({ expiryTimestamp: block.timestamp + 10, + executor: address(0), + loose: false, targetOption: GlobalProposal.TargetOption.BridgeManager, value: 0, calldata_: calldata_, @@ -368,6 +374,8 @@ contract BaseIntegration_Test is Base_Test { // set min governors GlobalProposal.GlobalProposalDetail memory globalProposal = _roninProposalUtils.createGlobalProposal({ expiryTimestamp: block.timestamp + 10, + executor: address(0), + loose: false, targetOption: GlobalProposal.TargetOption.BridgeManager, value: 0, calldata_: abi.encodeCall(IBridgeManager.setMinRequiredGovernor, (_param.roninBridgeManager.minRequiredGovernor)), @@ -406,6 +414,8 @@ contract BaseIntegration_Test is Base_Test { // set targets GlobalProposal.GlobalProposalDetail memory globalProposal = _mainchainProposalUtils.createGlobalProposal({ expiryTimestamp: block.timestamp + 10, + executor: address(0), + loose: false, targetOption: GlobalProposal.TargetOption.BridgeManager, value: 0, calldata_: abi.encodeCall(GlobalCoreGovernance.updateManyTargetOption, (param.targetOptions, param.targets)), @@ -422,6 +432,8 @@ contract BaseIntegration_Test is Base_Test { // set bridge contract GlobalProposal.GlobalProposalDetail memory globalProposal = _mainchainProposalUtils.createGlobalProposal({ expiryTimestamp: block.timestamp + 10, + executor: address(0), + loose: false, targetOption: GlobalProposal.TargetOption.BridgeManager, value: 0, calldata_: abi.encodeCall(IHasContracts.setContract, (ContractType.BRIDGE, param.bridgeContract)), @@ -440,6 +452,8 @@ contract BaseIntegration_Test is Base_Test { bytes memory calldata_ = abi.encodeCall(IBridgeManagerCallbackRegister.registerCallbacks, (param.callbackRegisters)); GlobalProposal.GlobalProposalDetail memory globalProposal = _mainchainProposalUtils.createGlobalProposal({ expiryTimestamp: block.timestamp + 10, + executor: address(0), + loose: false, targetOption: GlobalProposal.TargetOption.BridgeManager, value: 0, calldata_: calldata_, @@ -457,6 +471,8 @@ contract BaseIntegration_Test is Base_Test { // set min governors GlobalProposal.GlobalProposalDetail memory globalProposal = _roninProposalUtils.createGlobalProposal({ expiryTimestamp: block.timestamp + 10, + executor: address(0), + loose: false, targetOption: GlobalProposal.TargetOption.BridgeManager, value: 0, calldata_: abi.encodeCall(IBridgeManager.setMinRequiredGovernor, (_param.roninBridgeManager.minRequiredGovernor)), diff --git a/test/bridge/integration/bridge-manager/proposal-executor/executor.proposeCurrent.RoninBridgeManager.t.sol b/test/bridge/integration/bridge-manager/proposal-executor/executor.proposeCurrent.RoninBridgeManager.t.sol new file mode 100644 index 00000000..6acebfa4 --- /dev/null +++ b/test/bridge/integration/bridge-manager/proposal-executor/executor.proposeCurrent.RoninBridgeManager.t.sol @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import { console2 as console } from "forge-std/console2.sol"; +import { GlobalProposal } from "@ronin/contracts/libraries/GlobalProposal.sol"; +import { Ballot } from "@ronin/contracts/libraries/Ballot.sol"; +import { ContractType } from "@ronin/contracts/utils/ContractType.sol"; +import { IBridgeManager } from "@ronin/contracts/interfaces/bridge/IBridgeManager.sol"; +import { SignatureConsumer } from "@ronin/contracts/interfaces/consumers/SignatureConsumer.sol"; +import { LibSort } from "solady/utils/LibSort.sol"; + +import "../../BaseIntegration.t.sol"; + +contract ProposalWithExecutor_CurrentNetworkProposal_RoninBridgeManager_Test is BaseIntegration_Test { + event ProposalVoted(bytes32 indexed proposalHash, address indexed voter, Ballot.VoteType support, uint256 weight); + event ProposalApproved(bytes32 indexed proposalHash); + event ProposalExecuted(bytes32 indexed proposalHash, bool[] successCalls, bytes[] returnDatas); + + error ErrInvalidExecutor(); + error ErrProposalNotApproved(); + error ErrInvalidProposalNonce(bytes4 sig); + error ErrLooseProposalInternallyRevert(uint, bytes); + + using LibSort for address[]; + + uint256 _proposalExpiryDuration; + uint256 _addingOperatorNum; + address[] _addingOperators; + address[] _addingGovernors; + uint96[] _voteWeights; + + address[] _beforeRelayedOperators; + address[] _beforeRelayedGovernors; + + address[] _afterRelayedOperators; + address[] _afterRelayedGovernors; + + Ballot.VoteType[] _supports; + + GlobalProposal.GlobalProposalDetail _globalProposal; + SignatureConsumer.Signature[] _signatures; + + Proposal.ProposalDetail _proposal; + + bytes32 _anyValue; + + function setUp() public virtual override { + super.setUp(); + + _proposalExpiryDuration = 60; + _addingOperatorNum = 3; + + _beforeRelayedOperators = _param.roninBridgeManager.bridgeOperators; + _beforeRelayedGovernors = _param.roninBridgeManager.governors; + + _supports = new Ballot.VoteType[](_beforeRelayedOperators.length); + for (uint256 i; i < _beforeRelayedGovernors.length; i++) { + _supports[i] = Ballot.VoteType.For; + } + + _generateAddingOperators(_addingOperatorNum); + + _proposal.nonce = _roninBridgeManager.round(block.chainid) + 1; + _proposal.chainId = block.chainid; + _proposal.executor = address(0); + _proposal.loose = false; + _proposal.expiryTimestamp = block.timestamp + _proposalExpiryDuration; + + _proposal.targets.push(address(_roninBridgeManager)); + _proposal.values.push(0); + _proposal.calldatas.push(abi.encodeCall(IBridgeManager.addBridgeOperators, (_voteWeights, _addingGovernors, _addingOperators))); + _proposal.gasAmounts.push(1_000_000); + + // Duplicate the internal call + _proposal.targets.push(address(_roninBridgeManager)); + _proposal.values.push(0); + _proposal.calldatas.push(abi.encodeCall(IBridgeManager.addBridgeOperators, (_voteWeights, _addingGovernors, _addingOperators))); + _proposal.gasAmounts.push(1_000_000); + } + + // Should the auto proposal executes on the last valid vote + function test_autoProposal_strictProposal_WhenAllInternalCallsPass() public { + _proposal.loose = false; + _proposal.executor = address(0); + + vm.expectEmit(false, true, true, true); + emit ProposalVoted(_anyValue, _param.roninBridgeManager.governors[0], Ballot.VoteType.For, 100); + vm.expectEmit(false, false, false, false); + emit ProposalApproved(_anyValue); + vm.expectEmit(false, false, false, false); + emit ProposalExecuted(_anyValue, new bool[](2), new bytes[](2)); + + SignatureConsumer.Signature[] memory signatures = _roninProposalUtils.generateSignatures(_proposal, _param.test.governorPKs); + for (uint256 i; i < signatures.length; i++) { + _signatures.push(signatures[i]); + } + + vm.prank(_param.roninBridgeManager.governors[0]); + _roninBridgeManager.proposeProposalStructAndCastVotes(_proposal, _supports, _signatures); + + assertEq(_roninBridgeManager.proposalVoted(block.chainid, _proposal.nonce, _param.roninBridgeManager.governors[0]), true); + assertEq(_roninBridgeManager.getBridgeOperators(), _afterRelayedOperators); + } + + // Should revert when the non-auto proposal get executed again + function test_autoProposal_revertWhen_proposalIsAlreadyExecuted() external { + test_autoProposal_strictProposal_WhenAllInternalCallsPass(); + + vm.expectRevert(abi.encodeWithSelector(ErrProposalNotApproved.selector)); + + vm.prank(_param.roninBridgeManager.governors[0]); + _roninBridgeManager.execute(_proposal); + } + + // Should the non-auto proposal be execute by the specified executor + function test_executorProposal_strictProposal_WhenAllInternalCallsPass() public { + _proposal.loose = false; + _proposal.executor = _param.roninBridgeManager.governors[0]; + _proposal.gasAmounts[1] = 1_000_000; // Set gas for the second call becomes success + + vm.expectEmit(false, true, true, true); + emit ProposalVoted(_anyValue, _param.roninBridgeManager.governors[0], Ballot.VoteType.For, 100); + vm.expectEmit(false, false, false, false); + emit ProposalApproved(_anyValue); + + SignatureConsumer.Signature[] memory signatures = _roninProposalUtils.generateSignatures(_proposal, _param.test.governorPKs); + for (uint256 i; i < signatures.length; i++) { + _signatures.push(signatures[i]); + } + + vm.prank(_param.roninBridgeManager.governors[0]); + _roninBridgeManager.proposeProposalStructAndCastVotes(_proposal, _supports, _signatures); + assertEq(_roninBridgeManager.proposalVoted(block.chainid, _proposal.nonce, _param.roninBridgeManager.governors[0]), true); + assertEq(_roninBridgeManager.getBridgeOperators(), _beforeRelayedOperators); + + vm.expectEmit(false, false, false, false); + emit ProposalExecuted(_anyValue, new bool[](2), new bytes[](2)); + + vm.prank(_param.roninBridgeManager.governors[0]); + _roninBridgeManager.execute(_proposal); + assertEq(_roninBridgeManager.getBridgeOperators(), _afterRelayedOperators); + } + + // Should revert when the auto proposal get executed again + function test_executorProposal_revertWhen_proposalIsAlreadyExecuted() external { + test_executorProposal_strictProposal_WhenAllInternalCallsPass(); + + vm.expectRevert(abi.encodeWithSelector(ErrProposalNotApproved.selector)); + + vm.prank(_param.roninBridgeManager.governors[0]); + _roninBridgeManager.execute(_proposal); + } + + // Should the non-auto proposal can not be execute by other governor + function test_executorProposal_revertWhen_proposalIsExecutedByAnotherGovernor() external { + _proposal.loose = false; + _proposal.executor = _param.roninBridgeManager.governors[0]; + _proposal.gasAmounts[1] = 1_000_000; // Set gas for the second call becomes success + + vm.expectEmit(false, true, true, true); + emit ProposalVoted(_anyValue, _param.roninBridgeManager.governors[0], Ballot.VoteType.For, 100); + vm.expectEmit(false, false, false, false); + emit ProposalApproved(_anyValue); + + SignatureConsumer.Signature[] memory signatures = _roninProposalUtils.generateSignatures(_proposal, _param.test.governorPKs); + for (uint256 i; i < signatures.length; i++) { + _signatures.push(signatures[i]); + } + + vm.prank(_param.roninBridgeManager.governors[0]); + _roninBridgeManager.proposeProposalStructAndCastVotes(_proposal, _supports, _signatures); + assertEq(_roninBridgeManager.proposalVoted(block.chainid, _proposal.nonce, _param.roninBridgeManager.governors[0]), true); + assertEq(_roninBridgeManager.getBridgeOperators(), _beforeRelayedOperators); + + vm.expectRevert(abi.encodeWithSelector(ErrInvalidExecutor.selector)); + vm.prank(_param.roninBridgeManager.governors[1]); + _roninBridgeManager.execute(_proposal); + } + + function _generateAddingOperators(uint256 num) internal { + delete _addingOperators; + delete _addingGovernors; + delete _voteWeights; + + _afterRelayedOperators = _beforeRelayedOperators; + _afterRelayedGovernors = _beforeRelayedGovernors; + + for (uint256 i; i < num; i++) { + _addingOperators.push(makeAddr(string.concat("adding-operator", vm.toString(i)))); + _addingGovernors.push(makeAddr(string.concat("adding-governor", vm.toString(i)))); + _voteWeights.push(uint96(uint256(100))); + + _afterRelayedOperators.push(_addingOperators[i]); + _afterRelayedGovernors.push(_addingGovernors[i]); + } + } +} diff --git a/test/bridge/integration/bridge-manager/proposal-executor/executor.proposeGlobal.RoninBridgeManager.t.sol b/test/bridge/integration/bridge-manager/proposal-executor/executor.proposeGlobal.RoninBridgeManager.t.sol new file mode 100644 index 00000000..bf524b16 --- /dev/null +++ b/test/bridge/integration/bridge-manager/proposal-executor/executor.proposeGlobal.RoninBridgeManager.t.sol @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import { console2 as console } from "forge-std/console2.sol"; +import { GlobalProposal } from "@ronin/contracts/libraries/GlobalProposal.sol"; +import { Ballot } from "@ronin/contracts/libraries/Ballot.sol"; +import { ContractType } from "@ronin/contracts/utils/ContractType.sol"; +import { IBridgeManager } from "@ronin/contracts/interfaces/bridge/IBridgeManager.sol"; +import { SignatureConsumer } from "@ronin/contracts/interfaces/consumers/SignatureConsumer.sol"; +import { LibSort } from "solady/utils/LibSort.sol"; + +import "../../BaseIntegration.t.sol"; + +contract ProposalWithExecutor_GlobalProposal_RoninBridgeManager_Test is BaseIntegration_Test { + event ProposalVoted(bytes32 indexed proposalHash, address indexed voter, Ballot.VoteType support, uint256 weight); + event ProposalApproved(bytes32 indexed proposalHash); + event ProposalExecuted(bytes32 indexed proposalHash, bool[] successCalls, bytes[] returnDatas); + + error ErrInvalidExecutor(); + error ErrProposalNotApproved(); + error ErrInvalidProposalNonce(bytes4 sig); + error ErrLooseProposalInternallyRevert(uint, bytes); + + using LibSort for address[]; + + uint256 _proposalExpiryDuration; + uint256 _addingOperatorNum; + address[] _addingOperators; + address[] _addingGovernors; + uint96[] _voteWeights; + + address[] _beforeRelayedOperators; + address[] _beforeRelayedGovernors; + + address[] _afterRelayedOperators; + address[] _afterRelayedGovernors; + + Ballot.VoteType[] _supports; + + GlobalProposal.GlobalProposalDetail _globalProposal; + SignatureConsumer.Signature[] _signatures; + + bytes32 _anyValue; + + function setUp() public virtual override { + super.setUp(); + + _proposalExpiryDuration = 60; + _addingOperatorNum = 3; + + _beforeRelayedOperators = _param.roninBridgeManager.bridgeOperators; + _beforeRelayedGovernors = _param.roninBridgeManager.governors; + + _supports = new Ballot.VoteType[](_beforeRelayedOperators.length); + for (uint256 i; i < _beforeRelayedGovernors.length; i++) { + _supports[i] = Ballot.VoteType.For; + } + + _generateAddingOperators(_addingOperatorNum); + + _globalProposal.nonce = _roninBridgeManager.round(0) + 1; + _globalProposal.executor = address(0); + _globalProposal.loose = false; + _globalProposal.expiryTimestamp = block.timestamp + _proposalExpiryDuration; + + _globalProposal.targetOptions.push(GlobalProposal.TargetOption.BridgeManager); + _globalProposal.values.push(0); + _globalProposal.calldatas.push(abi.encodeCall(IBridgeManager.addBridgeOperators, (_voteWeights, _addingGovernors, _addingOperators))); + _globalProposal.gasAmounts.push(1_000_000); + + // Duplicate the internal call + _globalProposal.targetOptions.push(GlobalProposal.TargetOption.BridgeManager); + _globalProposal.values.push(0); + _globalProposal.calldatas.push(abi.encodeCall(IBridgeManager.addBridgeOperators, (_voteWeights, _addingGovernors, _addingOperators))); + _globalProposal.gasAmounts.push(1_000_000); + } + + // Should the auto proposal executes on the last valid vote + function test_autoProposal_strictProposal_WhenAllInternalCallsPass() public { + _globalProposal.loose = false; + _globalProposal.executor = address(0); + + vm.expectEmit(false, true, true, true); + emit ProposalVoted(_anyValue, _param.roninBridgeManager.governors[0], Ballot.VoteType.For, 100); + vm.expectEmit(false, false, false, false); + emit ProposalApproved(_anyValue); + vm.expectEmit(false, false, false, false); + emit ProposalExecuted(_anyValue, new bool[](2), new bytes[](2)); + + SignatureConsumer.Signature[] memory signatures = _roninProposalUtils.generateSignaturesGlobal(_globalProposal, _param.test.governorPKs); + for (uint256 i; i < signatures.length; i++) { + _signatures.push(signatures[i]); + } + + vm.prank(_param.roninBridgeManager.governors[0]); + _roninBridgeManager.proposeGlobalProposalStructAndCastVotes(_globalProposal, _supports, _signatures); + + assertEq(_roninBridgeManager.globalProposalVoted(_globalProposal.nonce, _param.roninBridgeManager.governors[0]), true); + assertEq(_roninBridgeManager.getBridgeOperators(), _afterRelayedOperators); + } + + // Should revert when the non-auto proposal get executed again + function test_autoProposal_revertWhen_proposalIsAlreadyExecuted() external { + test_autoProposal_strictProposal_WhenAllInternalCallsPass(); + + vm.expectRevert(abi.encodeWithSelector(ErrProposalNotApproved.selector)); + + vm.prank(_param.roninBridgeManager.governors[0]); + _roninBridgeManager.executeGlobal(_globalProposal); + } + + // Should the non-auto proposal be execute by the specified executor + function test_executorProposal_strictProposal_WhenAllInternalCallsPass() public { + _globalProposal.loose = false; + _globalProposal.executor = _param.roninBridgeManager.governors[0]; + _globalProposal.gasAmounts[1] = 1_000_000; // Set gas for the second call becomes success + + vm.expectEmit(false, true, true, true); + emit ProposalVoted(_anyValue, _param.roninBridgeManager.governors[0], Ballot.VoteType.For, 100); + vm.expectEmit(false, false, false, false); + emit ProposalApproved(_anyValue); + + SignatureConsumer.Signature[] memory signatures = _roninProposalUtils.generateSignaturesGlobal(_globalProposal, _param.test.governorPKs); + for (uint256 i; i < signatures.length; i++) { + _signatures.push(signatures[i]); + } + + vm.prank(_param.roninBridgeManager.governors[0]); + _roninBridgeManager.proposeGlobalProposalStructAndCastVotes(_globalProposal, _supports, _signatures); + assertEq(_roninBridgeManager.globalProposalVoted(_globalProposal.nonce, _param.roninBridgeManager.governors[0]), true); + assertEq(_roninBridgeManager.getBridgeOperators(), _beforeRelayedOperators); + + vm.expectEmit(false, false, false, false); + emit ProposalExecuted(_anyValue, new bool[](2), new bytes[](2)); + + vm.prank(_param.roninBridgeManager.governors[0]); + _roninBridgeManager.executeGlobal(_globalProposal); + assertEq(_roninBridgeManager.getBridgeOperators(), _afterRelayedOperators); + } + + // Should revert when the auto proposal get executed again + function test_executorProposal_revertWhen_proposalIsAlreadyExecuted() external { + test_executorProposal_strictProposal_WhenAllInternalCallsPass(); + + vm.expectRevert(abi.encodeWithSelector(ErrProposalNotApproved.selector)); + + vm.prank(_param.roninBridgeManager.governors[0]); + _roninBridgeManager.executeGlobal(_globalProposal); + } + + // Should the non-auto proposal can not be execute by other governor + function test_executorProposal_revertWhen_proposalIsExecutedByAnotherGovernor() external { + _globalProposal.loose = false; + _globalProposal.executor = _param.roninBridgeManager.governors[0]; + _globalProposal.gasAmounts[1] = 1_000_000; // Set gas for the second call becomes success + + vm.expectEmit(false, true, true, true); + emit ProposalVoted(_anyValue, _param.roninBridgeManager.governors[0], Ballot.VoteType.For, 100); + vm.expectEmit(false, false, false, false); + emit ProposalApproved(_anyValue); + + SignatureConsumer.Signature[] memory signatures = _roninProposalUtils.generateSignaturesGlobal(_globalProposal, _param.test.governorPKs); + for (uint256 i; i < signatures.length; i++) { + _signatures.push(signatures[i]); + } + + vm.prank(_param.roninBridgeManager.governors[0]); + _roninBridgeManager.proposeGlobalProposalStructAndCastVotes(_globalProposal, _supports, _signatures); + assertEq(_roninBridgeManager.globalProposalVoted(_globalProposal.nonce, _param.roninBridgeManager.governors[0]), true); + assertEq(_roninBridgeManager.getBridgeOperators(), _beforeRelayedOperators); + + vm.expectRevert(abi.encodeWithSelector(ErrInvalidExecutor.selector)); + vm.prank(_param.roninBridgeManager.governors[1]); + _roninBridgeManager.executeGlobal(_globalProposal); + } + + function _generateAddingOperators(uint256 num) internal { + delete _addingOperators; + delete _addingGovernors; + delete _voteWeights; + + _afterRelayedOperators = _beforeRelayedOperators; + _afterRelayedGovernors = _beforeRelayedGovernors; + + for (uint256 i; i < num; i++) { + _addingOperators.push(makeAddr(string.concat("adding-operator", vm.toString(i)))); + _addingGovernors.push(makeAddr(string.concat("adding-governor", vm.toString(i)))); + _voteWeights.push(uint96(uint256(100))); + + _afterRelayedOperators.push(_addingOperators[i]); + _afterRelayedGovernors.push(_addingGovernors[i]); + } + } +} diff --git a/test/bridge/integration/bridge-manager/proposal-executor/executor.proposeMainchain.RoninBridgeManager.t.sol b/test/bridge/integration/bridge-manager/proposal-executor/executor.proposeMainchain.RoninBridgeManager.t.sol new file mode 100644 index 00000000..53467717 --- /dev/null +++ b/test/bridge/integration/bridge-manager/proposal-executor/executor.proposeMainchain.RoninBridgeManager.t.sol @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import { console2 as console } from "forge-std/console2.sol"; +import { GlobalProposal } from "@ronin/contracts/libraries/GlobalProposal.sol"; +import { Ballot } from "@ronin/contracts/libraries/Ballot.sol"; +import { ContractType } from "@ronin/contracts/utils/ContractType.sol"; +import { IBridgeManager } from "@ronin/contracts/interfaces/bridge/IBridgeManager.sol"; +import { SignatureConsumer } from "@ronin/contracts/interfaces/consumers/SignatureConsumer.sol"; +import { LibSort } from "solady/utils/LibSort.sol"; + +import "../../BaseIntegration.t.sol"; + +contract ProposalWithExecutor_MainchainProposal_RoninBridgeManager_Test is BaseIntegration_Test { + event ProposalVoted(bytes32 indexed proposalHash, address indexed voter, Ballot.VoteType support, uint256 weight); + event ProposalApproved(bytes32 indexed proposalHash); + event ProposalExecuted(bytes32 indexed proposalHash, bool[] successCalls, bytes[] returnDatas); + + error ErrInvalidExecutor(); + error ErrProposalNotApproved(); + error ErrInvalidProposalNonce(bytes4 sig); + error ErrLooseProposalInternallyRevert(uint, bytes); + + using LibSort for address[]; + + uint256 _proposalExpiryDuration; + uint256 _addingOperatorNum; + address[] _addingOperators; + address[] _addingGovernors; + uint96[] _voteWeights; + + address[] _beforeRelayedOperators; + address[] _beforeRelayedGovernors; + + address[] _afterRelayedOperators; + address[] _afterRelayedGovernors; + + Ballot.VoteType[] _supports; + + Proposal.ProposalDetail _proposal; + SignatureConsumer.Signature[] _signatures; + + bytes32 _anyValue; + + function setUp() public virtual override { + super.setUp(); + + _proposalExpiryDuration = 60; + _addingOperatorNum = 3; + + _beforeRelayedOperators = _param.roninBridgeManager.bridgeOperators; + _beforeRelayedGovernors = _param.roninBridgeManager.governors; + + _supports = new Ballot.VoteType[](_beforeRelayedOperators.length); + for (uint256 i; i < _beforeRelayedGovernors.length; i++) { + _supports[i] = Ballot.VoteType.For; + } + + _generateAddingOperators(_addingOperatorNum); + + _proposal.nonce = _roninBridgeManager.round(block.chainid) + 1; + _proposal.chainId = block.chainid; + _proposal.executor = address(0); + _proposal.loose = false; + _proposal.expiryTimestamp = block.timestamp + _proposalExpiryDuration; + + _proposal.targets.push(address(_mainchainBridgeManager)); // Test Relay + _proposal.values.push(0); + _proposal.calldatas.push(abi.encodeCall(IBridgeManager.addBridgeOperators, (_voteWeights, _addingGovernors, _addingOperators))); + _proposal.gasAmounts.push(1_000_000); + + // Duplicate the internal call + _proposal.targets.push(address(_mainchainBridgeManager)); // Test Relay + _proposal.values.push(0); + _proposal.calldatas.push(abi.encodeCall(IBridgeManager.addBridgeOperators, (_voteWeights, _addingGovernors, _addingOperators))); + _proposal.gasAmounts.push(1_000_000); + } + + // Should the proposal is approved but not executed on Ronin chain + function test_proposeMainchain_autoProposal_looseProposal() public { + _proposal.loose = true; + _proposal.executor = address(0); + + vm.expectEmit(false, true, true, true); + emit ProposalVoted(_anyValue, _param.roninBridgeManager.governors[0], Ballot.VoteType.For, 100); + vm.expectEmit(false, false, false, false); + emit ProposalApproved(_anyValue); + vm.expectEmit(false, false, false, false); + emit ProposalExecuted(_anyValue, new bool[](2), new bytes[](2)); + + SignatureConsumer.Signature[] memory signatures = _roninProposalUtils.generateSignatures(_proposal, _param.test.governorPKs); + for (uint256 i; i < signatures.length; i++) { + _signatures.push(signatures[i]); + } + + vm.prank(_param.roninBridgeManager.governors[0]); + _roninBridgeManager.proposeProposalStructAndCastVotes(_proposal, _supports, _signatures); + + assertEq(_roninBridgeManager.proposalVoted(block.chainid, _proposal.nonce, _param.roninBridgeManager.governors[0]), true); + + // Mainchain proposal does not take effect on Ronin chain + assertEq(_roninBridgeManager.getBridgeOperators(), _beforeRelayedOperators); + } + + // Should the non-auto proposal be execute by the specified executor + function test_proposeMainchain_executorProposal_looseProposal_WhenAllInternalCallsPass() public { + _proposal.loose = true; + _proposal.executor = _param.roninBridgeManager.governors[0]; + _proposal.gasAmounts[1] = 1_000_000; // Set gas for the second call becomes success + + vm.expectEmit(false, true, true, true); + emit ProposalVoted(_anyValue, _param.roninBridgeManager.governors[0], Ballot.VoteType.For, 100); + vm.expectEmit(false, false, false, false); + emit ProposalApproved(_anyValue); + + SignatureConsumer.Signature[] memory signatures = _roninProposalUtils.generateSignatures(_proposal, _param.test.governorPKs); + for (uint256 i; i < signatures.length; i++) { + _signatures.push(signatures[i]); + } + + vm.prank(_param.roninBridgeManager.governors[0]); + _roninBridgeManager.proposeProposalStructAndCastVotes(_proposal, _supports, _signatures); + assertEq(_roninBridgeManager.proposalVoted(block.chainid, _proposal.nonce, _param.roninBridgeManager.governors[0]), true); + assertEq(_roninBridgeManager.getBridgeOperators(), _beforeRelayedOperators); + + vm.expectEmit(false, false, false, false); + emit ProposalExecuted(_anyValue, new bool[](2), new bytes[](2)); + + vm.prank(_param.roninBridgeManager.governors[0]); + _roninBridgeManager.execute(_proposal); + + // Mainchain proposal does not take effect on Ronin chain + assertEq(_roninBridgeManager.getBridgeOperators(), _beforeRelayedOperators); + } + + + // Should the non-auto proposal can not be execute by other governor + function test_proposeMainchain_executorProposal_revertWhen_proposalIsExecutedByAnotherGovernor() external { + _proposal.loose = false; + _proposal.executor = _param.roninBridgeManager.governors[0]; + _proposal.gasAmounts[1] = 1_000_000; // Set gas for the second call becomes success + + vm.expectEmit(false, true, true, true); + emit ProposalVoted(_anyValue, _param.roninBridgeManager.governors[0], Ballot.VoteType.For, 100); + vm.expectEmit(false, false, false, false); + emit ProposalApproved(_anyValue); + + SignatureConsumer.Signature[] memory signatures = _roninProposalUtils.generateSignatures(_proposal, _param.test.governorPKs); + for (uint256 i; i < signatures.length; i++) { + _signatures.push(signatures[i]); + } + + vm.prank(_param.roninBridgeManager.governors[0]); + _roninBridgeManager.proposeProposalStructAndCastVotes(_proposal, _supports, _signatures); + assertEq(_roninBridgeManager.proposalVoted(block.chainid, _proposal.nonce, _param.roninBridgeManager.governors[0]), true); + assertEq(_roninBridgeManager.getBridgeOperators(), _beforeRelayedOperators); + + vm.expectRevert(abi.encodeWithSelector(ErrInvalidExecutor.selector)); + vm.prank(_param.roninBridgeManager.governors[1]); + _roninBridgeManager.execute(_proposal); + } + + function _generateAddingOperators(uint256 num) internal { + delete _addingOperators; + delete _addingGovernors; + delete _voteWeights; + + _afterRelayedOperators = _beforeRelayedOperators; + _afterRelayedGovernors = _beforeRelayedGovernors; + + for (uint256 i; i < num; i++) { + _addingOperators.push(makeAddr(string.concat("adding-operator", vm.toString(i)))); + _addingGovernors.push(makeAddr(string.concat("adding-governor", vm.toString(i)))); + _voteWeights.push(uint96(uint256(100))); + + _afterRelayedOperators.push(_addingOperators[i]); + _afterRelayedGovernors.push(_addingGovernors[i]); + } + } +} diff --git a/test/bridge/integration/bridge-manager/proposal-executor/executor.relayGlobal.MainchainBridgeManager.t.sol b/test/bridge/integration/bridge-manager/proposal-executor/executor.relayGlobal.MainchainBridgeManager.t.sol new file mode 100644 index 00000000..fba61e10 --- /dev/null +++ b/test/bridge/integration/bridge-manager/proposal-executor/executor.relayGlobal.MainchainBridgeManager.t.sol @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import { console2 as console } from "forge-std/console2.sol"; +import { GlobalProposal } from "@ronin/contracts/libraries/GlobalProposal.sol"; +import { Ballot } from "@ronin/contracts/libraries/Ballot.sol"; +import { ContractType } from "@ronin/contracts/utils/ContractType.sol"; +import { IBridgeManager } from "@ronin/contracts/interfaces/bridge/IBridgeManager.sol"; +import { SignatureConsumer } from "@ronin/contracts/interfaces/consumers/SignatureConsumer.sol"; +import { LibSort } from "solady/utils/LibSort.sol"; + +import "../../BaseIntegration.t.sol"; + +contract ProposalWithExecutor_GlobalProposal_MainchainBridgeManager_Test is BaseIntegration_Test { + event ProposalVoted(bytes32 indexed proposalHash, address indexed voter, Ballot.VoteType support, uint256 weight); + event ProposalApproved(bytes32 indexed proposalHash); + event ProposalExecuted(bytes32 indexed proposalHash, bool[] successCalls, bytes[] returnDatas); + + error ErrInvalidExecutor(); + error ErrProposalNotApproved(); + error ErrInvalidProposalNonce(bytes4 sig); + error ErrNonExecutorCannotRelay(address executor, address caller); + error ErrLooseProposalInternallyRevert(uint, bytes); + + using LibSort for address[]; + + uint256 _proposalExpiryDuration; + uint256 _addingOperatorNum; + address[] _addingOperators; + address[] _addingGovernors; + uint96[] _voteWeights; + + address[] _beforeRelayedOperators; + address[] _beforeRelayedGovernors; + + address[] _afterRelayedOperators; + address[] _afterRelayedGovernors; + + Ballot.VoteType[] _supports; + + GlobalProposal.GlobalProposalDetail _globalProposal; + SignatureConsumer.Signature[] _signatures; + + bytes32 _anyValue; + + function setUp() public virtual override { + super.setUp(); + + _proposalExpiryDuration = 60; + _addingOperatorNum = 3; + + _beforeRelayedOperators = _param.roninBridgeManager.bridgeOperators; + _beforeRelayedGovernors = _param.roninBridgeManager.governors; + + _supports = new Ballot.VoteType[](_beforeRelayedOperators.length); + for (uint256 i; i < _beforeRelayedGovernors.length; i++) { + _supports[i] = Ballot.VoteType.For; + } + + _generateAddingOperators(_addingOperatorNum); + + _globalProposal.nonce = _mainchainBridgeManager.round(0) + 1; + _globalProposal.executor = address(0); + _globalProposal.loose = false; + _globalProposal.expiryTimestamp = block.timestamp + _proposalExpiryDuration; + + _globalProposal.targetOptions.push(GlobalProposal.TargetOption.BridgeManager); + _globalProposal.values.push(0); + _globalProposal.calldatas.push(abi.encodeCall(IBridgeManager.addBridgeOperators, (_voteWeights, _addingGovernors, _addingOperators))); + _globalProposal.gasAmounts.push(1_000_000); + + // Duplicate the internal call + _globalProposal.targetOptions.push(GlobalProposal.TargetOption.BridgeManager); + _globalProposal.values.push(0); + _globalProposal.calldatas.push(abi.encodeCall(IBridgeManager.addBridgeOperators, (_voteWeights, _addingGovernors, _addingOperators))); + _globalProposal.gasAmounts.push(1_000_000); + } + + // Should the auto proposal executes on the last valid vote + function test_relayGlobal_autoProposal_strictProposal_WhenAllInternalCallsPass() public { + _globalProposal.loose = false; + _globalProposal.executor = address(0); + + vm.expectEmit(false, false, false, false); + emit ProposalApproved(_anyValue); + vm.expectEmit(false, false, false, false); + emit ProposalExecuted(_anyValue, new bool[](2), new bytes[](2)); + + SignatureConsumer.Signature[] memory signatures = _roninProposalUtils.generateSignaturesGlobal(_globalProposal, _param.test.governorPKs); + for (uint256 i; i < signatures.length; i++) { + _signatures.push(signatures[i]); + } + + vm.prank(_param.roninBridgeManager.governors[0]); + _mainchainBridgeManager.relayGlobalProposal(_globalProposal, _supports, _signatures); + + assertEq(_mainchainBridgeManager.globalProposalRelayed(_globalProposal.nonce), true); + assertEq(_mainchainBridgeManager.getBridgeOperators(), _afterRelayedOperators); + } + + // Should revert when the non-auto proposal get executed again + function test_relayGlobal_autoProposal_revertWhen_proposalIsAlreadyExecuted() external { + test_relayGlobal_autoProposal_strictProposal_WhenAllInternalCallsPass(); + + vm.expectRevert(abi.encodeWithSelector(ErrInvalidProposalNonce.selector, MainchainBridgeManager.relayGlobalProposal.selector)); + + vm.prank(_param.roninBridgeManager.governors[0]); + _mainchainBridgeManager.relayGlobalProposal(_globalProposal, _supports, _signatures); + } + + // Should the non-auto proposal be execute by the specified executor + function test_relayGlobal_executorProposal_strictProposal_WhenAllInternalCallsPass() public { + _globalProposal.loose = false; + _globalProposal.executor = _param.roninBridgeManager.governors[0]; + _globalProposal.gasAmounts[1] = 1_000_000; // Set gas for the second call becomes success + + vm.expectEmit(false, false, false, false); + emit ProposalApproved(_anyValue); + vm.expectEmit(false, false, false, false); + emit ProposalExecuted(_anyValue, new bool[](2), new bytes[](2)); + + SignatureConsumer.Signature[] memory signatures = _roninProposalUtils.generateSignaturesGlobal(_globalProposal, _param.test.governorPKs); + for (uint256 i; i < signatures.length; i++) { + _signatures.push(signatures[i]); + } + + vm.prank(_param.roninBridgeManager.governors[0]); + _mainchainBridgeManager.relayGlobalProposal(_globalProposal, _supports, _signatures); + assertEq(_mainchainBridgeManager.globalProposalRelayed(_globalProposal.nonce), true); + assertEq(_mainchainBridgeManager.getBridgeOperators(), _afterRelayedOperators); + } + + // Should revert when the auto proposal get executed again + function test_relayGlobal_executorProposal_revertWhen_proposalIsAlreadyExecuted() external { + test_relayGlobal_executorProposal_strictProposal_WhenAllInternalCallsPass(); + + vm.expectRevert(abi.encodeWithSelector(ErrInvalidProposalNonce.selector, MainchainBridgeManager.relayGlobalProposal.selector)); + + vm.prank(_param.roninBridgeManager.governors[0]); + _mainchainBridgeManager.relayGlobalProposal(_globalProposal, _supports, _signatures); + } + + // Should the non-auto proposal can not be execute by other governor + function test_relayGlobal_executorProposal_revertWhen_proposalIsExecutedByAnotherGovernor() external { + _globalProposal.loose = false; + _globalProposal.executor = _param.roninBridgeManager.governors[0]; + _globalProposal.gasAmounts[1] = 1_000_000; // Set gas for the second call becomes success + + SignatureConsumer.Signature[] memory signatures = _roninProposalUtils.generateSignaturesGlobal(_globalProposal, _param.test.governorPKs); + for (uint256 i; i < signatures.length; i++) { + _signatures.push(signatures[i]); + } + vm.expectRevert(abi.encodeWithSelector(ErrNonExecutorCannotRelay.selector, _param.roninBridgeManager.governors[0], _param.roninBridgeManager.governors[1])); + + vm.prank(_param.roninBridgeManager.governors[1]); + _mainchainBridgeManager.relayGlobalProposal(_globalProposal, _supports, _signatures); + } + + function _generateAddingOperators(uint256 num) internal { + delete _addingOperators; + delete _addingGovernors; + delete _voteWeights; + + _afterRelayedOperators = _beforeRelayedOperators; + _afterRelayedGovernors = _beforeRelayedGovernors; + + for (uint256 i; i < num; i++) { + _addingOperators.push(makeAddr(string.concat("adding-operator", vm.toString(i)))); + _addingGovernors.push(makeAddr(string.concat("adding-governor", vm.toString(i)))); + _voteWeights.push(uint96(uint256(100))); + + _afterRelayedOperators.push(_addingOperators[i]); + _afterRelayedGovernors.push(_addingGovernors[i]); + } + } +} diff --git a/test/bridge/integration/bridge-manager/proposal-executor/executor.relayMainchain.MainchainBridgeManager.t.sol b/test/bridge/integration/bridge-manager/proposal-executor/executor.relayMainchain.MainchainBridgeManager.t.sol new file mode 100644 index 00000000..89818604 --- /dev/null +++ b/test/bridge/integration/bridge-manager/proposal-executor/executor.relayMainchain.MainchainBridgeManager.t.sol @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import { console2 as console } from "forge-std/console2.sol"; +import { GlobalProposal } from "@ronin/contracts/libraries/GlobalProposal.sol"; +import { Ballot } from "@ronin/contracts/libraries/Ballot.sol"; +import { ContractType } from "@ronin/contracts/utils/ContractType.sol"; +import { IBridgeManager } from "@ronin/contracts/interfaces/bridge/IBridgeManager.sol"; +import { SignatureConsumer } from "@ronin/contracts/interfaces/consumers/SignatureConsumer.sol"; +import { LibSort } from "solady/utils/LibSort.sol"; + +import "../../BaseIntegration.t.sol"; + +contract ProposalWithExecutor_MainchainProposal_MainchainBridgeManager_Test is BaseIntegration_Test { + event ProposalVoted(bytes32 indexed proposalHash, address indexed voter, Ballot.VoteType support, uint256 weight); + event ProposalApproved(bytes32 indexed proposalHash); + event ProposalExecuted(bytes32 indexed proposalHash, bool[] successCalls, bytes[] returnDatas); + + error ErrNonExecutorCannotRelay(address executor, address caller); + error ErrInvalidExecutor(); + error ErrProposalNotApproved(); + error ErrInvalidProposalNonce(bytes4 sig); + error ErrLooseProposalInternallyRevert(uint, bytes); + + using LibSort for address[]; + + uint256 _proposalExpiryDuration; + uint256 _addingOperatorNum; + address[] _addingOperators; + address[] _addingGovernors; + uint96[] _voteWeights; + + address[] _beforeRelayedOperators; + address[] _beforeRelayedGovernors; + + address[] _afterRelayedOperators; + address[] _afterRelayedGovernors; + + Ballot.VoteType[] _supports; + + Proposal.ProposalDetail _proposal; + SignatureConsumer.Signature[] _signatures; + + bytes32 _anyValue; + + function setUp() public virtual override { + super.setUp(); + + _proposalExpiryDuration = 60; + _addingOperatorNum = 3; + + _beforeRelayedOperators = _param.roninBridgeManager.bridgeOperators; + _beforeRelayedGovernors = _param.roninBridgeManager.governors; + + _supports = new Ballot.VoteType[](_beforeRelayedOperators.length); + for (uint256 i; i < _beforeRelayedGovernors.length; i++) { + _supports[i] = Ballot.VoteType.For; + } + + _generateAddingOperators(_addingOperatorNum); + + _proposal.nonce = _mainchainBridgeManager.round(block.chainid) + 1; + _proposal.chainId = block.chainid; + _proposal.executor = address(0); + _proposal.loose = false; + _proposal.expiryTimestamp = block.timestamp + _proposalExpiryDuration; + + _proposal.targets.push(address(_mainchainBridgeManager)); // Test Relay + _proposal.values.push(0); + _proposal.calldatas.push(abi.encodeCall(IBridgeManager.addBridgeOperators, (_voteWeights, _addingGovernors, _addingOperators))); + _proposal.gasAmounts.push(1_000_000); + + // Duplicate the internal call + _proposal.targets.push(address(_mainchainBridgeManager)); // Test Relay + _proposal.values.push(0); + _proposal.calldatas.push(abi.encodeCall(IBridgeManager.addBridgeOperators, (_voteWeights, _addingGovernors, _addingOperators))); + _proposal.gasAmounts.push(1_000_000); + } + + // Should the proposal is approved but not executed on Ronin chain + function test_relayMainchain_autoProposal_looseProposal() public { + _proposal.loose = true; + _proposal.executor = address(0); + + vm.expectEmit(false, false, false, false); + emit ProposalApproved(_anyValue); + vm.expectEmit(false, false, false, false); + emit ProposalExecuted(_anyValue, new bool[](2), new bytes[](2)); + + SignatureConsumer.Signature[] memory signatures = _roninProposalUtils.generateSignatures(_proposal, _param.test.governorPKs); + for (uint256 i; i < signatures.length; i++) { + _signatures.push(signatures[i]); + } + + // Mainchain proposal take effect on Mainchain + assertEq(_mainchainBridgeManager.getBridgeOperators(), _beforeRelayedOperators); + vm.prank(_param.roninBridgeManager.governors[0]); + _mainchainBridgeManager.relayProposal(_proposal, _supports, _signatures); + assertEq(_mainchainBridgeManager.getBridgeOperators(), _afterRelayedOperators); + } + + // Should the non-auto proposal be relay by the specified executor + function test_relayMainchain_executorProposal_looseProposal_WhenAllInternalCallsPass() public { + _proposal.loose = true; + _proposal.executor = _param.roninBridgeManager.governors[0]; + _proposal.gasAmounts[1] = 1_000_000; // Set gas for the second call becomes success + + vm.expectEmit(false, false, false, false); + emit ProposalApproved(_anyValue); + vm.expectEmit(false, false, false, false); + emit ProposalExecuted(_anyValue, new bool[](2), new bytes[](2)); + + SignatureConsumer.Signature[] memory signatures = _roninProposalUtils.generateSignatures(_proposal, _param.test.governorPKs); + for (uint256 i; i < signatures.length; i++) { + _signatures.push(signatures[i]); + } + + // Mainchain proposal take effect on Mainchain + assertEq(_mainchainBridgeManager.getBridgeOperators(), _beforeRelayedOperators); + vm.prank(_param.roninBridgeManager.governors[0]); + _mainchainBridgeManager.relayProposal(_proposal, _supports, _signatures); + assertEq(_mainchainBridgeManager.getBridgeOperators(), _afterRelayedOperators); + } + + // Should the non-auto proposal can not be execute by other governor + function test_relayMainchain_executorProposal_revertWhen_proposalIsExecutedByAnotherGovernor() external { + _proposal.loose = false; + _proposal.executor = _param.roninBridgeManager.governors[0]; + _proposal.gasAmounts[1] = 1_000_000; // Set gas for the second call becomes success + + SignatureConsumer.Signature[] memory signatures = _roninProposalUtils.generateSignatures(_proposal, _param.test.governorPKs); + for (uint256 i; i < signatures.length; i++) { + _signatures.push(signatures[i]); + } + + vm.expectRevert(abi.encodeWithSelector(ErrNonExecutorCannotRelay.selector, _param.roninBridgeManager.governors[0], _param.roninBridgeManager.governors[1])); + vm.prank(_param.roninBridgeManager.governors[1]); + _mainchainBridgeManager.relayProposal(_proposal, _supports, _signatures); + } + + function _generateAddingOperators(uint256 num) internal { + delete _addingOperators; + delete _addingGovernors; + delete _voteWeights; + + _afterRelayedOperators = _beforeRelayedOperators; + _afterRelayedGovernors = _beforeRelayedGovernors; + + for (uint256 i; i < num; i++) { + _addingOperators.push(makeAddr(string.concat("adding-operator", vm.toString(i)))); + _addingGovernors.push(makeAddr(string.concat("adding-governor", vm.toString(i)))); + _voteWeights.push(uint96(uint256(100))); + + _afterRelayedOperators.push(_addingOperators[i]); + _afterRelayedGovernors.push(_addingGovernors[i]); + } + } +} diff --git a/test/bridge/integration/bridge-manager/proposal-loose/proposeCurrent.RoninBridgeManager.t.sol b/test/bridge/integration/bridge-manager/proposal-loose/proposeCurrent.RoninBridgeManager.t.sol new file mode 100644 index 00000000..17ee78e1 --- /dev/null +++ b/test/bridge/integration/bridge-manager/proposal-loose/proposeCurrent.RoninBridgeManager.t.sol @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import { console2 as console } from "forge-std/console2.sol"; +import { GlobalProposal } from "@ronin/contracts/libraries/GlobalProposal.sol"; +import { Ballot } from "@ronin/contracts/libraries/Ballot.sol"; +import { ContractType } from "@ronin/contracts/utils/ContractType.sol"; +import { IBridgeManager } from "@ronin/contracts/interfaces/bridge/IBridgeManager.sol"; +import { SignatureConsumer } from "@ronin/contracts/interfaces/consumers/SignatureConsumer.sol"; +import { LibSort } from "solady/utils/LibSort.sol"; + +import "../../BaseIntegration.t.sol"; + +contract LooseProposal_CurrentNetworkProposal_RoninBridgeManager_Test is BaseIntegration_Test { + event ProposalVoted(bytes32 indexed proposalHash, address indexed voter, Ballot.VoteType support, uint256 weight); + + error ErrInvalidProposalNonce(bytes4 sig); + error ErrLooseProposalInternallyRevert(uint, bytes); + + using LibSort for address[]; + + uint256 _proposalExpiryDuration; + uint256 _addingOperatorNum; + address[] _addingOperators; + address[] _addingGovernors; + uint96[] _voteWeights; + + address[] _beforeRelayedOperators; + address[] _beforeRelayedGovernors; + + address[] _afterRelayedOperators; + address[] _afterRelayedGovernors; + + Ballot.VoteType[] _supports; + + GlobalProposal.GlobalProposalDetail _globalProposal; + SignatureConsumer.Signature[] _signatures; + + Proposal.ProposalDetail _proposal; + + bytes32 _anyValue; + + function setUp() public virtual override { + super.setUp(); + + _proposalExpiryDuration = 60; + _addingOperatorNum = 3; + + _beforeRelayedOperators = _param.roninBridgeManager.bridgeOperators; + _beforeRelayedGovernors = _param.roninBridgeManager.governors; + + _supports = new Ballot.VoteType[](_beforeRelayedOperators.length); + for (uint256 i; i < _beforeRelayedGovernors.length; i++) { + _supports[i] = Ballot.VoteType.For; + } + + _generateAddingOperators(_addingOperatorNum); + + _proposal.nonce = _roninBridgeManager.round(block.chainid) + 1; + _proposal.chainId = block.chainid; + _proposal.executor = address(0); + _proposal.loose = false; + _proposal.expiryTimestamp = block.timestamp + _proposalExpiryDuration; + + _proposal.targets.push(address(_roninBridgeManager)); + _proposal.values.push(0); + _proposal.calldatas.push(abi.encodeCall(IBridgeManager.addBridgeOperators, (_voteWeights, _addingGovernors, _addingOperators))); + _proposal.gasAmounts.push(1_000_000); + + // Duplicate the internal call + _proposal.targets.push(address(_roninBridgeManager)); + _proposal.values.push(0); + _proposal.calldatas.push(abi.encodeCall(IBridgeManager.addBridgeOperators, (_voteWeights, _addingGovernors, _addingOperators))); + _proposal.gasAmounts.push(1_000_000); + } + + // Should the strict proposal failed when containing one failed internal call + function test_strictProposal_revertWhen_containingOneFailedInternalCall() external { + _proposal.loose = false; + _proposal.gasAmounts[1] = 1_000; // Set gas for the second call becomes failed + + vm.expectEmit(false, true, true, true); + emit ProposalVoted(_anyValue, _param.roninBridgeManager.governors[0], Ballot.VoteType.For, 100); + + SignatureConsumer.Signature[] memory signatures = _roninProposalUtils.generateSignatures(_proposal, _param.test.governorPKs); + for (uint256 i; i < signatures.length; i++) { + _signatures.push(signatures[i]); + } + + vm.expectRevert(abi.encodeWithSelector(ErrLooseProposalInternallyRevert.selector, 1, "")); + vm.prank(_param.roninBridgeManager.governors[0]); + _roninBridgeManager.proposeProposalStructAndCastVotes(_proposal, _supports, _signatures); + } + + // Should the strict proposal passes when all internal calls are valid + function test_strictProposal_WhenAllInternalCallsPass() external { + _proposal.loose = false; + _proposal.gasAmounts[1] = 1_000_000; // Set gas for the second call becomes success + + vm.expectEmit(false, true, true, true); + emit ProposalVoted(_anyValue, _param.roninBridgeManager.governors[0], Ballot.VoteType.For, 100); + + SignatureConsumer.Signature[] memory signatures = _roninProposalUtils.generateSignatures(_proposal, _param.test.governorPKs); + for (uint256 i; i < signatures.length; i++) { + _signatures.push(signatures[i]); + } + + vm.prank(_param.roninBridgeManager.governors[0]); + _roninBridgeManager.proposeProposalStructAndCastVotes(_proposal, _supports, _signatures); + + assertEq(_roninBridgeManager.proposalVoted(block.chainid, _proposal.nonce, _param.roninBridgeManager.governors[0]), true); + assertEq(_roninBridgeManager.getBridgeOperators(), _afterRelayedOperators); + } + + // Should the loose proposal passes when containing one failed internal call + function test_looseProposal_WhenContainsOneInternalCallFailed() external { + _proposal.loose = true; + _proposal.gasAmounts[1] = 1_000; // Set gas for the second call becomes failed + + vm.expectEmit(false, true, true, true); + emit ProposalVoted(_anyValue, _param.roninBridgeManager.governors[0], Ballot.VoteType.For, 100); + + SignatureConsumer.Signature[] memory signatures = _roninProposalUtils.generateSignatures(_proposal, _param.test.governorPKs); + for (uint256 i; i < signatures.length; i++) { + _signatures.push(signatures[i]); + } + + vm.prank(_param.roninBridgeManager.governors[0]); + _roninBridgeManager.proposeProposalStructAndCastVotes(_proposal, _supports, _signatures); + + assertEq(_roninBridgeManager.proposalVoted(block.chainid, _proposal.nonce, _param.roninBridgeManager.governors[0]), true); + assertEq(_roninBridgeManager.getBridgeOperators(), _afterRelayedOperators); + } + + // Should the loose proposal passes when all internal calls are valid + function test_looseProposal_WhenAllInternalCallsPass() external { + _proposal.loose = true; + _proposal.gasAmounts[1] = 1_000_000; // Set gas for the second call becomes success + + vm.expectEmit(false, true, true, true); + emit ProposalVoted(_anyValue, _param.roninBridgeManager.governors[0], Ballot.VoteType.For, 100); + + SignatureConsumer.Signature[] memory signatures = _roninProposalUtils.generateSignatures(_proposal, _param.test.governorPKs); + for (uint256 i; i < signatures.length; i++) { + _signatures.push(signatures[i]); + } + + vm.prank(_param.roninBridgeManager.governors[0]); + _roninBridgeManager.proposeProposalStructAndCastVotes(_proposal, _supports, _signatures); + + assertEq(_roninBridgeManager.proposalVoted(block.chainid, _proposal.nonce, _param.roninBridgeManager.governors[0]), true); + assertEq(_roninBridgeManager.getBridgeOperators(), _afterRelayedOperators); + } + + function _generateAddingOperators(uint256 num) internal { + delete _addingOperators; + delete _addingGovernors; + delete _voteWeights; + + _afterRelayedOperators = _beforeRelayedOperators; + _afterRelayedGovernors = _beforeRelayedGovernors; + + for (uint256 i; i < num; i++) { + _addingOperators.push(makeAddr(string.concat("adding-operator", vm.toString(i)))); + _addingGovernors.push(makeAddr(string.concat("adding-governor", vm.toString(i)))); + _voteWeights.push(uint96(uint256(100))); + + _afterRelayedOperators.push(_addingOperators[i]); + _afterRelayedGovernors.push(_addingGovernors[i]); + } + } +} diff --git a/test/bridge/integration/bridge-manager/proposal-loose/proposeGlobal.RoninBridgeManager.t.sol b/test/bridge/integration/bridge-manager/proposal-loose/proposeGlobal.RoninBridgeManager.t.sol new file mode 100644 index 00000000..d05a5156 --- /dev/null +++ b/test/bridge/integration/bridge-manager/proposal-loose/proposeGlobal.RoninBridgeManager.t.sol @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import { console2 as console } from "forge-std/console2.sol"; +import { GlobalProposal } from "@ronin/contracts/libraries/GlobalProposal.sol"; +import { Ballot } from "@ronin/contracts/libraries/Ballot.sol"; +import { ContractType } from "@ronin/contracts/utils/ContractType.sol"; +import { IBridgeManager } from "@ronin/contracts/interfaces/bridge/IBridgeManager.sol"; +import { SignatureConsumer } from "@ronin/contracts/interfaces/consumers/SignatureConsumer.sol"; +import { LibSort } from "solady/utils/LibSort.sol"; + +import "../../BaseIntegration.t.sol"; + +contract LooseProposal_GlobalProposal_RoninBridgeManager_Test is BaseIntegration_Test { + event ProposalVoted(bytes32 indexed proposalHash, address indexed voter, Ballot.VoteType support, uint256 weight); + + error ErrInvalidProposalNonce(bytes4 sig); + error ErrLooseProposalInternallyRevert(uint, bytes); + + using LibSort for address[]; + + uint256 _proposalExpiryDuration; + uint256 _addingOperatorNum; + address[] _addingOperators; + address[] _addingGovernors; + uint96[] _voteWeights; + + address[] _beforeRelayedOperators; + address[] _beforeRelayedGovernors; + + address[] _afterRelayedOperators; + address[] _afterRelayedGovernors; + + Ballot.VoteType[] _supports; + + GlobalProposal.GlobalProposalDetail _globalProposal; + SignatureConsumer.Signature[] _signatures; + + bytes32 _anyValue; + + function setUp() public virtual override { + super.setUp(); + + _proposalExpiryDuration = 60; + _addingOperatorNum = 3; + + _beforeRelayedOperators = _param.roninBridgeManager.bridgeOperators; + _beforeRelayedGovernors = _param.roninBridgeManager.governors; + + _supports = new Ballot.VoteType[](_beforeRelayedOperators.length); + for (uint256 i; i < _beforeRelayedGovernors.length; i++) { + _supports[i] = Ballot.VoteType.For; + } + + _generateAddingOperators(_addingOperatorNum); + + _globalProposal.nonce = _roninBridgeManager.round(0) + 1; + _globalProposal.expiryTimestamp = block.timestamp + _proposalExpiryDuration; + _globalProposal.executor = address(0); + _globalProposal.loose = false; + + _globalProposal.targetOptions.push(GlobalProposal.TargetOption.BridgeManager); + _globalProposal.values.push(0); + _globalProposal.calldatas.push(abi.encodeCall(IBridgeManager.addBridgeOperators, (_voteWeights, _addingGovernors, _addingOperators))); + _globalProposal.gasAmounts.push(1_000_000); + + // Duplicate the internal call + _globalProposal.targetOptions.push(GlobalProposal.TargetOption.BridgeManager); + _globalProposal.values.push(0); + _globalProposal.calldatas.push(abi.encodeCall(IBridgeManager.addBridgeOperators, (_voteWeights, _addingGovernors, _addingOperators))); + _globalProposal.gasAmounts.push(1_000_000); + } + + // Should the strict proposal failed when containing one failed internal call + function test_strictProposal_globalProposal_RoninSide_revertWhen_containingOneFailedInternalCall() external { + _globalProposal.loose = false; + _globalProposal.gasAmounts[1] = 1_000; // Set gas for the second call becomes failed + + vm.expectEmit(false, true, true, true); + emit ProposalVoted(_anyValue, _param.roninBridgeManager.governors[0], Ballot.VoteType.For, 100); + + SignatureConsumer.Signature[] memory signatures = _roninProposalUtils.generateSignaturesGlobal(_globalProposal, _param.test.governorPKs); + for (uint256 i; i < signatures.length; i++) { + _signatures.push(signatures[i]); + } + + vm.expectRevert(abi.encodeWithSelector(ErrLooseProposalInternallyRevert.selector, 1, "")); + vm.prank(_param.roninBridgeManager.governors[0]); + _roninBridgeManager.proposeGlobalProposalStructAndCastVotes(_globalProposal, _supports, _signatures); + } + + // Should the strict proposal passes when all internal calls are valid + function test_strictProposal_globalProposal_RoninSide_WhenAllInternalCallsPass() external { + _globalProposal.loose = false; + _globalProposal.gasAmounts[1] = 1_000_000; // Set gas for the second call becomes success + + vm.expectEmit(false, true, true, true); + emit ProposalVoted(_anyValue, _param.roninBridgeManager.governors[0], Ballot.VoteType.For, 100); + + SignatureConsumer.Signature[] memory signatures = _roninProposalUtils.generateSignaturesGlobal(_globalProposal, _param.test.governorPKs); + for (uint256 i; i < signatures.length; i++) { + _signatures.push(signatures[i]); + } + + vm.prank(_param.roninBridgeManager.governors[0]); + _roninBridgeManager.proposeGlobalProposalStructAndCastVotes(_globalProposal, _supports, _signatures); + + assertEq(_roninBridgeManager.globalProposalVoted(_globalProposal.nonce, _param.roninBridgeManager.governors[0]), true); + assertEq(_roninBridgeManager.getBridgeOperators(), _afterRelayedOperators); + } + + // Should the loose proposal passes when containing one failed internal call + function test_looseProposal_globalProposal_RoninSide_WhenContainsOneInternalCallFailed() external { + _globalProposal.loose = true; + _globalProposal.gasAmounts[1] = 1_000; // Set gas for the second call becomes failed + + vm.expectEmit(false, true, true, true); + emit ProposalVoted(_anyValue, _param.roninBridgeManager.governors[0], Ballot.VoteType.For, 100); + + SignatureConsumer.Signature[] memory signatures = _roninProposalUtils.generateSignaturesGlobal(_globalProposal, _param.test.governorPKs); + for (uint256 i; i < signatures.length; i++) { + _signatures.push(signatures[i]); + } + + vm.prank(_param.roninBridgeManager.governors[0]); + _roninBridgeManager.proposeGlobalProposalStructAndCastVotes(_globalProposal, _supports, _signatures); + + assertEq(_roninBridgeManager.globalProposalVoted(_globalProposal.nonce, _param.roninBridgeManager.governors[0]), true); + assertEq(_roninBridgeManager.getBridgeOperators(), _afterRelayedOperators); + } + + // Should the loose proposal passes when all internal calls are valid + function test_looseProposal_globalProposal_RoninSide_WhenAllInternalCallsPass() external { + _globalProposal.loose = true; + _globalProposal.gasAmounts[1] = 1_000_000; // Set gas for the second call becomes success + + vm.expectEmit(false, true, true, true); + emit ProposalVoted(_anyValue, _param.roninBridgeManager.governors[0], Ballot.VoteType.For, 100); + + SignatureConsumer.Signature[] memory signatures = _roninProposalUtils.generateSignaturesGlobal(_globalProposal, _param.test.governorPKs); + for (uint256 i; i < signatures.length; i++) { + _signatures.push(signatures[i]); + } + + vm.prank(_param.roninBridgeManager.governors[0]); + _roninBridgeManager.proposeGlobalProposalStructAndCastVotes(_globalProposal, _supports, _signatures); + + assertEq(_roninBridgeManager.globalProposalVoted(_globalProposal.nonce, _param.roninBridgeManager.governors[0]), true); + assertEq(_roninBridgeManager.getBridgeOperators(), _afterRelayedOperators); + } + + function _generateAddingOperators(uint256 num) internal { + delete _addingOperators; + delete _addingGovernors; + delete _voteWeights; + + _afterRelayedOperators = _beforeRelayedOperators; + _afterRelayedGovernors = _beforeRelayedGovernors; + + for (uint256 i; i < num; i++) { + _addingOperators.push(makeAddr(string.concat("adding-operator", vm.toString(i)))); + _addingGovernors.push(makeAddr(string.concat("adding-governor", vm.toString(i)))); + _voteWeights.push(uint96(uint256(100))); + + _afterRelayedOperators.push(_addingOperators[i]); + _afterRelayedGovernors.push(_addingGovernors[i]); + } + } +} diff --git a/test/bridge/integration/bridge-manager/proposal-loose/relayGlobal.MainchainBridgeManager.t.sol b/test/bridge/integration/bridge-manager/proposal-loose/relayGlobal.MainchainBridgeManager.t.sol new file mode 100644 index 00000000..b2375358 --- /dev/null +++ b/test/bridge/integration/bridge-manager/proposal-loose/relayGlobal.MainchainBridgeManager.t.sol @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import { console2 as console } from "forge-std/console2.sol"; +import { GlobalProposal } from "@ronin/contracts/libraries/GlobalProposal.sol"; +import { Ballot } from "@ronin/contracts/libraries/Ballot.sol"; +import { ContractType } from "@ronin/contracts/utils/ContractType.sol"; +import { IBridgeManager } from "@ronin/contracts/interfaces/bridge/IBridgeManager.sol"; +import { SignatureConsumer } from "@ronin/contracts/interfaces/consumers/SignatureConsumer.sol"; +import { LibSort } from "solady/utils/LibSort.sol"; + +import "../../BaseIntegration.t.sol"; + +contract LooseProposal_GlobalProposal_MainchainBridgeManager_Test is BaseIntegration_Test { + event ProposalVoted(bytes32 indexed proposalHash, address indexed voter, Ballot.VoteType support, uint256 weight); + event ProposalApproved(bytes32 indexed proposalHash); + event ProposalExecuted(bytes32 indexed proposalHash, bool[] successCalls, bytes[] returnDatas); + + error ErrInvalidProposalNonce(bytes4 sig); + error ErrLooseProposalInternallyRevert(uint, bytes); + + using LibSort for address[]; + + uint256 _proposalExpiryDuration; + uint256 _addingOperatorNum; + address[] _addingOperators; + address[] _addingGovernors; + uint96[] _voteWeights; + + address[] _beforeRelayedOperators; + address[] _beforeRelayedGovernors; + + address[] _afterRelayedOperators; + address[] _afterRelayedGovernors; + + Ballot.VoteType[] _supports; + + GlobalProposal.GlobalProposalDetail _globalProposal; + SignatureConsumer.Signature[] _signatures; + + bytes32 _anyValue; + + function setUp() public virtual override { + super.setUp(); + + _proposalExpiryDuration = 60; + _addingOperatorNum = 3; + + _beforeRelayedOperators = _param.roninBridgeManager.bridgeOperators; + _beforeRelayedGovernors = _param.roninBridgeManager.governors; + + _supports = new Ballot.VoteType[](_beforeRelayedOperators.length); + for (uint256 i; i < _beforeRelayedGovernors.length; i++) { + _supports[i] = Ballot.VoteType.For; + } + + _generateAddingOperators(_addingOperatorNum); + + _globalProposal.nonce = _mainchainBridgeManager.round(0) + 1; + _globalProposal.expiryTimestamp = block.timestamp + _proposalExpiryDuration; + _globalProposal.executor = address(0); + _globalProposal.loose = false; + + _globalProposal.targetOptions.push(GlobalProposal.TargetOption.BridgeManager); + _globalProposal.values.push(0); + _globalProposal.calldatas.push(abi.encodeCall(IBridgeManager.addBridgeOperators, (_voteWeights, _addingGovernors, _addingOperators))); + _globalProposal.gasAmounts.push(1_000_000); + + // Duplicate the internal call + _globalProposal.targetOptions.push(GlobalProposal.TargetOption.BridgeManager); + _globalProposal.values.push(0); + _globalProposal.calldatas.push(abi.encodeCall(IBridgeManager.addBridgeOperators, (_voteWeights, _addingGovernors, _addingOperators))); + _globalProposal.gasAmounts.push(1_000_000); + } + + // Should the strict proposal failed when containing one failed internal call + function test_strictProposal_globalProposal_MainchainSide_revertWhen_containingOneFailedInternalCall() external { + _globalProposal.loose = false; + _globalProposal.gasAmounts[1] = 1_000; // Set gas for the second call becomes failed + + vm.expectEmit(false, true, true, true); + emit ProposalApproved(_anyValue); + + SignatureConsumer.Signature[] memory signatures = _roninProposalUtils.generateSignaturesGlobal(_globalProposal, _param.test.governorPKs); + for (uint256 i; i < signatures.length; i++) { + _signatures.push(signatures[i]); + } + + vm.expectRevert(abi.encodeWithSelector(ErrLooseProposalInternallyRevert.selector, 1, "")); + vm.prank(_param.roninBridgeManager.governors[0]); + _mainchainBridgeManager.relayGlobalProposal(_globalProposal, _supports, _signatures); + } + + // Should the strict proposal passes when all internal calls are valid + function test_strictProposal_globalProposal_MainchainSide_WhenAllInternalCallsPass() external { + _globalProposal.loose = false; + _globalProposal.gasAmounts[1] = 1_000_000; // Set gas for the second call becomes success + + vm.expectEmit(false, false, false, false); + emit ProposalApproved(_anyValue); + vm.expectEmit(false, false, false, false); + emit ProposalExecuted(_anyValue, new bool[](2), new bytes[](2)); + + SignatureConsumer.Signature[] memory signatures = _roninProposalUtils.generateSignaturesGlobal(_globalProposal, _param.test.governorPKs); + for (uint256 i; i < signatures.length; i++) { + _signatures.push(signatures[i]); + } + + vm.prank(_param.roninBridgeManager.governors[0]); + _mainchainBridgeManager.relayGlobalProposal(_globalProposal, _supports, _signatures); + + assertEq(_mainchainBridgeManager.globalProposalRelayed(_globalProposal.nonce), true); + assertEq(_mainchainBridgeManager.getBridgeOperators(), _afterRelayedOperators); + } + + // Should the loose proposal passes when containing one failed internal call + function test_looseProposal_globalProposal_MainchainSide_WhenContainsOneInternalCallFailed() external { + _globalProposal.loose = true; + _globalProposal.gasAmounts[1] = 1_000; // Set gas for the second call becomes failed + + vm.expectEmit(false, false, false, false); + emit ProposalApproved(_anyValue); + vm.expectEmit(false, false, false, false); + emit ProposalExecuted(_anyValue, new bool[](2), new bytes[](2)); + + SignatureConsumer.Signature[] memory signatures = _roninProposalUtils.generateSignaturesGlobal(_globalProposal, _param.test.governorPKs); + for (uint256 i; i < signatures.length; i++) { + _signatures.push(signatures[i]); + } + + vm.prank(_param.roninBridgeManager.governors[0]); + _mainchainBridgeManager.relayGlobalProposal(_globalProposal, _supports, _signatures); + + assertEq(_mainchainBridgeManager.globalProposalRelayed(_globalProposal.nonce), true); + assertEq(_mainchainBridgeManager.getBridgeOperators(), _afterRelayedOperators); + } + + // Should the loose proposal passes when all internal calls are valid + function test_looseProposal_globalProposal_MainchainSide_WhenAllInternalCallsPass() external { + _globalProposal.loose = true; + _globalProposal.gasAmounts[1] = 1_000_000; // Set gas for the second call becomes success + + vm.expectEmit(false, false, false, false); + emit ProposalApproved(_anyValue); + vm.expectEmit(false, false, false, false); + emit ProposalExecuted(_anyValue, new bool[](2), new bytes[](2)); + + SignatureConsumer.Signature[] memory signatures = _roninProposalUtils.generateSignaturesGlobal(_globalProposal, _param.test.governorPKs); + for (uint256 i; i < signatures.length; i++) { + _signatures.push(signatures[i]); + } + + vm.prank(_param.roninBridgeManager.governors[0]); + _mainchainBridgeManager.relayGlobalProposal(_globalProposal, _supports, _signatures); + + assertEq(_mainchainBridgeManager.globalProposalRelayed(_globalProposal.nonce), true); + assertEq(_mainchainBridgeManager.getBridgeOperators(), _afterRelayedOperators); + } + + function _generateAddingOperators(uint256 num) internal { + delete _addingOperators; + delete _addingGovernors; + delete _voteWeights; + + _afterRelayedOperators = _beforeRelayedOperators; + _afterRelayedGovernors = _beforeRelayedGovernors; + + for (uint256 i; i < num; i++) { + _addingOperators.push(makeAddr(string.concat("adding-operator", vm.toString(i)))); + _addingGovernors.push(makeAddr(string.concat("adding-governor", vm.toString(i)))); + _voteWeights.push(uint96(uint256(100))); + + _afterRelayedOperators.push(_addingOperators[i]); + _afterRelayedGovernors.push(_addingGovernors[i]); + } + } +} diff --git a/test/bridge/integration/bridge-manager/propose-and-cast-vote/voteBridgeOperator.RoninBridgeManager.t.sol b/test/bridge/integration/bridge-manager/propose-and-cast-vote/voteBridgeOperator.RoninBridgeManager.t.sol index 5d45e546..48e96924 100644 --- a/test/bridge/integration/bridge-manager/propose-and-cast-vote/voteBridgeOperator.RoninBridgeManager.t.sol +++ b/test/bridge/integration/bridge-manager/propose-and-cast-vote/voteBridgeOperator.RoninBridgeManager.t.sol @@ -58,6 +58,8 @@ contract VoteBridgeOperator_RoninBridgeManager_Test is BaseIntegration_Test { function test_voteAddBridgeOperatorsProposal() public { _globalProposal = _roninProposalUtils.createGlobalProposal({ expiryTimestamp: block.timestamp + _proposalExpiryDuration, + executor: address(0), + loose: false, targetOption: GlobalProposal.TargetOption.BridgeManager, value: 0, calldata_: abi.encodeCall(IBridgeManager.addBridgeOperators, (_voteWeights, _addingGovernors, _addingOperators)), @@ -119,6 +121,8 @@ contract VoteBridgeOperator_RoninBridgeManager_Test is BaseIntegration_Test { _globalProposal = _roninProposalUtils.createGlobalProposal({ expiryTimestamp: block.timestamp + _proposalExpiryDuration, + executor: address(0), + loose: false, targetOption: GlobalProposal.TargetOption.BridgeManager, value: 0, calldata_: abi.encodeCall(IBridgeManager.addBridgeOperators, (_voteWeights, _addingGovernors, _addingOperators)), diff --git a/test/helpers/MainchainBridgeAdminUtils.t.sol b/test/helpers/MainchainBridgeAdminUtils.t.sol index 55c534d0..322771c2 100644 --- a/test/helpers/MainchainBridgeAdminUtils.t.sol +++ b/test/helpers/MainchainBridgeAdminUtils.t.sol @@ -20,6 +20,8 @@ contract MainchainBridgeAdminUtils is ProposalUtils { function functionDelegateCall(address to, bytes memory data) public { Proposal.ProposalDetail memory proposal = this.createProposal({ expiryTimestamp: this.defaultExpiryTimestamp(), + executor: address(0), + loose: false, target: to, value: 0, calldata_: abi.encodeWithSignature("functionDelegateCall(bytes)", data), @@ -40,6 +42,8 @@ contract MainchainBridgeAdminUtils is ProposalUtils { function functionDelegateCallGlobal(GlobalProposal.TargetOption target, bytes memory data) public { GlobalProposal.GlobalProposalDetail memory proposal = this.createGlobalProposal({ expiryTimestamp: this.defaultExpiryTimestamp(), + executor: address(0), + loose: false, targetOption: target, value: 0, calldata_: abi.encodeWithSignature("functionDelegateCall(bytes)", data), @@ -73,6 +77,8 @@ contract MainchainBridgeAdminUtils is ProposalUtils { GlobalProposal.GlobalProposalDetail memory proposal = GlobalProposal.GlobalProposalDetail({ nonce: _contract.round(0) + 1, expiryTimestamp: this.defaultExpiryTimestamp(), + executor: address(0), + loose: false, targetOptions: targetOptions, values: values, calldatas: calldatas, @@ -92,6 +98,8 @@ contract MainchainBridgeAdminUtils is ProposalUtils { function upgradeGlobal(GlobalProposal.TargetOption targetOption, uint256 nonce, bytes memory data) public { GlobalProposal.GlobalProposalDetail memory proposal = this.createGlobalProposal({ expiryTimestamp: this.defaultExpiryTimestamp(), + executor: address(0), + loose: false, targetOption: targetOption, value: 0, calldata_: abi.encodeWithSignature("upgradeTo(bytes)", data), diff --git a/test/helpers/ProposalUtils.t.sol b/test/helpers/ProposalUtils.t.sol index 4e06b0b5..38a9bc65 100644 --- a/test/helpers/ProposalUtils.t.sol +++ b/test/helpers/ProposalUtils.t.sol @@ -28,6 +28,8 @@ contract ProposalUtils is Utils, Test { function createProposal( uint256 expiryTimestamp, + address executor, + bool loose, address target, uint256 value, bytes memory calldata_, @@ -37,6 +39,8 @@ contract ProposalUtils is Utils, Test { proposal = Proposal.ProposalDetail({ nonce: nonce, chainId: block.chainid, + executor: executor, + loose: loose, expiryTimestamp: expiryTimestamp, targets: wrapAddress(target), values: wrapUint(value), @@ -47,6 +51,8 @@ contract ProposalUtils is Utils, Test { function createGlobalProposal( uint256 expiryTimestamp, + address executor, + bool loose, GlobalProposal.TargetOption targetOption, uint256 value, bytes memory calldata_, @@ -59,6 +65,8 @@ contract ProposalUtils is Utils, Test { proposal = GlobalProposal.GlobalProposalDetail({ nonce: nonce, expiryTimestamp: expiryTimestamp, + executor: executor, + loose: loose, targetOptions: targetOptions, values: wrapUint(value), calldatas: wrapBytes(calldata_), @@ -75,19 +83,14 @@ contract ProposalUtils is Utils, Test { return generateSignaturesFor(proposalHash, signerPKs, support); } - function generateSignatures(Proposal.ProposalDetail memory proposal, uint256[] memory signerPKs) - public - view - returns (SignatureConsumer.Signature[] memory sigs) - { + function generateSignatures( + Proposal.ProposalDetail memory proposal, + uint256[] memory signerPKs + ) public view returns (SignatureConsumer.Signature[] memory sigs) { return generateSignatures(proposal, signerPKs, Ballot.VoteType.For); } - function generateSignatures(Proposal.ProposalDetail memory proposal) - public - view - returns (SignatureConsumer.Signature[] memory sigs) - { + function generateSignatures(Proposal.ProposalDetail memory proposal) public view returns (SignatureConsumer.Signature[] memory sigs) { return generateSignatures(proposal, _signerPKs, Ballot.VoteType.For); } @@ -100,19 +103,14 @@ contract ProposalUtils is Utils, Test { return generateSignaturesFor(proposalHash, signerPKs, support); } - function generateSignaturesGlobal(GlobalProposal.GlobalProposalDetail memory proposal, uint256[] memory signerPKs) - public - view - returns (SignatureConsumer.Signature[] memory sigs) - { + function generateSignaturesGlobal( + GlobalProposal.GlobalProposalDetail memory proposal, + uint256[] memory signerPKs + ) public view returns (SignatureConsumer.Signature[] memory sigs) { return generateSignaturesGlobal(proposal, signerPKs, Ballot.VoteType.For); } - function generateSignaturesGlobal(GlobalProposal.GlobalProposalDetail memory proposal) - public - view - returns (SignatureConsumer.Signature[] memory sigs) - { + function generateSignaturesGlobal(GlobalProposal.GlobalProposalDetail memory proposal) public view returns (SignatureConsumer.Signature[] memory sigs) { return generateSignaturesGlobal(proposal, _signerPKs, Ballot.VoteType.For); } @@ -120,18 +118,18 @@ contract ProposalUtils is Utils, Test { return keccak256( abi.encode( keccak256("EIP712Domain(string name,string version,bytes32 salt)"), - keccak256("BridgeAdmin"), // name hash - keccak256("2"), // version hash - keccak256(abi.encode("BRIDGE_ADMIN", block.chainid)) // salt + keccak256("BridgeManager"), // name hash + keccak256("3"), // version hash + keccak256(abi.encode("BRIDGE_MANAGER", block.chainid)) // salt ) ); } - function generateSignaturesFor(bytes32 proposalHash, uint256[] memory signerPKs, Ballot.VoteType support) - public - view - returns (SignatureConsumer.Signature[] memory sigs) - { + function generateSignaturesFor( + bytes32 proposalHash, + uint256[] memory signerPKs, + Ballot.VoteType support + ) public view returns (SignatureConsumer.Signature[] memory sigs) { sigs = new SignatureConsumer.Signature[](signerPKs.length); for (uint256 i; i < signerPKs.length; i++) { diff --git a/test/helpers/RoninBridgeAdminUtils.t.sol b/test/helpers/RoninBridgeAdminUtils.t.sol index 1b65257d..db703ab9 100644 --- a/test/helpers/RoninBridgeAdminUtils.t.sol +++ b/test/helpers/RoninBridgeAdminUtils.t.sol @@ -20,6 +20,8 @@ contract RoninBridgeAdminUtils is ProposalUtils { function functionDelegateCall(address to, bytes memory data) public { Proposal.ProposalDetail memory proposal = this.createProposal({ expiryTimestamp: this.defaultExpiryTimestamp(), + executor: address(0), + loose: false, target: to, value: 0, calldata_: abi.encodeWithSignature("functionDelegateCall(bytes)", data), @@ -40,6 +42,8 @@ contract RoninBridgeAdminUtils is ProposalUtils { function functionDelegateCallGlobal(GlobalProposal.TargetOption target, bytes memory data) public { GlobalProposal.GlobalProposalDetail memory proposal = this.createGlobalProposal({ expiryTimestamp: this.defaultExpiryTimestamp(), + executor: address(0), + loose: false, targetOption: target, value: 0, calldata_: abi.encodeWithSignature("functionDelegateCall(bytes)", data), @@ -73,6 +77,8 @@ contract RoninBridgeAdminUtils is ProposalUtils { GlobalProposal.GlobalProposalDetail memory proposal = GlobalProposal.GlobalProposalDetail({ nonce: _contract.round(0) + 1, expiryTimestamp: this.defaultExpiryTimestamp(), + executor: address(0), + loose: false, targetOptions: targetOptions, values: values, calldatas: calldatas, @@ -92,6 +98,8 @@ contract RoninBridgeAdminUtils is ProposalUtils { function upgradeGlobal(GlobalProposal.TargetOption targetOption, uint256 nonce, bytes memory data) public { GlobalProposal.GlobalProposalDetail memory proposal = this.createGlobalProposal({ expiryTimestamp: this.defaultExpiryTimestamp(), + executor: address(0), + loose: false, targetOption: targetOption, value: 0, calldata_: abi.encodeWithSignature("upgradeTo(bytes)", data),