From 0bd1198de7b50dae416de5a3e0f79f7614a89728 Mon Sep 17 00:00:00 2001 From: Rens Rooimans Date: Fri, 25 Oct 2024 10:30:22 +0200 Subject: [PATCH] add gho --- .../ccip/pools/GHO/UpgradeableTokenPool.sol | 364 ++++++++++++++++++ .../src/v0.8/ccip/test/fork/aave/gho.sol | 277 +++++++++++++ 2 files changed, 641 insertions(+) create mode 100644 contracts/src/v0.8/ccip/pools/GHO/UpgradeableTokenPool.sol create mode 100644 contracts/src/v0.8/ccip/test/fork/aave/gho.sol diff --git a/contracts/src/v0.8/ccip/pools/GHO/UpgradeableTokenPool.sol b/contracts/src/v0.8/ccip/pools/GHO/UpgradeableTokenPool.sol new file mode 100644 index 0000000000..dbc8f68d98 --- /dev/null +++ b/contracts/src/v0.8/ccip/pools/GHO/UpgradeableTokenPool.sol @@ -0,0 +1,364 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {IPoolV1} from "../../interfaces/IPool.sol"; +import {IRMN} from "../../interfaces/IRMN.sol"; +import {IRouter} from "../../interfaces/IRouter.sol"; + +import {OwnerIsCreator} from "../../../shared/access/OwnerIsCreator.sol"; +import {RateLimiter} from "../../libraries/RateLimiter.sol"; + +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {IERC165} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/introspection/IERC165.sol"; +import {EnumerableSet} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/structs/EnumerableSet.sol"; + +/// @title UpgradeableTokenPool +/// @author Aave Labs +/// @notice Upgradeable version of Chainlink's CCIP TokenPool +/// @dev Contract adaptations: +/// - Setters & Getters for new ProxyPool (to support 1.5 CCIP migration on the existing 1.4 Pool) +/// - Modify `onlyOnRamp` modifier to accept transactions from ProxyPool +abstract contract UpgradeableTokenPool is IPoolV1, OwnerIsCreator { + using EnumerableSet for EnumerableSet.AddressSet; + using EnumerableSet for EnumerableSet.UintSet; + using RateLimiter for RateLimiter.TokenBucket; + + error CallerIsNotARampOnRouter(address caller); + error ZeroAddressNotAllowed(); + error SenderNotAllowed(address sender); + error AllowListNotEnabled(); + error NonExistentChain(uint64 remoteChainSelector); + error ChainNotAllowed(uint64 remoteChainSelector); + error BadARMSignal(); + error ChainAlreadyExists(uint64 chainSelector); + + event Locked(address indexed sender, uint256 amount); + event Burned(address indexed sender, uint256 amount); + event Released(address indexed sender, address indexed recipient, uint256 amount); + event Minted(address indexed sender, address indexed recipient, uint256 amount); + event ChainAdded( + uint64 remoteChainSelector, + RateLimiter.Config outboundRateLimiterConfig, + RateLimiter.Config inboundRateLimiterConfig + ); + event ChainConfigured( + uint64 remoteChainSelector, + RateLimiter.Config outboundRateLimiterConfig, + RateLimiter.Config inboundRateLimiterConfig + ); + event ChainRemoved(uint64 remoteChainSelector); + event AllowListAdd(address sender); + event AllowListRemove(address sender); + event RouterUpdated(address oldRouter, address newRouter); + + struct ChainUpdate { + uint64 remoteChainSelector; // ──╮ Remote chain selector + bool allowed; // ────────────────╯ Whether the chain is allowed + RateLimiter.Config outboundRateLimiterConfig; // Outbound rate limited config, meaning the rate limits for all of the onRamps for the given chain + RateLimiter.Config inboundRateLimiterConfig; // Inbound rate limited config, meaning the rate limits for all of the offRamps for the given chain + } + + /// @dev The storage slot for Proxy Pool address, act as an on ramp "wrapper" post ccip 1.5 migration. + /// @dev This was added to continue support for 1.2 onRamp during 1.5 migration, and is stored + /// this way to avoid storage collision. + // bytes32(uint256(keccak256("ccip.pools.GHO.UpgradeableTokenPool.proxyPool")) - 1) + bytes32 internal constant PROXY_POOL_SLOT = 0x75bb68f1b335d4dab6963140ecff58281174ef4362bb85a8593ab9379f24fae2; + + /// @dev The bridgeable token that is managed by this pool. + IERC20 internal immutable i_token; + /// @dev The address of the arm proxy + address internal immutable i_armProxy; + /// @dev The immutable flag that indicates if the pool is access-controlled. + bool internal immutable i_allowlistEnabled; + /// @dev A set of addresses allowed to trigger lockOrBurn as original senders. + /// Only takes effect if i_allowlistEnabled is true. + /// This can be used to ensure only token-issuer specified addresses can + /// move tokens. + EnumerableSet.AddressSet internal s_allowList; + /// @dev The address of the router + IRouter internal s_router; + /// @dev A set of allowed chain selectors. We want the allowlist to be enumerable to + /// be able to quickly determine (without parsing logs) who can access the pool. + /// @dev The chain selectors are in uin256 format because of the EnumerableSet implementation. + EnumerableSet.UintSet internal s_remoteChainSelectors; + /// @dev Outbound rate limits. Corresponds to the inbound rate limit for the pool + /// on the remote chain. + mapping(uint64 => RateLimiter.TokenBucket) internal s_outboundRateLimits; + /// @dev Inbound rate limits. This allows per destination chain + /// token issuer specified rate limiting (e.g. issuers may trust chains to varying + /// degrees and prefer different limits) + mapping(uint64 => RateLimiter.TokenBucket) internal s_inboundRateLimits; + + constructor(IERC20 token, address armProxy, bool allowlistEnabled) { + if (address(token) == address(0)) revert ZeroAddressNotAllowed(); + i_token = token; + i_armProxy = armProxy; + i_allowlistEnabled = allowlistEnabled; + } + + /// @notice Get ARM proxy address + /// @return armProxy Address of arm proxy + function getArmProxy() public view returns (address armProxy) { + return i_armProxy; + } + + function getToken() public view returns (IERC20 token) { + return i_token; + } + + /// @notice Gets the pool's Router + /// @return router The pool's Router + function getRouter() public view returns (address router) { + return address(s_router); + } + + /// @notice Sets the pool's Router + /// @param newRouter The new Router + function setRouter( + address newRouter + ) public onlyOwner { + if (newRouter == address(0)) revert ZeroAddressNotAllowed(); + address oldRouter = address(s_router); + s_router = IRouter(newRouter); + + emit RouterUpdated(oldRouter, newRouter); + } + + function supportsInterface( + bytes4 interfaceId + ) public pure virtual override returns (bool) { + return interfaceId == type(IPoolV1).interfaceId || interfaceId == type(IERC165).interfaceId; + } + + // ================================================================ + // │ Chain permissions │ + // ================================================================ + + /// @notice Checks whether a chain selector is permissioned on this contract. + /// @return true if the given chain selector is a permissioned remote chain. + function isSupportedChain( + uint64 remoteChainSelector + ) public view returns (bool) { + return s_remoteChainSelectors.contains(remoteChainSelector); + } + + /// @notice Get list of allowed chains + /// @return list of chains. + function getSupportedChains() public view returns (uint64[] memory) { + uint256[] memory uint256ChainSelectors = s_remoteChainSelectors.values(); + uint64[] memory chainSelectors = new uint64[](uint256ChainSelectors.length); + for (uint256 i = 0; i < uint256ChainSelectors.length; ++i) { + chainSelectors[i] = uint64(uint256ChainSelectors[i]); + } + + return chainSelectors; + } + + /// @notice Sets the permissions for a list of chains selectors. Actual senders for these chains + /// need to be allowed on the Router to interact with this pool. + /// @dev Only callable by the owner + /// @param chains A list of chains and their new permission status & rate limits. Rate limits + /// are only used when the chain is being added through `allowed` being true. + function applyChainUpdates( + ChainUpdate[] calldata chains + ) external virtual onlyOwner { + for (uint256 i = 0; i < chains.length; ++i) { + ChainUpdate memory update = chains[i]; + RateLimiter._validateTokenBucketConfig(update.outboundRateLimiterConfig, !update.allowed); + RateLimiter._validateTokenBucketConfig(update.inboundRateLimiterConfig, !update.allowed); + + if (update.allowed) { + // If the chain already exists, revert + if (!s_remoteChainSelectors.add(update.remoteChainSelector)) { + revert ChainAlreadyExists(update.remoteChainSelector); + } + + s_outboundRateLimits[update.remoteChainSelector] = RateLimiter.TokenBucket({ + rate: update.outboundRateLimiterConfig.rate, + capacity: update.outboundRateLimiterConfig.capacity, + tokens: update.outboundRateLimiterConfig.capacity, + lastUpdated: uint32(block.timestamp), + isEnabled: update.outboundRateLimiterConfig.isEnabled + }); + + s_inboundRateLimits[update.remoteChainSelector] = RateLimiter.TokenBucket({ + rate: update.inboundRateLimiterConfig.rate, + capacity: update.inboundRateLimiterConfig.capacity, + tokens: update.inboundRateLimiterConfig.capacity, + lastUpdated: uint32(block.timestamp), + isEnabled: update.inboundRateLimiterConfig.isEnabled + }); + emit ChainAdded(update.remoteChainSelector, update.outboundRateLimiterConfig, update.inboundRateLimiterConfig); + } else { + // If the chain doesn't exist, revert + if (!s_remoteChainSelectors.remove(update.remoteChainSelector)) { + revert NonExistentChain(update.remoteChainSelector); + } + + delete s_inboundRateLimits[update.remoteChainSelector]; + delete s_outboundRateLimits[update.remoteChainSelector]; + emit ChainRemoved(update.remoteChainSelector); + } + } + } + + // ================================================================ + // │ Rate limiting │ + // ================================================================ + + /// @notice Consumes outbound rate limiting capacity in this pool + function _consumeOutboundRateLimit(uint64 remoteChainSelector, uint256 amount) internal { + s_outboundRateLimits[remoteChainSelector]._consume(amount, address(i_token)); + } + + /// @notice Consumes inbound rate limiting capacity in this pool + function _consumeInboundRateLimit(uint64 remoteChainSelector, uint256 amount) internal { + s_inboundRateLimits[remoteChainSelector]._consume(amount, address(i_token)); + } + + /// @notice Gets the token bucket with its values for the block it was requested at. + /// @return The token bucket. + function getCurrentOutboundRateLimiterState( + uint64 remoteChainSelector + ) external view returns (RateLimiter.TokenBucket memory) { + return s_outboundRateLimits[remoteChainSelector]._currentTokenBucketState(); + } + + /// @notice Gets the token bucket with its values for the block it was requested at. + /// @return The token bucket. + function getCurrentInboundRateLimiterState( + uint64 remoteChainSelector + ) external view returns (RateLimiter.TokenBucket memory) { + return s_inboundRateLimits[remoteChainSelector]._currentTokenBucketState(); + } + + /// @notice Sets the chain rate limiter config. + /// @param remoteChainSelector The remote chain selector for which the rate limits apply. + /// @param outboundConfig The new outbound rate limiter config, meaning the onRamp rate limits for the given chain. + /// @param inboundConfig The new inbound rate limiter config, meaning the offRamp rate limits for the given chain. + function setChainRateLimiterConfig( + uint64 remoteChainSelector, + RateLimiter.Config memory outboundConfig, + RateLimiter.Config memory inboundConfig + ) external virtual onlyOwner { + _setRateLimitConfig(remoteChainSelector, outboundConfig, inboundConfig); + } + + function _setRateLimitConfig( + uint64 remoteChainSelector, + RateLimiter.Config memory outboundConfig, + RateLimiter.Config memory inboundConfig + ) internal { + if (!isSupportedChain(remoteChainSelector)) revert NonExistentChain(remoteChainSelector); + RateLimiter._validateTokenBucketConfig(outboundConfig, false); + s_outboundRateLimits[remoteChainSelector]._setTokenBucketConfig(outboundConfig); + RateLimiter._validateTokenBucketConfig(inboundConfig, false); + s_inboundRateLimits[remoteChainSelector]._setTokenBucketConfig(inboundConfig); + emit ChainConfigured(remoteChainSelector, outboundConfig, inboundConfig); + } + + // ================================================================ + // │ Access │ + // ================================================================ + + /// @notice Checks whether remote chain selector is configured on this contract, and if the msg.sender + /// is a permissioned onRamp for the given chain on the Router. + modifier onlyOnRamp( + uint64 remoteChainSelector + ) { + if (!isSupportedChain(remoteChainSelector)) revert ChainNotAllowed(remoteChainSelector); + if (!(msg.sender == getProxyPool() || msg.sender == s_router.getOnRamp(remoteChainSelector))) { + revert CallerIsNotARampOnRouter(msg.sender); + } + _; + } + + /// @notice Checks whether remote chain selector is configured on this contract, and if the msg.sender + /// is a permissioned offRamp for the given chain on the Router. + modifier onlyOffRamp( + uint64 remoteChainSelector + ) { + if (!isSupportedChain(remoteChainSelector)) revert ChainNotAllowed(remoteChainSelector); + if (!(msg.sender == getProxyPool() || s_router.isOffRamp(remoteChainSelector, msg.sender))) { + revert CallerIsNotARampOnRouter(msg.sender); + } + _; + } + + // ================================================================ + // │ Allowlist │ + // ================================================================ + + modifier checkAllowList( + address sender + ) { + if (i_allowlistEnabled && !s_allowList.contains(sender)) revert SenderNotAllowed(sender); + _; + } + + /// @notice Gets whether the allowList functionality is enabled. + /// @return true is enabled, false if not. + function getAllowListEnabled() external view returns (bool) { + return i_allowlistEnabled; + } + + /// @notice Gets the allowed addresses. + /// @return The allowed addresses. + function getAllowList() external view returns (address[] memory) { + return s_allowList.values(); + } + + /// @notice Apply updates to the allow list. + /// @param removes The addresses to be removed. + /// @param adds The addresses to be added. + /// @dev allowListing will be removed before public launch + function applyAllowListUpdates(address[] calldata removes, address[] calldata adds) external onlyOwner { + _applyAllowListUpdates(removes, adds); + } + + /// @notice Internal version of applyAllowListUpdates to allow for reuse in the constructor. + function _applyAllowListUpdates(address[] memory removes, address[] memory adds) internal { + if (!i_allowlistEnabled) revert AllowListNotEnabled(); + + for (uint256 i = 0; i < removes.length; ++i) { + address toRemove = removes[i]; + if (s_allowList.remove(toRemove)) { + emit AllowListRemove(toRemove); + } + } + for (uint256 i = 0; i < adds.length; ++i) { + address toAdd = adds[i]; + if (toAdd == address(0)) { + continue; + } + if (s_allowList.add(toAdd)) { + emit AllowListAdd(toAdd); + } + } + } + + /// @notice Ensure that there is no active curse. + modifier whenHealthy() { + if (IRMN(i_armProxy).isCursed()) revert BadARMSignal(); + _; + } + + /// @notice Getter for proxy pool address. + /// @return proxyPool The proxy pool address. + function getProxyPool() public view returns (address proxyPool) { + assembly ("memory-safe") { + proxyPool := shr(96, shl(96, sload(PROXY_POOL_SLOT))) + } + } + + /// @notice Setter for proxy pool address, only callable by the DAO. + /// @param proxyPool The address of the proxy pool. + function setProxyPool( + address proxyPool + ) external onlyOwner { + if (proxyPool == address(0)) revert ZeroAddressNotAllowed(); + assembly ("memory-safe") { + sstore(PROXY_POOL_SLOT, proxyPool) + } + } +} diff --git a/contracts/src/v0.8/ccip/test/fork/aave/gho.sol b/contracts/src/v0.8/ccip/test/fork/aave/gho.sol new file mode 100644 index 0000000000..b0792bb707 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/fork/aave/gho.sol @@ -0,0 +1,277 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import {Router} from "../../../Router.sol"; +import {Client} from "../../../libraries/Client.sol"; +import {Internal} from "../../../libraries/Internal.sol"; +import {RateLimiter} from "../../../libraries/RateLimiter.sol"; +import {EVM2EVMOffRamp} from "../../../offRamp/EVM2EVMOffRamp.sol"; +import {EVM2EVMOnRamp} from "../../../onRamp/EVM2EVMOnRamp.sol"; +import {BurnMintTokenPoolAndProxy} from "../../../pools/BurnMintTokenPoolAndProxy.sol"; +import {TokenPool} from "../../../pools/TokenPool.sol"; +import {TokenAdminRegistry} from "../../../tokenAdminRegistry/TokenAdminRegistry.sol"; +import {TokenPoolAndProxy} from "../../legacy/TokenPoolAndProxy.t.sol"; + +import {console2} from "forge-std/Console2.sol"; +import {Test} from "forge-std/Test.sol"; +import {Vm} from "forge-std/Vm.sol"; +import {IERC20} from "forge-std/interfaces/IERC20.sol"; + +contract GHO is Test { + uint256 private constant TOKENS_TO_SEND = 1; + bytes32 internal constant TypeAndVersion1_5_OffRamp = keccak256("EVM2EVMOffRamp 1.5.0"); + + struct ChainConfig { + Router router; + bool isMigrated; + uint64 block; + uint64 chainSelector; + address gho; + address newOnRamp; + address newOffRamp; + address proxyPool; + } + + ChainConfig public SEPOLIA = ChainConfig({ + router: Router(0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59), + isMigrated: true, + block: 6937993, + chainSelector: 16015286601757825753, + gho: 0xc4bF5CbDaBE595361438F8c6a187bDc330539c60, + newOnRamp: address(0), + newOffRamp: address(0), + proxyPool: address(0) + }); + + ChainConfig public ARBITRUM_SEPOLIA = ChainConfig({ + router: Router(0x2a9C5afB0d0e4BAb2BCdaE109EC4b0c4Be15a165), + isMigrated: true, + block: 91386151, + chainSelector: 3478487238524512106, + gho: 0xb13Cfa6f8B2Eed2C37fB00fF0c1A59807C585810, + newOnRamp: address(0), + newOffRamp: address(0), + proxyPool: address(0) + }); + + ChainConfig public ETHEREUM = ChainConfig({ + router: Router(0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D), + isMigrated: false, + block: 21032248, + chainSelector: 5009297550715157269, + gho: 0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f, + newOnRamp: 0x69eCC4E2D8ea56E2d0a05bF57f4Fd6aEE7f2c284, + newOffRamp: 0xdf615eF8D4C64d0ED8Fd7824BBEd2f6a10245aC9, + proxyPool: 0x9Ec9F9804733df96D1641666818eFb5198eC50f0 + }); + + ChainConfig public ARBITRUM = ChainConfig({ + router: Router(0x141fa059441E0ca23ce184B6A78bafD2A517DdE8), + isMigrated: false, + block: 266996190, + chainSelector: 4949039107694359620, + gho: 0x7dfF72693f6A4149b17e7C6314655f6A9F7c8B33, + newOnRamp: 0x67761742ac8A21Ec4D76CA18cbd701e5A6F3Bef3, + newOffRamp: 0x91e46cc5590A4B9182e47f40006140A7077Dec31, + proxyPool: 0x26329558f08cbb40d6a4CCA0E0C67b29D64A8c50 + }); + + function test_gho_sepolia() public { + uint256 sepoliaForkId = vm.createFork(vm.envString("SEPOLIA_RPC_URL"), SEPOLIA.block); + uint256 arbitrumSepoliaForkId = vm.createFork(vm.envString("ARB_SEPOLIA_RPC_URL"), ARBITRUM_SEPOLIA.block); + + validateDirection(SEPOLIA, ARBITRUM_SEPOLIA, sepoliaForkId, arbitrumSepoliaForkId); + } + + function test_gho_arbitrum_sep() public { + uint256 sepoliaForkId = vm.createFork(vm.envString("SEPOLIA_RPC_URL"), SEPOLIA.block); + uint256 arbitrumSepoliaForkId = vm.createFork(vm.envString("ARB_SEPOLIA_RPC_URL"), ARBITRUM_SEPOLIA.block); + + validateDirection(ARBITRUM_SEPOLIA, SEPOLIA, arbitrumSepoliaForkId, sepoliaForkId); + } + + function test_gho_ethereum() public { + uint256 ethereumForkId = vm.createFork(vm.envString("ETHEREUM_RPC_URL"), ETHEREUM.block); + uint256 arbitrumSepoliaForkId = vm.createFork(vm.envString("ARBITRUM_RPC_URL"), ARBITRUM.block); + + validateDirection(ETHEREUM, ARBITRUM, ethereumForkId, arbitrumSepoliaForkId); + } + + function test_gho_arbitrum() public { + uint256 ethereumForkId = vm.createFork(vm.envString("ETHEREUM_RPC_URL"), ETHEREUM.block); + uint256 arbitrumSepoliaForkId = vm.createFork(vm.envString("ARBITRUM_RPC_URL"), ARBITRUM.block); + + validateDirection(ARBITRUM, ETHEREUM, arbitrumSepoliaForkId, ethereumForkId); + } + + function validateDirection( + ChainConfig memory source, + ChainConfig memory dest, + uint256 sourceForkId, + uint256 destForkId + ) public { + vm.selectFork(sourceForkId); + vm.deal(address(this), 10_000 ether); + + if (!source.isMigrated) { + // Succeeds pre-migration + this.sendTokenMsg(source.router, source.gho, dest.chainSelector); + console2.log("GHO message sent pre migration"); + + _migrateChain(source, dest); + vm.selectFork(destForkId); + _migrateChain(dest, source); + vm.selectFork(sourceForkId); + console2.log("Chains migrated"); + + // This fails as the current pools are not correctly set up. + vm.expectRevert(); + this.sendTokenMsg(source.router, source.gho, dest.chainSelector); + console2.log("GHO message failed as expected post migration"); + + // TODO Actually migrate using the normal AAVE migration path + // Will use the normal CCIP path here to unblock the test, simply replace this function + // with the actual method and run the test again. + _setProxyAsRouter(source, dest); + + // We do the same migration on the dest chain + vm.selectFork(destForkId); + _setProxyAsRouter(dest, source); + vm.selectFork(sourceForkId); + console2.log("Pools migrated"); + } + + // Pools have now been migrated, ready to be tested. + Internal.EVM2EVMMessage memory ghoMsg = sendTokenMsg(source.router, source.gho, dest.chainSelector); + console2.log("GHO message sent post migration"); + + vm.selectFork(destForkId); + _executeMsg(dest.router, ghoMsg); + console2.log("GHO message executed post migration"); + } + + function sendTokenMsg( + Router router, + address token, + uint64 destChainSelector + ) public returns (Internal.EVM2EVMMessage memory) { + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = Client.EVMTokenAmount({token: token, amount: TOKENS_TO_SEND}); + + deal(token, address(this), TOKENS_TO_SEND); + + IERC20(token).approve(address(router), TOKENS_TO_SEND); + + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(makeAddr("GHO_receiver")), + data: "", + tokenAmounts: tokenAmounts, + feeToken: address(0), + extraArgs: "" + }); + uint256 fee = router.getFee(destChainSelector, message); + + // `vm.getRecordedLogs` consumes the buffer, we do this to ensure we only get the latest event + vm.getRecordedLogs(); + vm.recordLogs(); + router.ccipSend{value: fee}(destChainSelector, message); + + Vm.Log[] memory logs = vm.getRecordedLogs(); + for (uint256 i = 0; i < logs.length; ++i) { + if (logs[i].topics[0] == EVM2EVMOnRamp.CCIPSendRequested.selector) { + return abi.decode(logs[i].data, (Internal.EVM2EVMMessage)); + } + } + revert("No CCIPSendRequested event found"); + } + + // Emulates the migration to 1.5, the methods used are not necessarily representative of the actual migration. + function _migrateChain(ChainConfig memory source, ChainConfig memory dest) internal { + // Check if the token admin reg already supports the token + TokenAdminRegistry tokenAdminRegistry = + TokenAdminRegistry(EVM2EVMOnRamp(source.newOnRamp).getStaticConfig().tokenAdminRegistry); + + if (tokenAdminRegistry.getPool(source.gho) == address(0)) { + address adminRegOwner = tokenAdminRegistry.owner(); + vm.startPrank(adminRegOwner); + tokenAdminRegistry.proposeAdministrator(source.gho, adminRegOwner); + tokenAdminRegistry.acceptAdminRole(source.gho); + + tokenAdminRegistry.setPool(source.gho, source.proxyPool); + + vm.stopPrank(); + } + + address poolOwner = TokenPool(source.proxyPool).owner(); + if (TokenPool(source.proxyPool).getRouter() != address(source.router)) { + vm.prank(poolOwner); + TokenPool(source.proxyPool).setRouter(address(source.router)); + } + + if (TokenPool(source.proxyPool).getRemotePool(dest.chainSelector).length == 0) { + TokenPool.ChainUpdate[] memory chains = new TokenPool.ChainUpdate[](1); + chains[0] = TokenPool.ChainUpdate({ + remoteChainSelector: dest.chainSelector, + remotePoolAddress: abi.encode(dest.proxyPool), + remoteTokenAddress: abi.encode(dest.gho), + allowed: true, + outboundRateLimiterConfig: RateLimiter.Config({isEnabled: false, capacity: 0, rate: 0}), + inboundRateLimiterConfig: RateLimiter.Config({isEnabled: false, capacity: 0, rate: 0}) + }); + vm.prank(poolOwner); + TokenPool(source.proxyPool).applyChainUpdates(chains); + } + + EVM2EVMOnRamp.DynamicConfig memory dynamicConfig = EVM2EVMOnRamp(source.newOnRamp).getDynamicConfig(); + if (dynamicConfig.router != address(source.router)) { + dynamicConfig.router = address(source.router); + + vm.prank(EVM2EVMOnRamp(source.newOnRamp).owner()); + EVM2EVMOnRamp(source.newOnRamp).setDynamicConfig(dynamicConfig); + } + + Router.OnRamp[] memory onRampUpdates = new Router.OnRamp[](1); + onRampUpdates[0] = Router.OnRamp({destChainSelector: dest.chainSelector, onRamp: source.newOnRamp}); + Router.OffRamp[] memory offRampUpdates = new Router.OffRamp[](1); + offRampUpdates[0] = Router.OffRamp({sourceChainSelector: dest.chainSelector, offRamp: source.newOffRamp}); + + vm.prank(source.router.owner()); + source.router.applyRampUpdates(onRampUpdates, new Router.OffRamp[](0), offRampUpdates); + } + + function _setProxyAsRouter(ChainConfig memory source, ChainConfig memory dest) internal { + EVM2EVMOnRamp onRamp = EVM2EVMOnRamp(source.router.getOnRamp(dest.chainSelector)); + EVM2EVMOnRamp.StaticConfig memory staticConfig = onRamp.getStaticConfig(); + TokenAdminRegistry tokenAdminRegistry = TokenAdminRegistry(staticConfig.tokenAdminRegistry); + BurnMintTokenPoolAndProxy ghoProxyPool = BurnMintTokenPoolAndProxy(tokenAdminRegistry.getPool(source.gho)); + TokenPool nonProxyPool = TokenPool(ghoProxyPool.getPreviousPool()); + + address ghoOwner = nonProxyPool.owner(); + vm.prank(ghoOwner); + nonProxyPool.setRouter(address(ghoProxyPool)); + } + + function _executeMsg(Router router, Internal.EVM2EVMMessage memory message) internal { + EVM2EVMOffRamp offRamp = _getOffRamp(router, message.sourceChainSelector); + + vm.prank(address(offRamp)); + + // Empty gas overrides so it uses the values from the message. + offRamp.executeSingleMessage(message, new bytes[](message.tokenAmounts.length), new uint32[](1)); + } + + function _getOffRamp(Router router, uint64 sourceChainSelector) internal view returns (EVM2EVMOffRamp) { + Router.OffRamp[] memory offRamps = router.getOffRamps(); + for (uint256 i = 0; i < offRamps.length; ++i) { + Router.OffRamp memory configOffRamp = offRamps[i]; + if (configOffRamp.sourceChainSelector == sourceChainSelector) { + EVM2EVMOffRamp offRamp = EVM2EVMOffRamp(configOffRamp.offRamp); + if (keccak256(bytes(offRamp.typeAndVersion())) == TypeAndVersion1_5_OffRamp) { + return offRamp; + } + } + } + + revert("No offRamp found"); + } +}