diff --git a/.gitmodules b/.gitmodules index 837cb4ae2..11a62babb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "lib/aave-address-book"] path = lib/aave-address-book url = https://github.com/bgd-labs/aave-address-book +[submodule "lib/governance-crosschain-bridges"] + path = lib/governance-crosschain-bridges + url = https://github.com/aave/governance-crosschain-bridges diff --git a/Makefile b/Makefile index 7283f4b36..77eca7782 100644 --- a/Makefile +++ b/Makefile @@ -14,16 +14,19 @@ test-rates-factory:; forge test -vvv --match-path src/test/V3RateStrategyFactory # Scripts -deploy-engine-eth :; forge script script/AaveV3ConfigEngine.s.sol:DeployEngineEth --rpc-url mainnet --broadcast --legacy --ledger --mnemonic-indexes ${MNEMONIC_INDEX} --sender ${LEDGER_SENDER} --verify -vvvv -deploy-engine-opt :; forge script script/AaveV3ConfigEngine.s.sol:DeployEngineOpt --rpc-url optimism --broadcast --legacy --ledger --mnemonic-indexes ${MNEMONIC_INDEX} --sender ${LEDGER_SENDER} --verify -vvvv -deploy-engine-arb :; forge script script/AaveV3ConfigEngine.s.sol:DeployEngineArb --rpc-url arbitrum --broadcast --legacy --ledger --mnemonic-indexes ${MNEMONIC_INDEX} --sender ${LEDGER_SENDER} --verify -vvvv -deploy-engine-pol :; forge script script/AaveV3ConfigEngine.s.sol:DeployEnginePol --rpc-url polygon --broadcast --legacy --ledger --mnemonic-indexes ${MNEMONIC_INDEX} --sender ${LEDGER_SENDER} --verify -vvvv -deploy-engine-ava :; forge script script/AaveV3ConfigEngine.s.sol:DeployEngineAva --rpc-url avalanche --broadcast --legacy --ledger --mnemonic-indexes ${MNEMONIC_INDEX} --sender ${LEDGER_SENDER} --verify -vvvv -deploy-rates-factory-eth :; forge script script/V3RateStrategyFactory.s.sol:DeployRatesFactoryEth --rpc-url mainnet --broadcast --legacy --ledger --mnemonic-indexes ${MNEMONIC_INDEX} --sender ${LEDGER_SENDER} --verify -vvvv -deploy-rates-factory-pol :; forge script script/V3RateStrategyFactory.s.sol:DeployRatesFactoryPol --rpc-url polygon --broadcast --legacy --ledger --mnemonic-indexes ${MNEMONIC_INDEX} --sender ${LEDGER_SENDER} --verify -vvvv -deploy-rates-factory-opt :; forge script script/V3RateStrategyFactory.s.sol:DeployRatesFactoryOpt --rpc-url optimism --broadcast --legacy --ledger --mnemonic-indexes ${MNEMONIC_INDEX} --sender ${LEDGER_SENDER} --verify -vvvv -deploy-rates-factory-arb :; forge script script/V3RateStrategyFactory.s.sol:DeployRatesFactoryArb --rpc-url arbitrum --broadcast --legacy --ledger --mnemonic-indexes ${MNEMONIC_INDEX} --sender ${LEDGER_SENDER} --verify -vvvv -deploy-rates-factory-ava :; forge script script/V3RateStrategyFactory.s.sol:DeployRatesFactoryAva --rpc-url avalanche --broadcast --legacy --ledger --mnemonic-indexes ${MNEMONIC_INDEX} --sender ${LEDGER_SENDER} --verify -vvvv +deploy-engine-eth :; forge script scripts/AaveV3ConfigEngine.s.sol:DeployEngineEth --rpc-url mainnet --broadcast --legacy --ledger --mnemonic-indexes ${MNEMONIC_INDEX} --sender ${LEDGER_SENDER} --verify -vvvv +deploy-engine-opt :; forge script scripts/AaveV3ConfigEngine.s.sol:DeployEngineOpt --rpc-url optimism --broadcast --legacy --ledger --mnemonic-indexes ${MNEMONIC_INDEX} --sender ${LEDGER_SENDER} --verify -vvvv +deploy-engine-arb :; forge script scripts/AaveV3ConfigEngine.s.sol:DeployEngineArb --rpc-url arbitrum --broadcast --legacy --ledger --mnemonic-indexes ${MNEMONIC_INDEX} --sender ${LEDGER_SENDER} --verify -vvvv +deploy-engine-pol :; forge script scripts/AaveV3ConfigEngine.s.sol:DeployEnginePol --rpc-url polygon --broadcast --legacy --ledger --mnemonic-indexes ${MNEMONIC_INDEX} --sender ${LEDGER_SENDER} --verify -vvvv +deploy-engine-ava :; forge script scripts/AaveV3ConfigEngine.s.sol:DeployEngineAva --rpc-url avalanche --broadcast --legacy --ledger --mnemonic-indexes ${MNEMONIC_INDEX} --sender ${LEDGER_SENDER} --verify -vvvv +deploy-rates-factory-eth :; forge script scripts/V3RateStrategyFactory.s.sol:DeployRatesFactoryEth --rpc-url mainnet --broadcast --legacy --ledger --mnemonic-indexes ${MNEMONIC_INDEX} --sender ${LEDGER_SENDER} --verify -vvvv +deploy-rates-factory-pol :; forge script scripts/V3RateStrategyFactory.s.sol:DeployRatesFactoryPol --rpc-url polygon --broadcast --legacy --ledger --mnemonic-indexes ${MNEMONIC_INDEX} --sender ${LEDGER_SENDER} --verify -vvvv +deploy-rates-factory-opt :; forge script scripts/V3RateStrategyFactory.s.sol:DeployRatesFactoryOpt --rpc-url optimism --broadcast --legacy --ledger --mnemonic-indexes ${MNEMONIC_INDEX} --sender ${LEDGER_SENDER} --verify -vvvv +deploy-rates-factory-arb :; forge script scripts/V3RateStrategyFactory.s.sol:DeployRatesFactoryArb --rpc-url arbitrum --broadcast --legacy --ledger --mnemonic-indexes ${MNEMONIC_INDEX} --sender ${LEDGER_SENDER} --verify -vvvv +deploy-rates-factory-ava :; forge script scripts/V3RateStrategyFactory.s.sol:DeployRatesFactoryAva --rpc-url avalanche --broadcast --legacy --ledger --mnemonic-indexes ${MNEMONIC_INDEX} --sender ${LEDGER_SENDER} --verify -vvvv +deploy-forwarder-pol :; forge script scripts/CrosschainForwarders.s.sol:DeployPol --rpc-url mainnet --broadcast --ledger --mnemonic-indexes ${MNEMONIC_INDEX} --sender ${LEDGER_SENDER} --verify -vvvv +deploy-forwarder-opt :; forge script scripts/CrosschainForwarders.s.sol:DeployOpt --rpc-url mainnet --broadcast --ledger --mnemonic-indexes ${MNEMONIC_INDEX} --sender ${LEDGER_SENDER} --verify -vvvv +deploy-forwarder-arb :; forge script scripts/CrosschainForwarders.s.sol:DeployArb --rpc-url mainnet --broadcast --ledger --mnemonic-indexes ${MNEMONIC_INDEX} --sender ${LEDGER_SENDER} --verify -vvvv # Utilities download :; cast etherscan-source --chain ${chain} -d src/etherscan/${chain}_${address} ${address} diff --git a/lib/aave-address-book b/lib/aave-address-book index 751ffd0af..9a64715cf 160000 --- a/lib/aave-address-book +++ b/lib/aave-address-book @@ -1 +1 @@ -Subproject commit 751ffd0affc9bd1609753ea47a7d9ac23f391dd8 +Subproject commit 9a64715cff37ff7ff9a7da3e3dd04a55cad78ccb diff --git a/lib/forge-std b/lib/forge-std index dc1901fa9..f4264050a 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit dc1901fa900fc2ceabb4aae91d8a3d6c0c2e0392 +Subproject commit f4264050aca5a2eedf243f9bd54b544c5a373bd2 diff --git a/lib/governance-crosschain-bridges b/lib/governance-crosschain-bridges new file mode 160000 index 000000000..32d66bdb4 --- /dev/null +++ b/lib/governance-crosschain-bridges @@ -0,0 +1 @@ +Subproject commit 32d66bdb45a02208c84056a741680fdfc8aed9ae diff --git a/lib/solidity-utils b/lib/solidity-utils index 21fec63a9..8f83c0144 160000 --- a/lib/solidity-utils +++ b/lib/solidity-utils @@ -1 +1 @@ -Subproject commit 21fec63a93ff6752a35cdaf347f0557b85d5b7c1 +Subproject commit 8f83c0144c522d2867dc5f448f68659b46281813 diff --git a/remappings.txt b/remappings.txt index 7cf9be487..abcb66ce2 100644 --- a/remappings.txt +++ b/remappings.txt @@ -5,4 +5,5 @@ aave-v3-core/=lib/aave-address-book/lib/aave-v3-core/ aave-v3-periphery/=lib/aave-address-book/lib/aave-v3-periphery/ ds-test/=lib/forge-std/lib/ds-test/src/ forge-std/=lib/forge-std/src/ -solidity-utils/=lib/solidity-utils/src/ \ No newline at end of file +solidity-utils/=lib/solidity-utils/src/ +governance-crosschain-bridges/=lib/governance-crosschain-bridges/ diff --git a/script/AaveV3ConfigEngine.s.sol b/scripts/AaveV3ConfigEngine.s.sol similarity index 100% rename from script/AaveV3ConfigEngine.s.sol rename to scripts/AaveV3ConfigEngine.s.sol diff --git a/scripts/CrosschainForwarders.s.sol b/scripts/CrosschainForwarders.s.sol new file mode 100644 index 000000000..e68148410 --- /dev/null +++ b/scripts/CrosschainForwarders.s.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import './Utils.s.sol'; +import {CrosschainForwarderPolygon} from '../src/crosschainforwarders/CrosschainForwarderPolygon.sol'; +import {CrosschainForwarderOptimism} from '../src/crosschainforwarders/CrosschainForwarderOptimism.sol'; +import {CrosschainForwarderArbitrum} from '../src/crosschainforwarders/CrosschainForwarderArbitrum.sol'; + +contract DeployPol is EthereumScript { + function run() external broadcast { + new CrosschainForwarderPolygon(); + } +} + +contract DeployOpt is EthereumScript { + function run() external broadcast { + new CrosschainForwarderOptimism(); + } +} + +contract DeployArb is EthereumScript { + function run() external broadcast { + new CrosschainForwarderArbitrum(); + } +} diff --git a/script/Utils.s.sol b/scripts/Utils.s.sol similarity index 100% rename from script/Utils.s.sol rename to scripts/Utils.s.sol diff --git a/script/V3RateStrategyFactory.s.sol b/scripts/V3RateStrategyFactory.s.sol similarity index 100% rename from script/V3RateStrategyFactory.s.sol rename to scripts/V3RateStrategyFactory.s.sol diff --git a/src/crosschainforwarders/CrosschainForwarderArbitrum.sol b/src/crosschainforwarders/CrosschainForwarderArbitrum.sol new file mode 100644 index 000000000..9c0aa56f2 --- /dev/null +++ b/src/crosschainforwarders/CrosschainForwarderArbitrum.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {AaveGovernanceV2} from 'aave-address-book/AaveGovernanceV2.sol'; +import {AaveV3Arbitrum} from 'aave-address-book/AaveV3Arbitrum.sol'; +import {IInbox} from 'governance-crosschain-bridges/contracts/dependencies/arbitrum/interfaces/IInbox.sol'; +import {IL2BridgeExecutor} from 'governance-crosschain-bridges/contracts/interfaces/IL2BridgeExecutor.sol'; + +/** + * @title A generic executor for proposals targeting the arbitrum v3 pool + * @author BGD Labs + * @notice You can **only** use this executor when the arbitrum payload has a `execute()` signature without parameters + * @notice You can **only** use this executor when the arbitrum payload is expected to be executed via `DELEGATECALL` + * @notice This contract assumes to be called via AAVE Governance V2 + * @notice This contract will assume the SHORT_EXECUTOR will be topped up with enough funds to fund the short executor + * @dev This executor is a generic wrapper to be used with Arbitrum Inbox (https://developer.offchainlabs.com/arbos/l1-to-l2-messaging) + * It encodes a parameterless `execute()` with delegate calls and a specified target. + * This encoded abi is then send to the Inbox to be synced executed on the arbitrum network. + * Once synced the ARBITRUM_BRIDGE_EXECUTOR will queue the execution of the payload. + */ +contract CrosschainForwarderArbitrum { + IInbox public constant INBOX = IInbox(0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f); + address public constant ARBITRUM_BRIDGE_EXECUTOR = AaveGovernanceV2.ARBITRUM_BRIDGE_EXECUTOR; + address public constant ARBITRUM_GUARDIAN = 0xbbd9f90699c1FA0D7A65870D241DD1f1217c96Eb; + + // amount of gwei to overpay on basefee for fast submission + uint256 public constant BASE_FEE_MARGIN = 10 gwei; + + /** + * @dev calculateRetryableSubmissionFee on `0x00000000000000000000000000000000000000C8` for a queue call with 1 slot will yield a constant gasLimit of `429478` + * To account for some margin we rounded up to 450000 + */ + uint256 public constant L2_GAS_LIMIT = 450000; + + /** + * @dev There is currently no oracle on L1 exposing gasPrice of arbitrum. Therefore we overpay by assuming 1 gwei (10x of current price). + */ + uint256 public constant L2_MAX_FEE_PER_GAS = 1 gwei; + + /** + * @dev returns the amount of gas needed for submitting the ticket + * @param bytesLength the payload bytes length (usually 580) + * @return uint256 maxSubmissionFee needed on L2 with BASE_FEE_MARGIN + * @return uint256 estimated L2 redepmption fee + */ + function getRequiredGas(uint256 bytesLength) public view returns (uint256, uint256) { + return ( + INBOX.calculateRetryableSubmissionFee(bytesLength, block.basefee + BASE_FEE_MARGIN), + L2_GAS_LIMIT * L2_MAX_FEE_PER_GAS + ); + } + + /** + * @dev checks if the short executor is topped up with enough eth for proposal execution + * with current basefee + * @param bytesLength the payload bytes length (usually 580) + * @return bool indicating if the SHORT_EXECUTOR has sufficient funds + * @return uint256 the gas required for ticket creation and redemption + */ + function hasSufficientGasForExecution(uint256 bytesLength) public view returns (bool, uint256) { + (uint256 maxSubmission, uint256 maxRedemption) = getRequiredGas(bytesLength); + uint256 requiredGas = maxSubmission + maxRedemption; + return (AaveGovernanceV2.SHORT_EXECUTOR.balance >= requiredGas, requiredGas); + } + + /** + * @dev encodes the queue call which is forwarded to arbitrum + * @param l2PayloadContract the address of the arbitrum payload + */ + function getEncodedPayload(address l2PayloadContract) public pure returns (bytes memory) { + address[] memory targets = new address[](1); + targets[0] = l2PayloadContract; + uint256[] memory values = new uint256[](1); + values[0] = 0; + string[] memory signatures = new string[](1); + signatures[0] = 'execute()'; + bytes[] memory calldatas = new bytes[](1); + calldatas[0] = ''; + bool[] memory withDelegatecalls = new bool[](1); + withDelegatecalls[0] = true; + return + abi.encodeWithSelector( + IL2BridgeExecutor.queue.selector, + targets, + values, + signatures, + calldatas, + withDelegatecalls + ); + } + + /** + * @dev this function will be executed once the proposal passes the mainnet vote. + * @param l2PayloadContract the arbitrum contract containing the `execute()` signature. + */ + function execute(address l2PayloadContract) public { + bytes memory queue = getEncodedPayload(l2PayloadContract); + (uint256 maxSubmission, uint256 maxRedemption) = getRequiredGas(queue.length); + INBOX.unsafeCreateRetryableTicket{value: maxSubmission + maxRedemption}( + ARBITRUM_BRIDGE_EXECUTOR, + 0, // l2CallValue + maxSubmission, // maxSubmissionCost + address(ARBITRUM_BRIDGE_EXECUTOR), // excessFeeRefundAddress + address(ARBITRUM_GUARDIAN), // callValueRefundAddress + L2_GAS_LIMIT, // gasLimit + L2_MAX_FEE_PER_GAS, // maxFeePerGas + queue + ); + } +} diff --git a/src/crosschainforwarders/CrosschainForwarderOptimism.sol b/src/crosschainforwarders/CrosschainForwarderOptimism.sol new file mode 100644 index 000000000..fca432e82 --- /dev/null +++ b/src/crosschainforwarders/CrosschainForwarderOptimism.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {ICrossDomainMessenger} from 'governance-crosschain-bridges/contracts/dependencies/optimism/interfaces/ICrossDomainMessenger.sol'; +import {IL2BridgeExecutor} from 'governance-crosschain-bridges/contracts/interfaces/IL2BridgeExecutor.sol'; + +interface ICanonicalTransactionChain { + function enqueueL2GasPrepaid() external view returns (uint256); +} + +/** + * @title A generic executor for proposals targeting the optimism v3 pool + * @author BGD Labs + * @notice You can **only** use this executor when the optimism payload has a `execute()` signature without parameters + * @notice You can **only** use this executor when the optimism payload is expected to be executed via `DELEGATECALL` + * @notice You can **only** execute payloads on optimism with up to prepayed gas which is specified in `enqueueL2GasPrepaid` gas. + * Prepaid gas is the maximum gas covered by the bridge without additional payment. + * @dev This executor is a generic wrapper to be used with Optimism CrossDomainMessenger (https://etherscan.io/address/0x25ace71c97b33cc4729cf772ae268934f7ab5fa1) + * It encodes and sends via the L2CrossDomainMessenger a message to queue for execution an action on L2, in the Aave OPTIMISM_BRIDGE_EXECUTOR. + */ +contract CrosschainForwarderOptimism { + /** + * @dev The L1 Cross Domain Messenger contract sends messages from L1 to L2, and relays messages + * from L2 onto L1. In this contract it's used by the governance SHORT_EXECUTOR to send the encoded L2 queuing over the bridge. + */ + address public constant L1_CROSS_DOMAIN_MESSENGER_ADDRESS = + 0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1; + + /** + * @dev The optimism bridge executor is a L2 governance execution contract. + * This contract allows queuing of proposals by allow listed addresses (in this case the L1 short executor). + * https://optimistic.etherscan.io/address/0x7d9103572bE58FfE99dc390E8246f02dcAe6f611 + */ + address public constant OPTIMISM_BRIDGE_EXECUTOR = 0x7d9103572bE58FfE99dc390E8246f02dcAe6f611; + + /** + * @dev The CTC contract is an append only log of transactions which must be applied to the rollup state. + * It also holds configurations like the currently prepayed amount of gas which is what this contract is utilizing. + * https://etherscan.io/address/0x5e4e65926ba27467555eb562121fac00d24e9dd2#code + */ + ICanonicalTransactionChain public constant CANONICAL_TRANSACTION_CHAIN = + ICanonicalTransactionChain(0x5E4e65926BA27467555EB562121fac00D24E9dD2); + + /** + * @dev this function will be executed once the proposal passes the mainnet vote. + * @param l2PayloadContract the optimism contract containing the `execute()` signature. + */ + function execute(address l2PayloadContract) public { + address[] memory targets = new address[](1); + targets[0] = l2PayloadContract; + uint256[] memory values = new uint256[](1); + values[0] = 0; + string[] memory signatures = new string[](1); + signatures[0] = 'execute()'; + bytes[] memory calldatas = new bytes[](1); + calldatas[0] = ''; + bool[] memory withDelegatecalls = new bool[](1); + withDelegatecalls[0] = true; + + bytes memory queue = abi.encodeWithSelector( + IL2BridgeExecutor.queue.selector, + targets, + values, + signatures, + calldatas, + withDelegatecalls + ); + ICrossDomainMessenger(L1_CROSS_DOMAIN_MESSENGER_ADDRESS).sendMessage( + OPTIMISM_BRIDGE_EXECUTOR, + queue, + uint32(CANONICAL_TRANSACTION_CHAIN.enqueueL2GasPrepaid()) + ); + } +} diff --git a/src/crosschainforwarders/CrosschainForwarderPolygon.sol b/src/crosschainforwarders/CrosschainForwarderPolygon.sol new file mode 100644 index 000000000..1ada02d6c --- /dev/null +++ b/src/crosschainforwarders/CrosschainForwarderPolygon.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IFxStateSender} from 'governance-crosschain-bridges/contracts/dependencies/polygon/fxportal/FxRoot.sol'; + +/** + * @title A generic executor for proposals targeting the polygon v3 pool + * @author BGD Labs + * @notice You can **only** use this executor when the polygon payload has a `execute()` signature without parameters + * @notice You can **only** use this executor when the polygon payload is expected to be executed via `DELEGATECALL` + * @dev This executor is a generic wrapper to be used with FX bridges (https://github.com/fx-portal/contracts) + * It encodes a parameterless `execute()` with delegate calls and a specified target. + * This encoded abi is then send to the FX-root to be synced to the FX-child on the polygon network. + * Once synced the POLYGON_BRIDGE_EXECUTOR will queue the execution of the payload. + */ +contract CrosschainForwarderPolygon { + address public constant FX_ROOT_ADDRESS = 0xfe5e5D361b2ad62c541bAb87C45a0B9B018389a2; + address public constant POLYGON_BRIDGE_EXECUTOR = 0xdc9A35B16DB4e126cFeDC41322b3a36454B1F772; + + /** + * @dev this function will be executed once the proposal passes the mainnet vote. + * @param l2PayloadContract the polygon contract containing the `execute()` signature. + */ + function execute(address l2PayloadContract) public { + address[] memory targets = new address[](1); + targets[0] = l2PayloadContract; + uint256[] memory values = new uint256[](1); + values[0] = 0; + string[] memory signatures = new string[](1); + signatures[0] = 'execute()'; + bytes[] memory calldatas = new bytes[](1); + calldatas[0] = ''; + bool[] memory withDelegatecalls = new bool[](1); + withDelegatecalls[0] = true; + + bytes memory actions = abi.encode(targets, values, signatures, calldatas, withDelegatecalls); + IFxStateSender(FX_ROOT_ADDRESS).sendMessageToChild(POLYGON_BRIDGE_EXECUTOR, actions); + } +} diff --git a/src/crosschainforwarders/README.md b/src/crosschainforwarders/README.md new file mode 100644 index 000000000..4e6ed72b8 --- /dev/null +++ b/src/crosschainforwarders/README.md @@ -0,0 +1,48 @@ +# CrosschainForwarder + +## About + +To simplify the process of creating a cross-chain proposal this repository contains opinionated `CrosschainForwarder` contracts for `Polygon`, `Optimism` and `Arbitrum` abstracting away the complexity of bridging & cross-chain gas calculations. +All the forwarders follow the same pattern. They expect a payload to be deployed on L2 and to be executed with a parameterless `execute()` signature and via `DELEGATECALL`. + +![visualization](./bridge-listing.png) + +### Polygon + +For a proposal to be executed on Polygon it needs to pass a mainnet governance proposal that sends an encoded payload via `sendMessageToChild(address,bytes)` on [FX_ROOT](https://etherscan.io/address/0xfe5e5D361b2ad62c541bAb87C45a0B9B018389a2#code)(mainnet) to [FX_CHILD](https://polygonscan.com/address/0x8397259c983751DAf40400790063935a11afa28a#code)(Polygon). +Once the state is synced to `FX_CHILD` on Polygon network it will queue the payload on [POLYGON_BRIDGE_EXECUTOR](https://polygonscan.com/address/0xdc9A35B16DB4e126cFeDC41322b3a36454B1F772#code). + +### Optimism + +For a proposal to be executed on Optimism it needs to pass a mainnet governance proposal that sends an encoded payload via `sendMessage(address,bytes,uint32)` on [L1_CROSS_DOMAIN_MESSENGER](https://etherscan.io/address/0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1)(mainnet) to [L2_CROSS_DOMAIN_MESSENGER](https://optimistic.etherscan.io/address/0x4200000000000000000000000000000000000007#code)(Optimism). +Once the state is `L2_CROSS_DOMAIN_MESSENGER` on Optimism it will queue the payload on [OPTIMISM_BRIDGE_EXECUTOR](https://optimistic.etherscan.io/address/0x7d9103572bE58FfE99dc390E8246f02dcAe6f611). + +### Arbitrum + +For a proposal to be executed on Arbitrum it needs to pass a mainnet governance proposal that sends an encoded payload via `unsafeCreateRetryableTicket{value: uint256}(address,uint256,uint256,address,address,uint256,uint256,bytes)` on [INBOX](https://etherscan.io/address/0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f)(mainnet). The Arbitrum bridge will then call the bridged calldata via the L2_ALIAS of the mainnet `msg.sender` (in this case is the aliased mainnet governance executor) which will queue the payload on [ARBITRUM_BRIDGE_EXECUTOR](https://arbiscan.io/address/0x7d9103572bE58FfE99dc390E8246f02dcAe6f611). + +Caveat: Opposed to the other bridges, Arbitrum inbox bridge requires you to supply some gas. +For simplicity the `CrosschainForwarderArbitrum` expects some eth to be available on the [SHORT_EXECUTOR](https://etherscan.io/address/0xEE56e2B3D491590B5b31738cC34d5232F378a8D5). +You can check if you need to top-up the SHORT_EXECUTOR by calling `getRequiredGas(580)` on the `CrosschainForwarderArbitrum`. + +## Deployed addresses + +### Forwarders + +- [CrosschainForwarderPolygon](https://etherscan.io/address/0x158a6bc04f0828318821bae797f50b0a1299d45b#code) +- [CrosschainForwarderOptimism](https://etherscan.io/address/0x5f5c02875a8e9b5a26fbd09040abcfdeb2aa6711#code) +- [CrosschainForwarderArbitrum](https://etherscan.io/address/0x2e2B1F112C4D79A9D22464F0D345dE9b792705f1#code) + +## References + +- [PolygonBridge: FxRoot](https://etherscan.io/address/0xfe5e5d361b2ad62c541bab87c45a0b9b018389a2#code) +- [PolygonBridge: PolygonBridgeExecutor](https://polygonscan.com/address/0xdc9A35B16DB4e126cFeDC41322b3a36454B1F772#code) + +- [OptimismBridge: L1CrossDomainMessenger](https://etherscan.io/address/0x25ace71c97b33cc4729cf772ae268934f7ab5fa1#readProxyContract) +- [OptimismBridge: OptimismBridgeExecutor](https://optimistic.etherscan.io/address/0x7d9103572be58ffe99dc390e8246f02dcae6f611#code) + +- [ArbitrumBridge: Inbox](https://etherscan.io/address/0x4dbd4fc535ac27206064b68ffcf827b0a60bab3f#code) +- [ArtitrumBridge: ArbitrumBridgeExecutor](https://arbiscan.io/address/0x7d9103572be58ffe99dc390e8246f02dcae6f611#code) + +- [aave crosschain-bridge repository](https://github.com/aave/governance-crosschain-bridges#polygon-governance-bridge) +- [first ever Polygon bridge proposal](https://github.com/pakim249CAL/Polygon-Asset-Deployment-Generic-Executor) diff --git a/src/crosschainforwarders/bridge-listing.png b/src/crosschainforwarders/bridge-listing.png new file mode 100644 index 000000000..2ffb59451 Binary files /dev/null and b/src/crosschainforwarders/bridge-listing.png differ diff --git a/src/test/AaveV3ConfigEngineGauntletProposal.t.sol b/src/test/AaveV3ConfigEngineGauntletProposal.t.sol index 8832308af..0f73bbda8 100644 --- a/src/test/AaveV3ConfigEngineGauntletProposal.t.sol +++ b/src/test/AaveV3ConfigEngineGauntletProposal.t.sol @@ -14,7 +14,7 @@ import {AaveV3PolygonRatesUpdates070322} from './mocks/gauntlet-updates/AaveV3Po import {AaveV3AvalancheRatesUpdates070322} from './mocks/gauntlet-updates/AaveV3AvalancheRatesUpdates070322.sol'; import {AaveV3OptimismRatesUpdates070322} from './mocks/gauntlet-updates/AaveV3OptimismRatesUpdates070322.sol'; import {AaveV3ArbitrumRatesUpdates070322} from './mocks/gauntlet-updates/AaveV3ArbitrumRatesUpdates070322.sol'; -import {DeployEnginePolLib, DeployEngineEthLib, DeployEngineAvaLib, DeployEngineOptLib, DeployEngineArbLib} from '../../script/AaveV3ConfigEngine.s.sol'; +import {DeployEnginePolLib, DeployEngineEthLib, DeployEngineAvaLib, DeployEngineOptLib, DeployEngineArbLib} from '../../scripts/AaveV3ConfigEngine.s.sol'; import {TestWithExecutor} from '../GovHelpers.sol'; import '../ProtocolV3TestBase.sol'; diff --git a/src/test/AaveV3ConfigEngineTest.t.sol b/src/test/AaveV3ConfigEngineTest.t.sol index 32bddec83..c91fff936 100644 --- a/src/test/AaveV3ConfigEngineTest.t.sol +++ b/src/test/AaveV3ConfigEngineTest.t.sol @@ -9,8 +9,8 @@ import {AaveV3AvalancheCollateralUpdate} from './mocks/AaveV3AvalancheCollateral import {AaveV3PolygonBorrowUpdate} from './mocks/AaveV3PolygonBorrowUpdate.sol'; import {AaveV3PolygonPriceFeedUpdate} from './mocks/AaveV3PolygonPriceFeedUpdate.sol'; import {AaveV3OptimismMockRatesUpdate} from './mocks/AaveV3OptimismMockRatesUpdate.sol'; -import {DeployRatesFactoryPolLib, DeployRatesFactoryEthLib, DeployRatesFactoryAvaLib, DeployRatesFactoryArbLib, DeployRatesFactoryOptLib} from '../../script/V3RateStrategyFactory.s.sol'; -import {DeployEnginePolLib, DeployEngineEthLib, DeployEngineAvaLib, DeployEngineOptLib, DeployEngineArbLib} from '../../script/AaveV3ConfigEngine.s.sol'; +import {DeployRatesFactoryPolLib, DeployRatesFactoryEthLib, DeployRatesFactoryAvaLib, DeployRatesFactoryArbLib, DeployRatesFactoryOptLib} from '../../scripts/V3RateStrategyFactory.s.sol'; +import {DeployEnginePolLib, DeployEngineEthLib, DeployEngineAvaLib, DeployEngineOptLib, DeployEngineArbLib} from '../../scripts/AaveV3ConfigEngine.s.sol'; import {AaveV3Ethereum, AaveV3Polygon, AaveV3Optimism, AaveV3Avalanche, AaveV3Arbitrum} from 'aave-address-book/AaveAddressBook.sol'; import {AaveV3OptimismAssets} from 'aave-address-book/AaveV3Optimism.sol'; import {AaveV3PolygonAssets} from 'aave-address-book/AaveV3Polygon.sol'; diff --git a/src/test/crosschainforwarders/ArbitrumCrossChainForwarderTest.t.sol b/src/test/crosschainforwarders/ArbitrumCrossChainForwarderTest.t.sol new file mode 100644 index 000000000..98dc9a48c --- /dev/null +++ b/src/test/crosschainforwarders/ArbitrumCrossChainForwarderTest.t.sol @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; +import {AaveV3Arbitrum, AaveMisc} from 'aave-address-book/AaveAddressBook.sol'; +import {AaveGovernanceV2, IExecutorWithTimelock} from 'aave-address-book/AaveGovernanceV2.sol'; +import {AddressAliasHelper} from 'governance-crosschain-bridges/contracts/dependencies/arbitrum/AddressAliasHelper.sol'; +import {IInbox} from 'governance-crosschain-bridges/contracts/dependencies/arbitrum/interfaces/IInbox.sol'; +import {IL2BridgeExecutor} from 'governance-crosschain-bridges/contracts/interfaces/IL2BridgeExecutor.sol'; +import {GovHelpers} from '../../GovHelpers.sol'; +import {ProtocolV3TestBase, ReserveConfig, ReserveTokens, IERC20} from '../../ProtocolV3TestBase.sol'; +import {BridgeExecutorHelpers} from '../../BridgeExecutorHelpers.sol'; +import {ProtocolV3TestBase, ReserveConfig} from '../../ProtocolV3TestBase.sol'; +import {CrosschainForwarderArbitrum} from '../../crosschainforwarders/CrosschainForwarderArbitrum.sol'; +import {PayloadWithEmit} from '../mocks/PayloadWithEmit.sol'; + +/** + * This test covers syncing between mainnet and arbitrum. + */ +contract ArbitrumCrossChainForwarderTest is ProtocolV3TestBase { + event TestEvent(); + + // the identifiers of the forks + uint256 mainnetFork; + uint256 arbitrumFork; + + PayloadWithEmit public payloadWithEmit; + + IInbox public constant INBOX = IInbox(0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f); + + address public constant ARBITRUM_BRIDGE_EXECUTOR = AaveGovernanceV2.ARBITRUM_BRIDGE_EXECUTOR; + + uint256 public constant MESSAGE_LENGTH = 580; + + CrosschainForwarderArbitrum public forwarder; + + function setUp() public { + mainnetFork = vm.createSelectFork(vm.rpcUrl('mainnet'), 16128510); + forwarder = new CrosschainForwarderArbitrum(); + arbitrumFork = vm.createSelectFork(vm.rpcUrl('arbitrum'), 62456736); + payloadWithEmit = new PayloadWithEmit(); + } + + // utility to transform memory to calldata so array range access is available + function _cutBytes(bytes calldata input) public pure returns (bytes calldata) { + return input[64:]; + } + + function testHasSufficientGas() public { + vm.selectFork(mainnetFork); + assertEq(AaveGovernanceV2.SHORT_EXECUTOR.balance, 0); + (bool hasEnoughGasBefore, ) = forwarder.hasSufficientGasForExecution(580); + assertEq(hasEnoughGasBefore, false); + deal(address(AaveGovernanceV2.SHORT_EXECUTOR), 0.001 ether); + (bool hasEnoughGasAfter, ) = forwarder.hasSufficientGasForExecution(580); + assertEq(hasEnoughGasAfter, true); + } + + function testgetGetMaxSubmissionCost() public { + vm.selectFork(mainnetFork); + (uint256 maxSubmission, ) = forwarder.getRequiredGas(580); + assertGt(maxSubmission, 0); + } + + function testProposalE2E() public { + // assumes the short exec will be topped up with some eth to pay for l2 fee + vm.selectFork(mainnetFork); + deal(address(AaveGovernanceV2.SHORT_EXECUTOR), 0.001 ether); + + // 1. create l1 proposal + vm.startPrank(AaveMisc.ECOSYSTEM_RESERVE); + GovHelpers.Payload[] memory payloads = new GovHelpers.Payload[](1); + payloads[0] = GovHelpers.Payload({ + target: address(forwarder), + signature: 'execute(address)', + callData: abi.encode(address(payloadWithEmit)) + }); + + uint256 proposalId = GovHelpers.createProposal( + payloads, + 0xec9d2289ab7db9bfbf2b0f2dd41ccdc0a4003e9e0d09e40dee09095145c63fb5 + ); + vm.stopPrank(); + + // 2. execute proposal and record logs so we can extract the emitted StateSynced event + vm.recordLogs(); + bytes memory payload = forwarder.getEncodedPayload(address(payloadWithEmit)); + + (uint256 maxSubmission, ) = forwarder.getRequiredGas(580); + // check ticket is created correctly + vm.expectCall( + address(INBOX), + abi.encodeCall( + IInbox.unsafeCreateRetryableTicket, + ( + ARBITRUM_BRIDGE_EXECUTOR, + 0, + maxSubmission, + forwarder.ARBITRUM_BRIDGE_EXECUTOR(), + forwarder.ARBITRUM_GUARDIAN(), + forwarder.L2_GAS_LIMIT(), + forwarder.L2_MAX_FEE_PER_GAS(), + payload + ) + ) + ); + GovHelpers.passVoteAndExecute(vm, proposalId); + + // check events are emitted correctly + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(keccak256('InboxMessageDelivered(uint256,bytes)'), entries[3].topics[0]); + // uint256 messageId = uint256(entries[3].topics[1]); + ( + address to, + uint256 callvalue, + uint256 value, + uint256 maxSubmissionCost, + address excessFeeRefundAddress, + address callValueRefundAddress, + uint256 maxGas, + uint256 gasPriceBid, + uint256 length + ) = abi.decode( + this._cutBytes(entries[3].data), + (address, uint256, uint256, uint256, address, address, uint256, uint256, uint256) + ); + assertEq(callvalue, 0); + assertEq(value > 0, true); + assertEq(maxSubmissionCost > 0, true); + assertEq(to, ARBITRUM_BRIDGE_EXECUTOR); + assertEq(excessFeeRefundAddress, ARBITRUM_BRIDGE_EXECUTOR); + assertEq(callValueRefundAddress, forwarder.ARBITRUM_GUARDIAN()); + assertEq(maxGas, forwarder.L2_GAS_LIMIT()); + assertEq(gasPriceBid, forwarder.L2_MAX_FEE_PER_GAS()); + assertEq(length, 580); + + // 3. mock the queuing on l2 with the data emitted on InboxMessageDelivered + vm.selectFork(arbitrumFork); + vm.startPrank(AddressAliasHelper.applyL1ToL2Alias(AaveGovernanceV2.SHORT_EXECUTOR)); + ARBITRUM_BRIDGE_EXECUTOR.call(payload); + vm.stopPrank(); + // 4. execute the proposal + vm.expectEmit(true, true, true, true); + emit TestEvent(); + BridgeExecutorHelpers.waitAndExecuteLatest(vm, ARBITRUM_BRIDGE_EXECUTOR); + } +} diff --git a/src/test/crosschainforwarders/OptimismCrossChainForwarderTest.t.sol b/src/test/crosschainforwarders/OptimismCrossChainForwarderTest.t.sol new file mode 100644 index 000000000..6369c5478 --- /dev/null +++ b/src/test/crosschainforwarders/OptimismCrossChainForwarderTest.t.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; +import {AaveV3Optimism, AaveMisc} from 'aave-address-book/AaveAddressBook.sol'; +import {AaveGovernanceV2, IExecutorWithTimelock} from 'aave-address-book/AaveGovernanceV2.sol'; +import {AddressAliasHelper} from 'governance-crosschain-bridges/contracts/dependencies/arbitrum/AddressAliasHelper.sol'; +import {IL2CrossDomainMessenger} from 'governance-crosschain-bridges/contracts/dependencies/optimism/interfaces/IL2CrossDomainMessenger.sol'; +import {GovHelpers} from '../../GovHelpers.sol'; +import {ProtocolV3TestBase, ReserveConfig, ReserveTokens, IERC20} from '../../ProtocolV3TestBase.sol'; +import {BridgeExecutorHelpers} from '../../BridgeExecutorHelpers.sol'; +import {CrosschainForwarderOptimism} from '../../crosschainforwarders/CrosschainForwarderOptimism.sol'; +import {PayloadWithEmit} from '../mocks/PayloadWithEmit.sol'; + +/** + * This test covers syncing between mainnet and optimism. + */ +contract OptimismCrossChainForwarderTest is ProtocolV3TestBase { + event TestEvent(); + // the identifiers of the forks + uint256 mainnetFork; + uint256 optimismFork; + + address public constant OPTIMISM_BRIDGE_EXECUTOR = AaveGovernanceV2.OPTIMISM_BRIDGE_EXECUTOR; + + IL2CrossDomainMessenger public OVM_L2_CROSS_DOMAIN_MESSENGER = + IL2CrossDomainMessenger(0x4200000000000000000000000000000000000007); + + PayloadWithEmit public payloadWithEmit; + + CrosschainForwarderOptimism public forwarder; + + function setUp() public { + mainnetFork = vm.createSelectFork(vm.rpcUrl('mainnet'), 15783218); + forwarder = new CrosschainForwarderOptimism(); + optimismFork = vm.createSelectFork(vm.rpcUrl('optimism'), 30264427); + payloadWithEmit = new PayloadWithEmit(); + } + + function testProposalE2E() public { + // 1. create l1 proposal + vm.selectFork(mainnetFork); + vm.startPrank(AaveMisc.ECOSYSTEM_RESERVE); + GovHelpers.Payload[] memory payloads = new GovHelpers.Payload[](1); + payloads[0] = GovHelpers.Payload({ + target: address(forwarder), + signature: 'execute(address)', + callData: abi.encode(address(payloadWithEmit)) + }); + + uint256 proposalId = GovHelpers.createProposal( + payloads, + 0x7ecafb3b0b7e418336cccb0c82b3e25944011bf11e41f8dc541841da073fe4f1 + ); + vm.stopPrank(); + + // 2. execute proposal and record logs so we can extract the emitted StateSynced event + vm.recordLogs(); + GovHelpers.passVoteAndExecute(vm, proposalId); + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(keccak256('SentMessage(address,address,bytes,uint256,uint256)'), entries[3].topics[0]); + assertEq(address(uint160(uint256(entries[3].topics[1]))), OPTIMISM_BRIDGE_EXECUTOR); + (address sender, bytes memory message, uint256 nonce) = abi.decode( + entries[3].data, + (address, bytes, uint256) + ); + + // 3. mock the receive on l2 with the data emitted on StateSynced + vm.selectFork(optimismFork); + vm.startPrank(0x36BDE71C97B33Cc4729cf772aE268934f7AB70B2); // AddressAliasHelper.applyL1ToL2Alias on L1_CROSS_DOMAIN_MESSENGER_ADDRESS + OVM_L2_CROSS_DOMAIN_MESSENGER.relayMessage(OPTIMISM_BRIDGE_EXECUTOR, sender, message, nonce); + vm.stopPrank(); + + // 4. execute proposal on l2 + vm.expectEmit(true, true, true, true); + emit TestEvent(); + BridgeExecutorHelpers.waitAndExecuteLatest(vm, OPTIMISM_BRIDGE_EXECUTOR); + } +} diff --git a/src/test/crosschainforwarders/PolygonCrossChainForwarderTest.t.sol b/src/test/crosschainforwarders/PolygonCrossChainForwarderTest.t.sol new file mode 100644 index 000000000..57959fc94 --- /dev/null +++ b/src/test/crosschainforwarders/PolygonCrossChainForwarderTest.t.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; +import {AaveV3Polygon, AaveMisc} from 'aave-address-book/AaveAddressBook.sol'; +import {AaveGovernanceV2, IExecutorWithTimelock} from 'aave-address-book/AaveGovernanceV2.sol'; +import {IStateReceiver} from 'governance-crosschain-bridges/contracts/dependencies/polygon/fxportal/FxChild.sol'; +import {GovHelpers} from '../../GovHelpers.sol'; +import {ProtocolV3TestBase, ReserveConfig, ReserveTokens, IERC20} from '../../ProtocolV3TestBase.sol'; +import {BridgeExecutorHelpers} from '../../BridgeExecutorHelpers.sol'; +import {CrosschainForwarderPolygon} from '../../crosschainforwarders/CrosschainForwarderPolygon.sol'; +import {PayloadWithEmit} from '../mocks/PayloadWithEmit.sol'; + +/** + * This test covers syncing between mainnet and polygon. + */ +contract PolygonCrossChainForwarderTest is ProtocolV3TestBase { + event TestEvent(); + // the identifiers of the forks + uint256 mainnetFork; + uint256 polygonFork; + + address public constant BRIDGE_ADMIN = 0x0000000000000000000000000000000000001001; + + address public constant FX_CHILD_ADDRESS = 0x8397259c983751DAf40400790063935a11afa28a; + + address public constant POLYGON_BRIDGE_EXECUTOR = 0xdc9A35B16DB4e126cFeDC41322b3a36454B1F772; + + PayloadWithEmit public payloadWithEmit; + + CrosschainForwarderPolygon public forwarder; + + function setUp() public { + mainnetFork = vm.createSelectFork(vm.rpcUrl('mainnet'), 15275388); + forwarder = new CrosschainForwarderPolygon(); + polygonFork = vm.createSelectFork(vm.rpcUrl('polygon'), 31507646); + payloadWithEmit = new PayloadWithEmit(); + } + + // utility to transform memory to calldata so array range access is available + function _cutBytes(bytes calldata input) public pure returns (bytes calldata) { + return input[64:]; + } + + function testProposalE2E() public { + // 1. create l1 proposal + vm.selectFork(mainnetFork); + vm.startPrank(AaveMisc.ECOSYSTEM_RESERVE); + GovHelpers.Payload[] memory payloads = new GovHelpers.Payload[](1); + payloads[0] = GovHelpers.Payload({ + target: address(forwarder), + signature: 'execute(address)', + callData: abi.encode(address(payloadWithEmit)) + }); + + uint256 proposalId = GovHelpers.createProposal( + payloads, + 0xf6e50d5a3f824f5ab4ffa15fb79f4fa1871b8bf7af9e9b32c1aaaa9ea633006d + ); + vm.stopPrank(); + + // 2. execute proposal and record logs so we can extract the emitted StateSynced event + vm.recordLogs(); + GovHelpers.passVoteAndExecute(vm, proposalId); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(keccak256('StateSynced(uint256,address,bytes)'), entries[2].topics[0]); + assertEq(address(uint160(uint256(entries[2].topics[2]))), FX_CHILD_ADDRESS); + + // 3. mock the receive on l2 with the data emitted on StateSynced + vm.selectFork(polygonFork); + vm.startPrank(BRIDGE_ADMIN); + IStateReceiver(FX_CHILD_ADDRESS).onStateReceive( + uint256(entries[2].topics[1]), + this._cutBytes(entries[2].data) + ); + vm.stopPrank(); + + // 4. Forward time & execute proposal + vm.expectEmit(true, true, true, true); + emit TestEvent(); + BridgeExecutorHelpers.waitAndExecuteLatest(vm, POLYGON_BRIDGE_EXECUTOR); + } +} diff --git a/src/test/mocks/PayloadWithEmit.sol b/src/test/mocks/PayloadWithEmit.sol new file mode 100644 index 000000000..0d65de5f9 --- /dev/null +++ b/src/test/mocks/PayloadWithEmit.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import {IPoolConfigurator, ConfiguratorInputTypes} from 'aave-address-book/AaveV3.sol'; +import {IProposalGenericExecutor} from '../../interfaces/IProposalGenericExecutor.sol'; + +/** + * @dev This payload simply emits an event on execution + */ +contract PayloadWithEmit is IProposalGenericExecutor { + event TestEvent(); + + function execute() external { + emit TestEvent(); + } +}