diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2c4e7d8f..b75a24ec 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,3 +10,4 @@ on: jobs: test: uses: bgd-labs/github-workflows/.github/workflows/foundry-test.yml@main + secrets: inherit diff --git a/Makefile b/Makefile index 8e72cba0..ec83231b 100644 --- a/Makefile +++ b/Makefile @@ -258,6 +258,9 @@ deploy_mock_ccc: send-message-via-adapter: $(call deploy_fn,helpers/Send_Message_Via_Adapter,ethereum) +deploy_ccc_granular_guardian: + $(call deploy_fn,helpers/Send_Message_Via_Adapter,avalanche, polygon, binance, gnosis) + deploy-ccc-revision-and-update: $(call deploy_fn,CCC/UpdateCCC,ethereum) diff --git a/docs/overview.md b/docs/overview.md index 098f8034..27cba75f 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -55,7 +55,7 @@ Immutable utility contracts, which integrate into a.DI the specifics of each und send messages, by directly communicating with the receiving contract in the same chain where it is deployed. This pattern is useful to have same-chain-communication (bypass the cross chain bridging), but still following the same a.DI high-level flow. Misc aspects of the bridge adapters: -- To send a message to the bridge provider, the method `forwardMessage()` is called, via `DELEGATECALL`. This way, the CCC can hold the funds to pay the bridges, and can also be used as a trusted receiver. +- To send a message to the bridge provider, the method `forwardMessage()` is called, via `DELEGATECALL`. This way, the CCC can hold the funds to pay the bridges, and can also be used as a trusted receiver. - **No storage variables should be used on the bridge adapters**. - To receive a message a `handleReceive` (*the function name will actually depend on the bridge being integrated*) method needs to be called via `CALL` . This call will compare the caller to the trusted remote configured on deployment. @@ -164,6 +164,17 @@ Once the Emergency is solved, the permissions granted to the `guardian` are remo
+## CrossChainController access control + +The guardian system of the CrossChainController is externalized to the [GranularGuardianAccessControl](../src/contracts/access_control/GranularGuardianAccessControl.sol) contract. +This contract holds a granular way of assigning responsibilities to different entities (roles) so that guardian access +to CCC methods can be more deterministic. The roles that it holds are: +- **SOLVE_EMERGENCY_ROLE**: The holders of ths role can call the method `solveEmergency` on CrossChainController on the networks that have emergency enabled. +- **RETRY_ROLE**: the holders of this role can call the methods `retryTransaction` and `retryEnvelope` on CrossChainController (Forwarder). +- **DEFAULT_ADMIN_ROLE**: the holder of this role can grant roles, and change the Guardian of CrossChainController by calling `updateGuardian` method. + +The audit report by Certora can be found [here](../security/certora/reports/Granular-Guardian-Access-Control.pdf) + ### Contracts - [EmergencyRegistry](../src/contracts/emergency/EmergencyRegistry.sol): contract containing the registry of the emergency @@ -179,7 +190,7 @@ the `guardian` can call `solveEmergency`. When the call succeeds the local count The `solveEmergency` goal is to return a.DI to the operational mode, which granularly, means the following: - **Working Forwarding state**: this means that there is at least one BridgeAdapter configured for a specified chain, so a message can be sent to that chain. - **Working Receiving state**: this means that there are enough bridge adapters configured so that when a message is received, `requiredConfirmations` can be reached. - + The method can also be used as a way to force the system to a non-working state. This would be needed in the case that more than half of the bridges are compromised, so message should not be trusted (as the confirmations would then also be compromised), and `owner` could not be trusted since it is controlled via cross chain messaging. In that case there would be two ways of solving the emergency: diff --git a/scripts/BaseScript.sol b/scripts/BaseScript.sol index fd2f0f1a..9dae8f95 100644 --- a/scripts/BaseScript.sol +++ b/scripts/BaseScript.sol @@ -26,6 +26,7 @@ library DeployerHelpers { address crossChainControllerImpl; address emergencyRegistry; address gnosisAdapter; + address granularCCCGuardian; address guardian; address hlAdapter; address lzAdapter; @@ -105,6 +106,7 @@ library DeployerHelpers { proxyFactory: abi.decode(persistedJson.parseRaw('.proxyFactory'), (address)), owner: abi.decode(persistedJson.parseRaw('.owner'), (address)), guardian: abi.decode(persistedJson.parseRaw('.guardian'), (address)), + granularCCCGuardian: abi.decode(persistedJson.parseRaw('.granularCCCGuardian'), (address)), clEmergencyOracle: abi.decode(persistedJson.parseRaw('.clEmergencyOracle'), (address)), create3Factory: abi.decode(persistedJson.parseRaw('.create3Factory'), (address)), crossChainController: abi.decode(persistedJson.parseRaw('.crossChainController'), (address)), @@ -145,6 +147,7 @@ library DeployerHelpers { json.serialize('crossChainControllerImpl', addresses.crossChainControllerImpl); json.serialize('emergencyRegistry', addresses.emergencyRegistry); json.serialize('gnosisAdapter', addresses.gnosisAdapter); + json.serialize('granularCCCGuardian', addresses.granularCCCGuardian); json.serialize('guardian', addresses.guardian); json.serialize('hlAdapter', addresses.hlAdapter); json.serialize('lzAdapter', addresses.lzAdapter); diff --git a/scripts/access_control/Deploy_Granular_CCC_Guardian.s.sol b/scripts/access_control/Deploy_Granular_CCC_Guardian.s.sol new file mode 100644 index 00000000..74b6285e --- /dev/null +++ b/scripts/access_control/Deploy_Granular_CCC_Guardian.s.sol @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import '../../src/contracts/access_control/GranularGuardianAccessControl.sol'; +import {GovernanceV3Polygon} from 'aave-address-book/GovernanceV3Polygon.sol'; +import {GovernanceV3Ethereum} from 'aave-address-book/GovernanceV3Ethereum.sol'; +import {GovernanceV3Avalanche} from 'aave-address-book/GovernanceV3Avalanche.sol'; +import {GovernanceV3BNB} from 'aave-address-book/GovernanceV3BNB.sol'; +import {GovernanceV3Gnosis} from 'aave-address-book/GovernanceV3Gnosis.sol'; +import {GovernanceV3Arbitrum} from 'aave-address-book/GovernanceV3Arbitrum.sol'; +import {GovernanceV3Optimism} from 'aave-address-book/GovernanceV3Optimism.sol'; +import {GovernanceV3Scroll} from 'aave-address-book/GovernanceV3Scroll.sol'; +import {GovernanceV3Metis} from 'aave-address-book/GovernanceV3Metis.sol'; +import {GovernanceV3Base} from 'aave-address-book/GovernanceV3Base.sol'; +import {MiscPolygon} from 'aave-address-book/MiscPolygon.sol'; +import {MiscAvalanche} from 'aave-address-book/MiscAvalanche.sol'; +import {MiscBNB} from 'aave-address-book/MiscBNB.sol'; +import {MiscGnosis} from 'aave-address-book/MiscGnosis.sol'; +import {MiscEthereum} from 'aave-address-book/MiscEthereum.sol'; +import {MiscArbitrum} from 'aave-address-book/MiscArbitrum.sol'; +import {MiscOptimism} from 'aave-address-book/MiscOptimism.sol'; +import {MiscBase} from 'aave-address-book/MiscBase.sol'; +import {MiscScroll} from 'aave-address-book/MiscScroll.sol'; +import {MiscMetis} from 'aave-address-book/MiscMetis.sol'; +import '../BaseScript.sol'; + +abstract contract BaseDeployGranularGuardian is BaseScript { + address public immutable DEFAULT_ADMIN; + address public immutable RETRY_GUARDIAN; + address public immutable SOLVE_EMERGENCY_GUARDIAN; + address public immutable CROSS_CHAIN_CONTROLLER; + + constructor( + IGranularGuardianAccessControl.InitialGuardians memory initialGuardians, + address crossChainController + ) { + DEFAULT_ADMIN = initialGuardians.defaultAdmin; + RETRY_GUARDIAN = initialGuardians.retryGuardian; + SOLVE_EMERGENCY_GUARDIAN = initialGuardians.solveEmergencyGuardian; + CROSS_CHAIN_CONTROLLER = crossChainController; + } + + function _execute(DeployerHelpers.Addresses memory addresses) internal override { + IGranularGuardianAccessControl.InitialGuardians + memory initialGuardians = IGranularGuardianAccessControl.InitialGuardians({ + defaultAdmin: DEFAULT_ADMIN, + retryGuardian: RETRY_GUARDIAN, + solveEmergencyGuardian: SOLVE_EMERGENCY_GUARDIAN + }); + address granularCCCGuardian = address( + new GranularGuardianAccessControl(initialGuardians, CROSS_CHAIN_CONTROLLER) + ); + + addresses.granularCCCGuardian = granularCCCGuardian; + } +} + +contract Ethereum is + BaseDeployGranularGuardian( + IGranularGuardianAccessControl.InitialGuardians({ + defaultAdmin: MiscEthereum.PROTOCOL_GUARDIAN, + retryGuardian: 0xb812d0944f8F581DfAA3a93Dda0d22EcEf51A9CF, + solveEmergencyGuardian: MiscEthereum.PROTOCOL_GUARDIAN + }), + GovernanceV3Ethereum.CROSS_CHAIN_CONTROLLER + ) +{ + function TRANSACTION_NETWORK() public pure override returns (uint256) { + return ChainIds.ETHEREUM; + } +} + +contract Avalanche is + BaseDeployGranularGuardian( + IGranularGuardianAccessControl.InitialGuardians({ + defaultAdmin: MiscAvalanche.PROTOCOL_GUARDIAN, + retryGuardian: 0x3DBA1c4094BC0eE4772A05180B7E0c2F1cFD9c36, + solveEmergencyGuardian: MiscAvalanche.PROTOCOL_GUARDIAN + }), + GovernanceV3Avalanche.CROSS_CHAIN_CONTROLLER + ) +{ + function TRANSACTION_NETWORK() public pure override returns (uint256) { + return ChainIds.AVALANCHE; + } +} + +contract Polygon is + BaseDeployGranularGuardian( + IGranularGuardianAccessControl.InitialGuardians({ + defaultAdmin: MiscPolygon.PROTOCOL_GUARDIAN, + retryGuardian: 0xbCEB4f363f2666E2E8E430806F37e97C405c130b, + solveEmergencyGuardian: MiscPolygon.PROTOCOL_GUARDIAN + }), + GovernanceV3Polygon.CROSS_CHAIN_CONTROLLER + ) +{ + function TRANSACTION_NETWORK() public pure override returns (uint256) { + return ChainIds.POLYGON; + } +} + +contract Binance is + BaseDeployGranularGuardian( + IGranularGuardianAccessControl.InitialGuardians({ + defaultAdmin: MiscBNB.PROTOCOL_GUARDIAN, + retryGuardian: 0xE8C5ab722d0b1B7316Cc4034f2BE91A5B1d29964, + solveEmergencyGuardian: MiscBNB.PROTOCOL_GUARDIAN + }), + GovernanceV3BNB.CROSS_CHAIN_CONTROLLER + ) +{ + function TRANSACTION_NETWORK() public pure override returns (uint256) { + return ChainIds.BNB; + } +} + +contract Gnosis is + BaseDeployGranularGuardian( + IGranularGuardianAccessControl.InitialGuardians({ + defaultAdmin: MiscGnosis.PROTOCOL_GUARDIAN, + retryGuardian: 0xcb8a3E864D12190eD2b03cbA0833b15f2c314Ed8, + solveEmergencyGuardian: MiscGnosis.PROTOCOL_GUARDIAN + }), + GovernanceV3Gnosis.CROSS_CHAIN_CONTROLLER + ) +{ + function TRANSACTION_NETWORK() public pure override returns (uint256) { + return ChainIds.GNOSIS; + } +} + +contract Metis is + BaseDeployGranularGuardian( + IGranularGuardianAccessControl.InitialGuardians({ + defaultAdmin: MiscMetis.PROTOCOL_GUARDIAN, + retryGuardian: 0x9853589F951D724D9f7c6724E0fD63F9d888C429, + solveEmergencyGuardian: MiscMetis.PROTOCOL_GUARDIAN + }), + GovernanceV3Metis.CROSS_CHAIN_CONTROLLER + ) +{ + function TRANSACTION_NETWORK() public pure override returns (uint256) { + return ChainIds.METIS; + } +} + +contract Scroll is + BaseDeployGranularGuardian( + IGranularGuardianAccessControl.InitialGuardians({ + defaultAdmin: MiscScroll.PROTOCOL_GUARDIAN, + retryGuardian: 0x4aAa03F0A61cf93eA252e987b585453578108358, + solveEmergencyGuardian: MiscScroll.PROTOCOL_GUARDIAN + }), + GovernanceV3Scroll.CROSS_CHAIN_CONTROLLER + ) +{ + function TRANSACTION_NETWORK() public pure override returns (uint256) { + return ChainIds.SCROLL; + } +} + +contract Optimism is + BaseDeployGranularGuardian( + IGranularGuardianAccessControl.InitialGuardians({ + defaultAdmin: MiscOptimism.PROTOCOL_GUARDIAN, + retryGuardian: 0x3A800fbDeAC82a4d9c68A9FA0a315e095129CDBF, + solveEmergencyGuardian: MiscOptimism.PROTOCOL_GUARDIAN + }), + GovernanceV3Optimism.CROSS_CHAIN_CONTROLLER + ) +{ + function TRANSACTION_NETWORK() public pure override returns (uint256) { + return ChainIds.OPTIMISM; + } +} + +contract Arbitrum is + BaseDeployGranularGuardian( + IGranularGuardianAccessControl.InitialGuardians({ + defaultAdmin: MiscArbitrum.PROTOCOL_GUARDIAN, + retryGuardian: 0x1Fcd437D8a9a6ea68da858b78b6cf10E8E0bF959, + solveEmergencyGuardian: MiscArbitrum.PROTOCOL_GUARDIAN + }), + GovernanceV3Arbitrum.CROSS_CHAIN_CONTROLLER + ) +{ + function TRANSACTION_NETWORK() public pure override returns (uint256) { + return ChainIds.ARBITRUM; + } +} + +contract Base is + BaseDeployGranularGuardian( + IGranularGuardianAccessControl.InitialGuardians({ + defaultAdmin: MiscBase.PROTOCOL_GUARDIAN, + retryGuardian: 0x7FDA7C3528ad8f05e62148a700D456898b55f8d2, + solveEmergencyGuardian: MiscBase.PROTOCOL_GUARDIAN + }), + GovernanceV3Base.CROSS_CHAIN_CONTROLLER + ) +{ + function TRANSACTION_NETWORK() public pure override returns (uint256) { + return ChainIds.BASE; + } +} diff --git a/security/certora/reports/Granular-Guardian-Access-Control.pdf b/security/certora/reports/Granular-Guardian-Access-Control.pdf new file mode 100644 index 00000000..980914c1 Binary files /dev/null and b/security/certora/reports/Granular-Guardian-Access-Control.pdf differ diff --git a/src/contracts/access_control/GranularGuardianAccessControl.sol b/src/contracts/access_control/GranularGuardianAccessControl.sol new file mode 100644 index 00000000..0bc17dda --- /dev/null +++ b/src/contracts/access_control/GranularGuardianAccessControl.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.8; + +import {ICrossChainForwarder} from '../interfaces/ICrossChainForwarder.sol'; +import {ICrossChainControllerWithEmergencyMode} from '../interfaces/ICrossChainControllerWithEmergencyMode.sol'; +import {IGranularGuardianAccessControl, Envelope, ICrossChainReceiver} from './IGranularGuardianAccessControl.sol'; +import {AccessControlEnumerable} from 'openzeppelin-contracts/contracts/access/AccessControlEnumerable.sol'; +import {IWithGuardian} from 'solidity-utils/contracts/access-control/OwnableWithGuardian.sol'; + +/** + * @title GranularGuardianAccessControl + * @author BGD Labs + * @notice Contract to manage a granular access to the methods safeguarded by guardian on CrossChainController + */ +contract GranularGuardianAccessControl is AccessControlEnumerable, IGranularGuardianAccessControl { + /// @inheritdoc IGranularGuardianAccessControl + address public immutable CROSS_CHAIN_CONTROLLER; + + /// @inheritdoc IGranularGuardianAccessControl + bytes32 public constant SOLVE_EMERGENCY_ROLE = keccak256('SOLVE_EMERGENCY_ROLE'); + + /// @inheritdoc IGranularGuardianAccessControl + bytes32 public constant RETRY_ROLE = keccak256('RETRY_ROLE'); + + /** + * @param initialGuardians object with the initial guardians to assign the roles to + * @param crossChainController address of the CrossChainController + */ + constructor(InitialGuardians memory initialGuardians, address crossChainController) { + if (crossChainController == address(0)) { + revert CrossChainControllerCantBe0(); + } + if (initialGuardians.defaultAdmin == address(0)) { + revert DefaultAdminCantBe0(); + } + + CROSS_CHAIN_CONTROLLER = crossChainController; + + _grantRole(DEFAULT_ADMIN_ROLE, initialGuardians.defaultAdmin); + + if (initialGuardians.solveEmergencyGuardian != address(0)) { + _grantRole(SOLVE_EMERGENCY_ROLE, initialGuardians.solveEmergencyGuardian); + } + if (initialGuardians.retryGuardian != address(0)) { + _grantRole(RETRY_ROLE, initialGuardians.retryGuardian); + } + } + + /// @inheritdoc IGranularGuardianAccessControl + function retryEnvelope( + Envelope memory envelope, + uint256 gasLimit + ) external onlyRole(RETRY_ROLE) returns (bytes32) { + return ICrossChainForwarder(CROSS_CHAIN_CONTROLLER).retryEnvelope(envelope, gasLimit); + } + + /// @inheritdoc IGranularGuardianAccessControl + function retryTransaction( + bytes memory encodedTransaction, + uint256 gasLimit, + address[] memory bridgeAdaptersToRetry + ) external onlyRole(RETRY_ROLE) { + ICrossChainForwarder(CROSS_CHAIN_CONTROLLER).retryTransaction( + encodedTransaction, + gasLimit, + bridgeAdaptersToRetry + ); + } + + /// @inheritdoc IGranularGuardianAccessControl + function solveEmergency( + ICrossChainReceiver.ConfirmationInput[] memory newConfirmations, + ICrossChainReceiver.ValidityTimestampInput[] memory newValidityTimestamp, + ICrossChainReceiver.ReceiverBridgeAdapterConfigInput[] memory receiverBridgeAdaptersToAllow, + ICrossChainReceiver.ReceiverBridgeAdapterConfigInput[] memory receiverBridgeAdaptersToDisallow, + address[] memory sendersToApprove, + address[] memory sendersToRemove, + ICrossChainForwarder.ForwarderBridgeAdapterConfigInput[] memory forwarderBridgeAdaptersToEnable, + ICrossChainForwarder.BridgeAdapterToDisable[] memory forwarderBridgeAdaptersToDisable + ) external onlyRole(SOLVE_EMERGENCY_ROLE) { + ICrossChainControllerWithEmergencyMode(CROSS_CHAIN_CONTROLLER).solveEmergency( + newConfirmations, + newValidityTimestamp, + receiverBridgeAdaptersToAllow, + receiverBridgeAdaptersToDisallow, + sendersToApprove, + sendersToRemove, + forwarderBridgeAdaptersToEnable, + forwarderBridgeAdaptersToDisable + ); + } + + /// @inheritdoc IGranularGuardianAccessControl + function updateGuardian( + address newCrossChainControllerGuardian + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + if (newCrossChainControllerGuardian == address(0)) { + revert NewGuardianCantBe0(); + } + IWithGuardian(CROSS_CHAIN_CONTROLLER).updateGuardian(newCrossChainControllerGuardian); + } +} diff --git a/src/contracts/access_control/IGranularGuardianAccessControl.sol b/src/contracts/access_control/IGranularGuardianAccessControl.sol new file mode 100644 index 00000000..69673ac2 --- /dev/null +++ b/src/contracts/access_control/IGranularGuardianAccessControl.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.8; + +import {Envelope} from '../libs/EncodingUtils.sol'; +import {ICrossChainReceiver} from '../interfaces/ICrossChainReceiver.sol'; +import {ICrossChainForwarder} from '../interfaces/ICrossChainForwarder.sol'; + +/** + * @title IGranularGuardianAccessControl + * @author BGD Labs + * @notice interface containing the objects, events and methods definitions of the GranularGuardianAccessControl contract + */ +interface IGranularGuardianAccessControl { + /// @dev default admin address can not be address 0 + error DefaultAdminCantBe0(); + + /// @dev CrossChainController address can not be address 0 + error CrossChainControllerCantBe0(); + + /// @dev new Guardian address can not be address 0 + error NewGuardianCantBe0(); + + /** + * @param defaultAdmin address that will have control of the default admin + * @param retryGuardian address to be added to the retry role + * @param solveEmergencyGuardian address to be added to the solve emergency role + */ + struct InitialGuardians { + address defaultAdmin; + address retryGuardian; + address solveEmergencyGuardian; + } + + /** + * @notice method called to re forward a previously sent envelope. + This method is only callable by the accounts holding the RETRY_ROLE role + * @param envelope the Envelope type data + * @param gasLimit gas cost on receiving side of the message + * @return the transaction id that has the retried envelope + * @dev This method will send an existing Envelope using a new Transaction. + * @dev This method should be used when the intention is to send the Envelope as if it was a new message. This way on + the Receiver side it will start from 0 to count for the required confirmations. (usual use case would be for + when an envelope has been invalidated on Receiver side, and needs to be retried as a new message) + */ + function retryEnvelope(Envelope memory envelope, uint256 gasLimit) external returns (bytes32); + + /** + * @notice method to retry forwarding an already forwarded transaction. + This method is only callable by the accounts holding the RETRY_ROLE role + * @param encodedTransaction the encoded Transaction data + * @param gasLimit limit of gas to spend on forwarding per bridge + * @param bridgeAdaptersToRetry list of bridge adapters to be used for the transaction forwarding retry + * @dev This method will send an existing Transaction with its Envelope to the specified adapters. + * @dev Should be used when some of the bridges on the initial forwarding did not work (out of gas), + and we want the Transaction with Envelope to still account for the required confirmations on the Receiver side + */ + function retryTransaction( + bytes memory encodedTransaction, + uint256 gasLimit, + address[] memory bridgeAdaptersToRetry + ) external; + + /** + * @notice method to solve an emergency. This method is only callable by the accounts holding the SOLVE_EMERGENCY_ROLE role + * @param newConfirmations number of confirmations necessary for a message to be routed to destination + * @param newValidityTimestamp timestamp in seconds indicating the point to where not confirmed messages will be + * invalidated. + * @param receiverBridgeAdaptersToAllow list of bridge adapter addresses to be allowed to receive messages + * @param receiverBridgeAdaptersToDisallow list of bridge adapter addresses to be disallowed + * @param sendersToApprove list of addresses to be approved as senders + * @param sendersToRemove list of sender addresses to be removed + * @param forwarderBridgeAdaptersToEnable list of bridge adapters to be enabled to send messages + * @param forwarderBridgeAdaptersToDisable list of bridge adapters to be disabled + */ + function solveEmergency( + ICrossChainReceiver.ConfirmationInput[] memory newConfirmations, + ICrossChainReceiver.ValidityTimestampInput[] memory newValidityTimestamp, + ICrossChainReceiver.ReceiverBridgeAdapterConfigInput[] memory receiverBridgeAdaptersToAllow, + ICrossChainReceiver.ReceiverBridgeAdapterConfigInput[] memory receiverBridgeAdaptersToDisallow, + address[] memory sendersToApprove, + address[] memory sendersToRemove, + ICrossChainForwarder.ForwarderBridgeAdapterConfigInput[] memory forwarderBridgeAdaptersToEnable, + ICrossChainForwarder.BridgeAdapterToDisable[] memory forwarderBridgeAdaptersToDisable + ) external; + + /** + * @notice method to update the CrossChainController guardian when this contract has been set as guardian + */ + function updateGuardian(address newCrossChainControllerGuardian) external; + + /** + * @notice method to get the address of the CrossChainController where the contract points to + * @return the address of the CrossChainController + */ + function CROSS_CHAIN_CONTROLLER() external view returns (address); + + /** + * @notice method to get the solve emergency role + * @return the solve emergency role id + */ + function SOLVE_EMERGENCY_ROLE() external view returns (bytes32); + + /** + * @notice method to get the retry role + * @return the retry role id + */ + function RETRY_ROLE() external view returns (bytes32); +} diff --git a/tests/BaseTest.sol b/tests/BaseTest.sol index 91f21149..3b33afee 100644 --- a/tests/BaseTest.sol +++ b/tests/BaseTest.sol @@ -2,7 +2,15 @@ pragma solidity ^0.8.0; import 'forge-std/Test.sol'; +import {ChainIds} from '../src/contracts/libs/ChainIds.sol'; import {Transaction, EncodedTransaction, Envelope, EncodedEnvelope} from '../src/contracts/libs/EncodingUtils.sol'; +import {ICrossChainForwarder} from '../src/contracts/interfaces/ICrossChainForwarder.sol'; +import {ICrossChainReceiver} from '../src/contracts/interfaces/ICrossChainReceiver.sol'; +import {CrossChainController, ICrossChainController} from '../src/contracts/CrossChainController.sol'; +import {CrossChainControllerWithEmergencyMode, ICrossChainControllerWithEmergencyMode} from '../src/contracts/CrossChainControllerWithEmergencyMode.sol'; +import {IEmergencyConsumer} from '../src/contracts/emergency/interfaces/IEmergencyConsumer.sol'; +import {ICLEmergencyOracle} from '../src/contracts/emergency/interfaces/ICLEmergencyOracle.sol'; +import {TransparentProxyFactory} from 'solidity-utils/contracts/transparent-proxy/TransparentProxyFactory.sol'; contract BaseTest is Test { bytes internal constant MESSAGE = bytes('this is the message to send'); @@ -14,13 +22,7 @@ contract BaseTest is Test { } modifier filterAddress(address addressToFilter) { - vm.assume( - addressToFilter != 0xCe71065D4017F316EC606Fe4422e11eB2c47c246 && // FuzzerDict - addressToFilter != 0x4e59b44847b379578588920cA78FbF26c0B4956C && // CREATE2 Factory (?) - addressToFilter != 0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84 && // address(this) - addressToFilter != 0x185a4dc360CE69bDCceE33b3784B0282f7961aea && // ??? - addressToFilter != 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D // cheat codes - ); + _filterAddress(addressToFilter); _; } @@ -42,6 +44,59 @@ contract BaseTest is Test { uint256 envelopeNonce; } + modifier generateEmergencyState(address ccc) { + address clEmergencyOracle = IEmergencyConsumer(ccc).getChainlinkEmergencyOracle(); + (, int256 answer, , , ) = ICLEmergencyOracle(clEmergencyOracle).latestRoundData(); + vm.mockCall( + clEmergencyOracle, + abi.encodeWithSelector(ICLEmergencyOracle.latestRoundData.selector), + abi.encode(uint80(2), int256(answer + 1), block.timestamp - 5, block.timestamp - 5, uint80(2)) + ); + _; + } + + modifier validateEmergencySolved(address ccc) { + _; + address clEmergencyOracle = IEmergencyConsumer(ccc).getChainlinkEmergencyOracle(); + (, int256 answer, , , ) = ICLEmergencyOracle(clEmergencyOracle).latestRoundData(); + + uint256 emergencyCount = IEmergencyConsumer(ccc).getEmergencyCount(); + + assertEq(emergencyCount, uint256(answer)); + } + + modifier generateRetryTxState( + address cccOwner, + address ccc, + uint256 destinationChainId, + address destination, + uint256 gasLimit + ) { + vm.assume(gasLimit < 300_000); + // set caller as an approved sender + address[] memory senders = new address[](1); + senders[0] = address(this); + + vm.startPrank(cccOwner); + ICrossChainForwarder(ccc).approveSenders(senders); + vm.stopPrank(); + + // call forward message + ICrossChainForwarder(ccc).forwardMessage(destinationChainId, destination, gasLimit, MESSAGE); + _; + } + + function _filterAddress(address addressToFilter) internal pure { + vm.assume( + addressToFilter != address(0) && + addressToFilter != 0xCe71065D4017F316EC606Fe4422e11eB2c47c246 && // FuzzerDict + addressToFilter != 0x4e59b44847b379578588920cA78FbF26c0B4956C && // CREATE2 Factory (?) + addressToFilter != 0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84 && // address(this) + addressToFilter != 0x185a4dc360CE69bDCceE33b3784B0282f7961aea && // ??? + addressToFilter != 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D // cheat codes + ); + } + function _generateExtendedTransaction( TestParams memory testParams ) internal pure returns (ExtendedTransaction memory) { diff --git a/tests/access_control/GranularGuardianAccessControl.t.sol b/tests/access_control/GranularGuardianAccessControl.t.sol new file mode 100644 index 00000000..687cc525 --- /dev/null +++ b/tests/access_control/GranularGuardianAccessControl.t.sol @@ -0,0 +1,328 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {TestUtils} from '../utils/TestUtils.sol'; +import '../../src/contracts/access_control/GranularGuardianAccessControl.sol'; +import {OwnableWithGuardian, IWithGuardian} from 'solidity-utils/contracts/access-control/OwnableWithGuardian.sol'; +import '../BaseTest.sol'; + +contract GranularGuardianAccessControlTest is BaseTest { + bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00; + + GranularGuardianAccessControl public control; + + modifier setUpGranularGuardians( + address defaultAdmin, + address retryGuardian, + address solveEmergencyGuardian, + address ccc + ) { + _filterAddress(defaultAdmin); + _filterAddress(retryGuardian); + _filterAddress(solveEmergencyGuardian); + _filterAddress(ccc); + IGranularGuardianAccessControl.InitialGuardians + memory initialGuardians = IGranularGuardianAccessControl.InitialGuardians({ + defaultAdmin: defaultAdmin, + retryGuardian: retryGuardian, + solveEmergencyGuardian: solveEmergencyGuardian + }); + control = new GranularGuardianAccessControl(initialGuardians, ccc); + _; + } + + function setUp() public {} + + function test_initializationWithWrongDefaultAdmin( + address retryGuardian, + address solveEmergencyGuardian, + address ccc + ) public { + _filterAddress(retryGuardian); + _filterAddress(solveEmergencyGuardian); + _filterAddress(ccc); + IGranularGuardianAccessControl.InitialGuardians + memory initialGuardians = IGranularGuardianAccessControl.InitialGuardians({ + defaultAdmin: address(0), + retryGuardian: retryGuardian, + solveEmergencyGuardian: solveEmergencyGuardian + }); + vm.expectRevert(IGranularGuardianAccessControl.DefaultAdminCantBe0.selector); + new GranularGuardianAccessControl(initialGuardians, ccc); + } + + function test_initializationWithWrongCCC( + address retryGuardian, + address solveEmergencyGuardian, + address defaultAdmin + ) public { + _filterAddress(defaultAdmin); + _filterAddress(retryGuardian); + _filterAddress(solveEmergencyGuardian); + IGranularGuardianAccessControl.InitialGuardians + memory initialGuardians = IGranularGuardianAccessControl.InitialGuardians({ + defaultAdmin: defaultAdmin, + retryGuardian: retryGuardian, + solveEmergencyGuardian: solveEmergencyGuardian + }); + vm.expectRevert(IGranularGuardianAccessControl.CrossChainControllerCantBe0.selector); + new GranularGuardianAccessControl(initialGuardians, address(0)); + } + + function test_initialization( + address defaultAdmin, + address retryGuardian, + address solveEmergencyGuardian, + address ccc + ) public setUpGranularGuardians(defaultAdmin, retryGuardian, solveEmergencyGuardian, ccc) { + assertEq(control.CROSS_CHAIN_CONTROLLER(), ccc); + assertEq(control.hasRole(DEFAULT_ADMIN_ROLE, defaultAdmin), true); + assertEq(control.getRoleAdmin(control.RETRY_ROLE()), DEFAULT_ADMIN_ROLE); + assertEq(control.getRoleAdmin(control.SOLVE_EMERGENCY_ROLE()), DEFAULT_ADMIN_ROLE); + assertEq(control.getRoleMemberCount(control.RETRY_ROLE()), 1); + assertEq(control.getRoleMemberCount(control.SOLVE_EMERGENCY_ROLE()), 1); + assertEq(control.getRoleMember(control.RETRY_ROLE(), 0), retryGuardian); + assertEq(control.getRoleMember(control.SOLVE_EMERGENCY_ROLE(), 0), solveEmergencyGuardian); + } + + function test_retryTx( + address defaultAdmin, + address retryGuardian, + address solveEmergencyGuardian, + address ccc + ) public setUpGranularGuardians(defaultAdmin, retryGuardian, solveEmergencyGuardian, ccc) { + vm.startPrank(retryGuardian); + _retryTx(ccc); + vm.stopPrank(); + } + + function test_retryTxWhenWrongCaller( + address defaultAdmin, + address retryGuardian, + address solveEmergencyGuardian, + address ccc, + address retryCaller + ) public setUpGranularGuardians(defaultAdmin, retryGuardian, solveEmergencyGuardian, ccc) { + vm.assume(retryCaller != retryGuardian); + hoax(retryCaller); + vm.expectRevert( + bytes( + string.concat( + 'AccessControl: account 0x', + TestUtils.toAsciiString(retryCaller), + ' is missing role 0xc448b9502bbdf9850cc39823b6ea40cfe96d3ac63008e89edd2b8e98c6cc0af3' + ) + ) + ); + _retryTx(ccc); + } + + function test_retryEnvelope( + address defaultAdmin, + address retryGuardian, + address solveEmergencyGuardian, + address ccc, + bytes32 mockEnvId + ) public setUpGranularGuardians(defaultAdmin, retryGuardian, solveEmergencyGuardian, ccc) { + vm.startPrank(retryGuardian); + // bytes32 mockEnvId = keccak256(abi.encode('mock envelope id')); + bytes32 envId = _retryEnvelope(ccc, mockEnvId); + assertEq(envId, mockEnvId); + vm.stopPrank(); + } + + function test_retryEnvelopeWhenWrongCaller( + address defaultAdmin, + address retryGuardian, + address solveEmergencyGuardian, + address ccc, + address retryCaller, + bytes32 mockEnvId + ) public setUpGranularGuardians(defaultAdmin, retryGuardian, solveEmergencyGuardian, ccc) { + vm.assume(retryCaller != retryGuardian); + hoax(retryCaller); + vm.expectRevert( + bytes( + string.concat( + 'AccessControl: account 0x', + TestUtils.toAsciiString(retryCaller), + ' is missing role 0xc448b9502bbdf9850cc39823b6ea40cfe96d3ac63008e89edd2b8e98c6cc0af3' + ) + ) + ); + _retryEnvelope(ccc, mockEnvId); + } + + function test_solveEmergency( + address defaultAdmin, + address retryGuardian, + address solveEmergencyGuardian, + address ccc + ) public setUpGranularGuardians(defaultAdmin, retryGuardian, solveEmergencyGuardian, ccc) { + vm.startPrank(solveEmergencyGuardian); + _solveEmergency(ccc); + vm.stopPrank(); + } + + function test_solveEmergencyWhenWrongCaller( + address defaultAdmin, + address retryGuardian, + address solveEmergencyGuardian, + address ccc, + address solveCaller + ) public setUpGranularGuardians(defaultAdmin, retryGuardian, solveEmergencyGuardian, ccc) { + vm.assume(solveCaller != solveEmergencyGuardian); + hoax(solveCaller); + vm.expectRevert( + bytes( + string.concat( + 'AccessControl: account 0x', + TestUtils.toAsciiString(solveCaller), + ' is missing role 0xf4cdc679c22cbf47d6de8e836ce79ffdae51f38408dcde3f0645de7634fa607d' + ) + ) + ); + _solveEmergency(ccc); + } + + function test_updateGuardian( + address defaultAdmin, + address retryGuardian, + address solveEmergencyGuardian, + address ccc, + address newGuardian + ) public setUpGranularGuardians(defaultAdmin, retryGuardian, solveEmergencyGuardian, ccc) { + _filterAddress(newGuardian); + vm.startPrank(defaultAdmin); + vm.mockCall( + ccc, + abi.encodeWithSelector(IWithGuardian.updateGuardian.selector, newGuardian), + abi.encode() + ); + control.updateGuardian(newGuardian); + vm.stopPrank(); + } + + function test_updateGuardianWhenWrongCaller( + address defaultAdmin, + address retryGuardian, + address solveEmergencyGuardian, + address ccc, + address guardianCaller, + address newGuardian + ) public setUpGranularGuardians(defaultAdmin, retryGuardian, solveEmergencyGuardian, ccc) { + vm.assume(guardianCaller != defaultAdmin); + hoax(guardianCaller); + vm.expectRevert( + bytes( + string.concat( + 'AccessControl: account 0x', + TestUtils.toAsciiString(guardianCaller), + ' is missing role 0x0000000000000000000000000000000000000000000000000000000000000000' + ) + ) + ); + control.updateGuardian(newGuardian); + } + + function test_updateGuardianWhenWrongAddress( + address defaultAdmin, + address retryGuardian, + address solveEmergencyGuardian, + address ccc + ) public setUpGranularGuardians(defaultAdmin, retryGuardian, solveEmergencyGuardian, ccc) { + vm.startPrank(defaultAdmin); + vm.expectRevert(IGranularGuardianAccessControl.NewGuardianCantBe0.selector); + control.updateGuardian(address(0)); + vm.stopPrank(); + } + + function _solveEmergency(address ccc) public { + ICrossChainReceiver.ConfirmationInput[] + memory newConfirmations = new ICrossChainReceiver.ConfirmationInput[](0); + ICrossChainReceiver.ValidityTimestampInput[] + memory newValidityTimestamp = new ICrossChainReceiver.ValidityTimestampInput[](0); + ICrossChainReceiver.ReceiverBridgeAdapterConfigInput[] + memory receiverBridgeAdaptersToAllow = new ICrossChainReceiver.ReceiverBridgeAdapterConfigInput[]( + 0 + ); + ICrossChainReceiver.ReceiverBridgeAdapterConfigInput[] + memory receiverBridgeAdaptersToDisallow = new ICrossChainReceiver.ReceiverBridgeAdapterConfigInput[]( + 0 + ); + address[] memory sendersToApprove = new address[](0); + address[] memory sendersToRemove = new address[](0); + ICrossChainForwarder.ForwarderBridgeAdapterConfigInput[] + memory forwarderBridgeAdaptersToEnable = new ICrossChainForwarder.ForwarderBridgeAdapterConfigInput[]( + 0 + ); + ICrossChainForwarder.BridgeAdapterToDisable[] + memory forwarderBridgeAdaptersToDisable = new ICrossChainForwarder.BridgeAdapterToDisable[]( + 0 + ); + + vm.mockCall( + ccc, + abi.encodeWithSelector( + ICrossChainControllerWithEmergencyMode.solveEmergency.selector, + newConfirmations, + newValidityTimestamp, + receiverBridgeAdaptersToAllow, + receiverBridgeAdaptersToDisallow, + sendersToApprove, + sendersToRemove, + forwarderBridgeAdaptersToEnable, + forwarderBridgeAdaptersToDisable + ), + abi.encode() + ); + return + control.solveEmergency( + newConfirmations, + newValidityTimestamp, + receiverBridgeAdaptersToAllow, + receiverBridgeAdaptersToDisallow, + sendersToApprove, + sendersToRemove, + forwarderBridgeAdaptersToEnable, + forwarderBridgeAdaptersToDisable + ); + } + + function _retryEnvelope(address ccc, bytes32 mockEnvId) internal returns (bytes32) { + Envelope memory envelope = Envelope({ + nonce: 1, + origin: address(12468), + destination: address(2341), + originChainId: 1, + destinationChainId: 137, + message: abi.encode('mock message') + }); + uint256 gasLimit = 300_000; + + vm.mockCall( + ccc, + abi.encodeWithSelector(ICrossChainForwarder.retryEnvelope.selector, envelope, gasLimit), + abi.encode(mockEnvId) + ); + return control.retryEnvelope(envelope, gasLimit); + } + + function _retryTx(address ccc) internal { + bytes memory encodedTransaction = abi.encode('some mock tx'); + uint256 gasLimit = 300_000; + address[] memory bridgeAdaptersToRetry = new address[](0); + + vm.mockCall( + ccc, + abi.encodeWithSelector( + ICrossChainForwarder.retryTransaction.selector, + encodedTransaction, + gasLimit, + bridgeAdaptersToRetry + ), + abi.encode() + ); + control.retryTransaction(encodedTransaction, gasLimit, bridgeAdaptersToRetry); + } +} diff --git a/tests/access_control/GranularGuardianAccessControl_int.t.sol b/tests/access_control/GranularGuardianAccessControl_int.t.sol new file mode 100644 index 00000000..c9da1f19 --- /dev/null +++ b/tests/access_control/GranularGuardianAccessControl_int.t.sol @@ -0,0 +1,291 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {TestUtils} from '../utils/TestUtils.sol'; +import '../../src/contracts/access_control/GranularGuardianAccessControl.sol'; +import {OwnableWithGuardian, IWithGuardian} from 'solidity-utils/contracts/access-control/OwnableWithGuardian.sol'; +import '../BaseTest.sol'; +import {GovernanceV3Polygon} from 'aave-address-book/GovernanceV3Polygon.sol'; +import {MiscPolygon} from 'aave-address-book/MiscPolygon.sol'; + +contract GranularGuardianAccessControlIntTest is BaseTest { + bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00; + address public constant BGD_GUARDIAN = 0xbCEB4f363f2666E2E8E430806F37e97C405c130b; + + address public constant AAVE_GUARDIAN = MiscPolygon.PROTOCOL_GUARDIAN; + + // list of supported chains + uint256 destinationChainId = ChainIds.ETHEREUM; + + GranularGuardianAccessControl public control; + address public ccc; + + modifier createGGAC(address retryGuardian, address solveEmergencyGuardian) { + vm.assume(retryGuardian != address(this)); + vm.assume(solveEmergencyGuardian != address(this)); + _filterAddress(retryGuardian); + _filterAddress(solveEmergencyGuardian); + + IGranularGuardianAccessControl.InitialGuardians + memory initialGuardians = IGranularGuardianAccessControl.InitialGuardians({ + defaultAdmin: AAVE_GUARDIAN, + retryGuardian: retryGuardian, + solveEmergencyGuardian: solveEmergencyGuardian + }); + control = new GranularGuardianAccessControl( + initialGuardians, + GovernanceV3Polygon.CROSS_CHAIN_CONTROLLER + ); + + hoax(BGD_GUARDIAN); + OwnableWithGuardian(GovernanceV3Polygon.CROSS_CHAIN_CONTROLLER).updateGuardian( + address(control) + ); + _; + } + + function setUp() public { + vm.createSelectFork('polygon', 56006881); + } + + function test_initialization( + address retryGuardian, + address solveEmergencyGuardian + ) public createGGAC(retryGuardian, solveEmergencyGuardian) { + assertEq(control.CROSS_CHAIN_CONTROLLER(), GovernanceV3Polygon.CROSS_CHAIN_CONTROLLER); + assertEq(control.hasRole(DEFAULT_ADMIN_ROLE, AAVE_GUARDIAN), true); + assertEq(control.getRoleAdmin(control.RETRY_ROLE()), DEFAULT_ADMIN_ROLE); + assertEq(control.getRoleAdmin(control.SOLVE_EMERGENCY_ROLE()), DEFAULT_ADMIN_ROLE); + assertEq(control.getRoleMemberCount(control.RETRY_ROLE()), 1); + assertEq(control.getRoleMemberCount(control.SOLVE_EMERGENCY_ROLE()), 1); + assertEq(control.getRoleMember(control.RETRY_ROLE(), 0), retryGuardian); + assertEq(control.getRoleMember(control.SOLVE_EMERGENCY_ROLE(), 0), solveEmergencyGuardian); + } + + function test_retryTx( + address retryGuardian, + address solveEmergencyGuardian, + address destination, + uint256 gasLimit + ) + public + createGGAC(retryGuardian, solveEmergencyGuardian) + generateRetryTxState( + GovernanceV3Polygon.EXECUTOR_LVL_1, + GovernanceV3Polygon.CROSS_CHAIN_CONTROLLER, + destinationChainId, + destination, + gasLimit + ) + { + _retryTx(destination, retryGuardian, gasLimit); + } + + function _retryTx(address destination, address retryGuardian, uint256 gasLimit) internal { + ExtendedTransaction memory extendedTx = _generateExtendedTransaction( + TestParams({ + destination: destination, + origin: address(this), + originChainId: block.chainid, + destinationChainId: destinationChainId, + envelopeNonce: ICrossChainForwarder(GovernanceV3Polygon.CROSS_CHAIN_CONTROLLER) + .getCurrentTransactionNonce() - 1, + transactionNonce: ICrossChainForwarder(GovernanceV3Polygon.CROSS_CHAIN_CONTROLLER) + .getCurrentTransactionNonce() - 1 + }) + ); + + ICrossChainForwarder.ChainIdBridgeConfig[] memory bridgeAdaptersByChain = ICrossChainForwarder( + GovernanceV3Polygon.CROSS_CHAIN_CONTROLLER + ).getForwarderBridgeAdaptersByChain(1); + address[] memory bridgeAdaptersToRetry = new address[](1); + bridgeAdaptersToRetry[0] = bridgeAdaptersByChain[0].currentChainBridgeAdapter; + + vm.startPrank(retryGuardian); + control.retryTransaction(extendedTx.transactionEncoded, gasLimit, bridgeAdaptersToRetry); + vm.stopPrank(); + } + + function test_retryTxWhenWrongCaller( + address retryGuardian, + address solveEmergencyGuardian, + uint256 gasLimit, + address caller + ) public createGGAC(retryGuardian, solveEmergencyGuardian) { + address[] memory bridgeAdaptersToRetry = new address[](0); + vm.assume(caller != retryGuardian); + + hoax(caller); + vm.expectRevert( + bytes( + string.concat( + 'AccessControl: account 0x', + TestUtils.toAsciiString(caller), + ' is missing role 0xc448b9502bbdf9850cc39823b6ea40cfe96d3ac63008e89edd2b8e98c6cc0af3' + ) + ) + ); + + control.retryTransaction(abi.encode('will not get used'), gasLimit, bridgeAdaptersToRetry); + } + + function test_retryEnvelope( + address retryGuardian, + address solveEmergencyGuardian, + address destination, + uint256 gasLimit + ) + public + createGGAC(retryGuardian, solveEmergencyGuardian) + generateRetryTxState( + GovernanceV3Polygon.EXECUTOR_LVL_1, + GovernanceV3Polygon.CROSS_CHAIN_CONTROLLER, + destinationChainId, + destination, + gasLimit + ) + { + _retryEnvelope(destination, retryGuardian, gasLimit); + } + + function _retryEnvelope(address destination, address retryGuardian, uint256 gasLimit) internal { + uint256 envNonce = ICrossChainForwarder(GovernanceV3Polygon.CROSS_CHAIN_CONTROLLER) + .getCurrentEnvelopeNonce() - 1; + + ExtendedTransaction memory extendedTx = _generateExtendedTransaction( + TestParams({ + destination: destination, + origin: address(this), + originChainId: block.chainid, + destinationChainId: destinationChainId, + envelopeNonce: envNonce, + transactionNonce: ICrossChainForwarder(GovernanceV3Polygon.CROSS_CHAIN_CONTROLLER) + .getCurrentTransactionNonce() - 1 + }) + ); + + vm.startPrank(retryGuardian); + bytes32 newTxId = control.retryEnvelope(extendedTx.envelope, gasLimit); + + ExtendedTransaction memory extendedTxAfter = _generateExtendedTransaction( + TestParams({ + destination: destination, + origin: address(this), + originChainId: block.chainid, + destinationChainId: destinationChainId, + envelopeNonce: envNonce, + transactionNonce: ICrossChainForwarder(GovernanceV3Polygon.CROSS_CHAIN_CONTROLLER) + .getCurrentTransactionNonce() - 1 + }) + ); + + assertEq(extendedTxAfter.transactionId, newTxId); + vm.stopPrank(); + } + + function test_retryEnvelopeWhenWrongCaller( + address retryGuardian, + address solveEmergencyGuardian, + uint256 gasLimit, + address caller + ) public createGGAC(retryGuardian, solveEmergencyGuardian) { + vm.assume(caller != retryGuardian); + + Envelope memory envelope; + + hoax(caller); + vm.expectRevert( + bytes( + string.concat( + 'AccessControl: account 0x', + TestUtils.toAsciiString(caller), + ' is missing role 0xc448b9502bbdf9850cc39823b6ea40cfe96d3ac63008e89edd2b8e98c6cc0af3' + ) + ) + ); + control.retryEnvelope(envelope, gasLimit); + } + + function test_solveEmergency( + address retryGuardian, + address solveEmergencyGuardian + ) + public + createGGAC(retryGuardian, solveEmergencyGuardian) + generateEmergencyState(GovernanceV3Polygon.CROSS_CHAIN_CONTROLLER) + validateEmergencySolved(GovernanceV3Polygon.CROSS_CHAIN_CONTROLLER) + { + vm.startPrank(solveEmergencyGuardian); + control.solveEmergency( + new ICrossChainReceiver.ConfirmationInput[](0), + new ICrossChainReceiver.ValidityTimestampInput[](0), + new ICrossChainReceiver.ReceiverBridgeAdapterConfigInput[](0), + new ICrossChainReceiver.ReceiverBridgeAdapterConfigInput[](0), + new address[](0), + new address[](0), + new ICrossChainForwarder.ForwarderBridgeAdapterConfigInput[](0), + new ICrossChainForwarder.BridgeAdapterToDisable[](0) + ); + vm.stopPrank(); + } + + function test_solveEmergencyWhenWrongCaller( + address retryGuardian, + address solveEmergencyGuardian, + address caller + ) public createGGAC(retryGuardian, solveEmergencyGuardian) { + vm.assume(caller != solveEmergencyGuardian); + hoax(caller); + vm.expectRevert( + bytes( + string.concat( + 'AccessControl: account 0x', + TestUtils.toAsciiString(caller), + ' is missing role 0xf4cdc679c22cbf47d6de8e836ce79ffdae51f38408dcde3f0645de7634fa607d' + ) + ) + ); + control.solveEmergency( + new ICrossChainReceiver.ConfirmationInput[](0), + new ICrossChainReceiver.ValidityTimestampInput[](0), + new ICrossChainReceiver.ReceiverBridgeAdapterConfigInput[](0), + new ICrossChainReceiver.ReceiverBridgeAdapterConfigInput[](0), + new address[](0), + new address[](0), + new ICrossChainForwarder.ForwarderBridgeAdapterConfigInput[](0), + new ICrossChainForwarder.BridgeAdapterToDisable[](0) + ); + } + + function test_updateGuardian( + address retryGuardian, + address solveEmergencyGuardian, + address newGuardian + ) public createGGAC(retryGuardian, solveEmergencyGuardian) { + _filterAddress(newGuardian); + vm.startPrank(AAVE_GUARDIAN); + control.updateGuardian(newGuardian); + assertEq(IWithGuardian(GovernanceV3Polygon.CROSS_CHAIN_CONTROLLER).guardian(), newGuardian); + vm.stopPrank(); + } + + function test_updateGuardianWhenWrongCaller( + address retryGuardian, + address solveEmergencyGuardian, + address newGuardian, + address caller + ) public createGGAC(retryGuardian, solveEmergencyGuardian) { + vm.assume(caller != AAVE_GUARDIAN); + hoax(caller); + vm.expectRevert( + bytes( + string.concat( + 'AccessControl: account 0x', + TestUtils.toAsciiString(caller), + ' is missing role 0x0000000000000000000000000000000000000000000000000000000000000000' + ) + ) + ); + control.updateGuardian(newGuardian); + } +} diff --git a/tests/utils/TestUtils.sol b/tests/utils/TestUtils.sol new file mode 100644 index 00000000..218be293 --- /dev/null +++ b/tests/utils/TestUtils.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +library TestUtils { + function toAsciiString(address x) internal pure returns (string memory) { + bytes memory s = new bytes(40); + for (uint i = 0; i < 20; i++) { + bytes1 b = bytes1(uint8(uint(uint160(x)) / (2 ** (8 * (19 - i))))); + bytes1 hi = bytes1(uint8(b) / 16); + bytes1 lo = bytes1(uint8(b) - 16 * uint8(hi)); + s[2 * i] = char(hi); + s[2 * i + 1] = char(lo); + } + return string(s); + } + + function char(bytes1 b) internal pure returns (bytes1 c) { + if (uint8(b) < 10) return bytes1(uint8(b) + 0x30); + else return bytes1(uint8(b) + 0x57); + } +}