diff --git a/.github/workflows/certora.yml b/.github/workflows/certora.yml index 7a2c63b81a..6a8e714868 100644 --- a/.github/workflows/certora.yml +++ b/.github/workflows/certora.yml @@ -20,13 +20,13 @@ jobs: with: { java-version: "11", java-package: jre } - name: Install certora cli - run: pip install certora-cli==7.6.3 + run: pip install certora-cli==7.17.2 - name: Install solc run: | - wget https://github.com/ethereum/solidity/releases/download/v0.8.10/solc-static-linux + wget https://github.com/ethereum/solidity/releases/download/v0.8.19/solc-static-linux chmod +x solc-static-linux - sudo mv solc-static-linux /usr/local/bin/solc8.10 + sudo mv solc-static-linux /usr/local/bin/solc8.19 - name: Verify rule ${{ matrix.rule }} run: | diff --git a/certora/confs/ccip.conf b/certora/confs/ccip.conf index 02245a1353..6df7e32e37 100644 --- a/certora/confs/ccip.conf +++ b/certora/confs/ccip.conf @@ -13,7 +13,7 @@ "process": "emv", "prover_args": ["-depth 10","-mediumTimeout 700"], "smt_timeout": "600", - "solc": "solc8.10", + "solc": "solc8.19", "verify": "UpgradeableLockReleaseTokenPool:certora/specs/ccip.spec", "rule_sanity": "basic", "msg": "CCIP" diff --git a/contracts/foundry.toml b/contracts/foundry.toml index c9b19de367..3ae4962578 100644 --- a/contracts/foundry.toml +++ b/contracts/foundry.toml @@ -16,6 +16,10 @@ gas_price = 1 block_timestamp = 1234567890 block_number = 12345 +[rpc_endpoints] +sepolia = "https://sepolia.gateway.tenderly.co" +arb_sepolia = "https://arbitrum-sepolia.gateway.tenderly.co" + [profile.ccip] solc_version = '0.8.19' src = 'src/v0.8/ccip' diff --git a/contracts/src/v0.8/ccip/pools/GHO/UpgradeableBurnMintTokenPool.sol b/contracts/src/v0.8/ccip/pools/GHO/UpgradeableBurnMintTokenPool.sol index 33c42760b7..a5cecc0430 100644 --- a/contracts/src/v0.8/ccip/pools/GHO/UpgradeableBurnMintTokenPool.sol +++ b/contracts/src/v0.8/ccip/pools/GHO/UpgradeableBurnMintTokenPool.sol @@ -2,14 +2,12 @@ pragma solidity ^0.8.0; import {Initializable} from "solidity-utils/contracts/transparent-proxy/Initializable.sol"; - import {ITypeAndVersion} from "../../../shared/interfaces/ITypeAndVersion.sol"; import {IBurnMintERC20} from "../../../shared/token/ERC20/IBurnMintERC20.sol"; import {UpgradeableTokenPool} from "./UpgradeableTokenPool.sol"; import {UpgradeableBurnMintTokenPoolAbstract} from "./UpgradeableBurnMintTokenPoolAbstract.sol"; import {RateLimiter} from "../../libraries/RateLimiter.sol"; - import {IRouter} from "../../interfaces/IRouter.sol"; /// @title UpgradeableBurnMintTokenPool @@ -19,6 +17,8 @@ import {IRouter} from "../../interfaces/IRouter.sol"; /// - Implementation of Initializable to allow upgrades /// - Move of allowlist and router definition to initialization stage /// - Inclusion of rate limit admin who may configure rate limits in addition to owner +/// - Modifications from inherited contract (see contract for more details): +/// - UpgradeableTokenPool: Modify `onlyOnRamp` & `onlyOffRamp` modifier to accept transactions from ProxyPool contract UpgradeableBurnMintTokenPool is Initializable, UpgradeableBurnMintTokenPoolAbstract, ITypeAndVersion { error Unauthorized(address caller); @@ -45,8 +45,7 @@ contract UpgradeableBurnMintTokenPool is Initializable, UpgradeableBurnMintToken /// @param allowlist A set of addresses allowed to trigger lockOrBurn as original senders /// @param router The address of the router function initialize(address owner, address[] memory allowlist, address router) public virtual initializer { - if (owner == address(0)) revert ZeroAddressNotAllowed(); - if (router == address(0)) revert ZeroAddressNotAllowed(); + if (owner == address(0) || router == address(0)) revert ZeroAddressNotAllowed(); _transferOwnership(owner); s_router = IRouter(router); diff --git a/contracts/src/v0.8/ccip/pools/GHO/UpgradeableLockReleaseTokenPool.sol b/contracts/src/v0.8/ccip/pools/GHO/UpgradeableLockReleaseTokenPool.sol index 8001ae59fa..4a63ab26ba 100644 --- a/contracts/src/v0.8/ccip/pools/GHO/UpgradeableLockReleaseTokenPool.sol +++ b/contracts/src/v0.8/ccip/pools/GHO/UpgradeableLockReleaseTokenPool.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.0; import {Initializable} from "solidity-utils/contracts/transparent-proxy/Initializable.sol"; - import {ITypeAndVersion} from "../../../shared/interfaces/ITypeAndVersion.sol"; import {ILiquidityContainer} from "../../../rebalancer/interfaces/ILiquidityContainer.sol"; @@ -11,7 +10,6 @@ import {RateLimiter} from "../../libraries/RateLimiter.sol"; import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; - import {IRouter} from "../../interfaces/IRouter.sol"; /// @title UpgradeableLockReleaseTokenPool @@ -21,6 +19,8 @@ import {IRouter} from "../../interfaces/IRouter.sol"; /// - Implementation of Initializable to allow upgrades /// - Move of allowlist and router definition to initialization stage /// - Addition of a bridge limit to regulate the maximum amount of tokens that can be transferred out (burned/locked) +/// - Modifications from inherited contract (see contract for more details): +/// - UpgradeableTokenPool: Modify `onlyOnRamp` & `onlyOffRamp` modifier to accept transactions from ProxyPool contract UpgradeableLockReleaseTokenPool is Initializable, UpgradeableTokenPool, ILiquidityContainer, ITypeAndVersion { using SafeERC20 for IERC20; @@ -86,8 +86,7 @@ contract UpgradeableLockReleaseTokenPool is Initializable, UpgradeableTokenPool, address router, uint256 bridgeLimit ) public virtual initializer { - if (owner == address(0)) revert ZeroAddressNotAllowed(); - if (router == address(0)) revert ZeroAddressNotAllowed(); + if (owner == address(0) || router == address(0)) revert ZeroAddressNotAllowed(); _transferOwnership(owner); s_router = IRouter(router); diff --git a/contracts/src/v0.8/ccip/pools/GHO/UpgradeableTokenPool.sol b/contracts/src/v0.8/ccip/pools/GHO/UpgradeableTokenPool.sol index ee359ac1f8..9c294974bc 100644 --- a/contracts/src/v0.8/ccip/pools/GHO/UpgradeableTokenPool.sol +++ b/contracts/src/v0.8/ccip/pools/GHO/UpgradeableTokenPool.sol @@ -12,9 +12,12 @@ import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/tok 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"; -/// @notice Base abstract class with common functions for all token pools. -/// A token pool serves as isolated place for holding tokens and token specific logic -/// that may execute as tokens move across the bridge. +/// @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` & `onlyOffRamp` modifier to accept transactions from ProxyPool abstract contract UpgradeableTokenPool is IPool, OwnerIsCreator, IERC165 { using EnumerableSet for EnumerableSet.AddressSet; using EnumerableSet for EnumerableSet.UintSet; @@ -55,6 +58,12 @@ abstract contract UpgradeableTokenPool is IPool, OwnerIsCreator, IERC165 { 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 @@ -250,7 +259,9 @@ abstract contract UpgradeableTokenPool is IPool, OwnerIsCreator, IERC165 { /// is a permissioned onRamp for the given chain on the Router. modifier onlyOnRamp(uint64 remoteChainSelector) { if (!isSupportedChain(remoteChainSelector)) revert ChainNotAllowed(remoteChainSelector); - if (!(msg.sender == s_router.getOnRamp(remoteChainSelector))) revert CallerIsNotARampOnRouter(msg.sender); + if (!(msg.sender == getProxyPool() || msg.sender == s_router.getOnRamp(remoteChainSelector))) { + revert CallerIsNotARampOnRouter(msg.sender); + } _; } @@ -258,7 +269,9 @@ abstract contract UpgradeableTokenPool is IPool, OwnerIsCreator, IERC165 { /// is a permissioned offRamp for the given chain on the Router. modifier onlyOffRamp(uint64 remoteChainSelector) { if (!isSupportedChain(remoteChainSelector)) revert ChainNotAllowed(remoteChainSelector); - if (!s_router.isOffRamp(remoteChainSelector, msg.sender)) revert CallerIsNotARampOnRouter(msg.sender); + if (!(msg.sender == getProxyPool() || s_router.isOffRamp(remoteChainSelector, msg.sender))) { + revert CallerIsNotARampOnRouter(msg.sender); + } _; } @@ -317,4 +330,21 @@ abstract contract UpgradeableTokenPool is IPool, OwnerIsCreator, IERC165 { if (IARM(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/pools/GHO/diffs/UpgradeableBurnMintTokenPoolAbstract_diff.md b/contracts/src/v0.8/ccip/pools/GHO/diffs/UpgradeableBurnMintTokenPoolAbstract_diff.md index 2255b2ca44..a1aa3e58c9 100644 --- a/contracts/src/v0.8/ccip/pools/GHO/diffs/UpgradeableBurnMintTokenPoolAbstract_diff.md +++ b/contracts/src/v0.8/ccip/pools/GHO/diffs/UpgradeableBurnMintTokenPoolAbstract_diff.md @@ -7,13 +7,13 @@ index f5eb135186..e228732855 100644 // SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.19; +pragma solidity ^0.8.0; - + -import {IBurnMintERC20} from "../../shared/token/ERC20/IBurnMintERC20.sol"; +import {IBurnMintERC20} from "../../../shared/token/ERC20/IBurnMintERC20.sol"; - + -import {TokenPool} from "./TokenPool.sol"; +import {UpgradeableTokenPool} from "./UpgradeableTokenPool.sol"; - + -abstract contract BurnMintTokenPoolAbstract is TokenPool { +abstract contract UpgradeableBurnMintTokenPoolAbstract is UpgradeableTokenPool { /// @notice Contains the specific burn call for a pool. diff --git a/contracts/src/v0.8/ccip/pools/GHO/diffs/UpgradeableBurnMintTokenPool_diff.md b/contracts/src/v0.8/ccip/pools/GHO/diffs/UpgradeableBurnMintTokenPool_diff.md index 066847e4f8..3302bad675 100644 --- a/contracts/src/v0.8/ccip/pools/GHO/diffs/UpgradeableBurnMintTokenPool_diff.md +++ b/contracts/src/v0.8/ccip/pools/GHO/diffs/UpgradeableBurnMintTokenPool_diff.md @@ -1,26 +1,24 @@ ```diff diff --git a/src/v0.8/ccip/pools/BurnMintTokenPool.sol b/src/v0.8/ccip/pools/GHO/UpgradeableBurnMintTokenPool.sol -index 9af0f22f4c..a46ff915e5 100644 +index 9af0f22f4c..a5cecc0430 100644 --- a/src/v0.8/ccip/pools/BurnMintTokenPool.sol +++ b/src/v0.8/ccip/pools/GHO/UpgradeableBurnMintTokenPool.sol @@ -1,28 +1,90 @@ // SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.19; +pragma solidity ^0.8.0; - + -import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol"; -import {IBurnMintERC20} from "../../shared/token/ERC20/IBurnMintERC20.sol"; +import {Initializable} from "solidity-utils/contracts/transparent-proxy/Initializable.sol"; - --import {TokenPool} from "./TokenPool.sol"; --import {BurnMintTokenPoolAbstract} from "./BurnMintTokenPoolAbstract.sol"; +import {ITypeAndVersion} from "../../../shared/interfaces/ITypeAndVersion.sol"; +import {IBurnMintERC20} from "../../../shared/token/ERC20/IBurnMintERC20.sol"; -+ + +-import {TokenPool} from "./TokenPool.sol"; +-import {BurnMintTokenPoolAbstract} from "./BurnMintTokenPoolAbstract.sol"; +import {UpgradeableTokenPool} from "./UpgradeableTokenPool.sol"; +import {UpgradeableBurnMintTokenPoolAbstract} from "./UpgradeableBurnMintTokenPoolAbstract.sol"; +import {RateLimiter} from "../../libraries/RateLimiter.sol"; -+ +import {IRouter} from "../../interfaces/IRouter.sol"; + +/// @title UpgradeableBurnMintTokenPool @@ -29,9 +27,12 @@ index 9af0f22f4c..a46ff915e5 100644 +/// @dev Contract adaptations: +/// - Implementation of Initializable to allow upgrades +/// - Move of allowlist and router definition to initialization stage ++/// - Inclusion of rate limit admin who may configure rate limits in addition to owner ++/// - Modifications from inherited contract (see contract for more details): ++/// - UpgradeableTokenPool: Modify `onlyOnRamp` & `onlyOffRamp` modifier to accept transactions from ProxyPool +contract UpgradeableBurnMintTokenPool is Initializable, UpgradeableBurnMintTokenPoolAbstract, ITypeAndVersion { + error Unauthorized(address caller); - + -/// @notice This pool mints and burns a 3rd-party token. -/// @dev Pool whitelisting mode is set in the constructor and cannot be modified later. -/// It either accepts any address as originalSender, or only accepts whitelisted originalSender. @@ -39,7 +40,7 @@ index 9af0f22f4c..a46ff915e5 100644 -/// If that is expected, please make sure the token's burner/minter roles are adjustable. -contract BurnMintTokenPool is BurnMintTokenPoolAbstract, ITypeAndVersion { string public constant override typeAndVersion = "BurnMintTokenPool 1.4.0"; - + + /// @notice The address of the rate limiter admin. + /// @dev Can be address(0) if none is configured. + address internal s_rateLimitAdmin; @@ -57,7 +58,7 @@ index 9af0f22f4c..a46ff915e5 100644 - ) TokenPool(token, allowlist, armProxy, router) {} + bool allowlistEnabled + ) UpgradeableTokenPool(IBurnMintERC20(token), armProxy, allowlistEnabled) {} - + - /// @inheritdoc BurnMintTokenPoolAbstract + /// @dev Initializer + /// @dev The address passed as `owner` must accept ownership after initialization. @@ -66,8 +67,7 @@ index 9af0f22f4c..a46ff915e5 100644 + /// @param allowlist A set of addresses allowed to trigger lockOrBurn as original senders + /// @param router The address of the router + function initialize(address owner, address[] memory allowlist, address router) public virtual initializer { -+ if (owner == address(0)) revert ZeroAddressNotAllowed(); -+ if (router == address(0)) revert ZeroAddressNotAllowed(); ++ if (owner == address(0) || router == address(0)) revert ZeroAddressNotAllowed(); + _transferOwnership(owner); + + s_router = IRouter(router); @@ -90,7 +90,7 @@ index 9af0f22f4c..a46ff915e5 100644 + return s_rateLimitAdmin; + } + -+ /// @notice Sets the rate limiter admin address. ++ /// @notice Sets the chain rate limiter config. + /// @dev Only callable by the owner or the rate limiter admin. NOTE: overwrites the normal + /// onlyAdmin check in the base implementation to also allow the rate limiter admin. + /// @param remoteChainSelector The remote chain selector for which the rate limits apply. diff --git a/contracts/src/v0.8/ccip/pools/GHO/diffs/UpgradeableLockReleaseTokenPool_diff.md b/contracts/src/v0.8/ccip/pools/GHO/diffs/UpgradeableLockReleaseTokenPool_diff.md index 1e738e3bdf..4275c54339 100644 --- a/contracts/src/v0.8/ccip/pools/GHO/diffs/UpgradeableLockReleaseTokenPool_diff.md +++ b/contracts/src/v0.8/ccip/pools/GHO/diffs/UpgradeableLockReleaseTokenPool_diff.md @@ -1,37 +1,35 @@ ```diff diff --git a/src/v0.8/ccip/pools/LockReleaseTokenPool.sol b/src/v0.8/ccip/pools/GHO/UpgradeableLockReleaseTokenPool.sol -index 1a17fa0398..9a30b1e977 100644 +index 1a17fa0398..4a63ab26ba 100644 --- a/src/v0.8/ccip/pools/LockReleaseTokenPool.sol +++ b/src/v0.8/ccip/pools/GHO/UpgradeableLockReleaseTokenPool.sol @@ -1,26 +1,39 @@ // SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.19; +pragma solidity ^0.8.0; - + -import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol"; -import {ILiquidityContainer} from "../../rebalancer/interfaces/ILiquidityContainer.sol"; +import {Initializable} from "solidity-utils/contracts/transparent-proxy/Initializable.sol"; - --import {TokenPool} from "./TokenPool.sol"; --import {RateLimiter} from "../libraries/RateLimiter.sol"; +import {ITypeAndVersion} from "../../../shared/interfaces/ITypeAndVersion.sol"; +import {ILiquidityContainer} from "../../../rebalancer/interfaces/ILiquidityContainer.sol"; - --import {IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; --import {SafeERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; + +-import {TokenPool} from "./TokenPool.sol"; +-import {RateLimiter} from "../libraries/RateLimiter.sol"; +import {UpgradeableTokenPool} from "./UpgradeableTokenPool.sol"; +import {RateLimiter} from "../../libraries/RateLimiter.sol"; - + +-import {IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +-import {SafeERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; ++import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; ++import {SafeERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; ++import {IRouter} from "../../interfaces/IRouter.sol"; + -/// @notice Token pool used for tokens on their native chain. This uses a lock and release mechanism. -/// Because of lock/unlock requiring liquidity, this pool contract also has function to add and remove -/// liquidity. This allows for proper bookkeeping for both user and liquidity provider balances. -/// @dev One token per LockReleaseTokenPool. -contract LockReleaseTokenPool is TokenPool, ILiquidityContainer, ITypeAndVersion { -+import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; -+import {SafeERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; -+ -+import {IRouter} from "../../interfaces/IRouter.sol"; -+ +/// @title UpgradeableLockReleaseTokenPool +/// @author Aave Labs +/// @notice Upgradeable version of Chainlink's CCIP LockReleaseTokenPool @@ -39,13 +37,15 @@ index 1a17fa0398..9a30b1e977 100644 +/// - Implementation of Initializable to allow upgrades +/// - Move of allowlist and router definition to initialization stage +/// - Addition of a bridge limit to regulate the maximum amount of tokens that can be transferred out (burned/locked) ++/// - Modifications from inherited contract (see contract for more details): ++/// - UpgradeableTokenPool: Modify `onlyOnRamp` & `onlyOffRamp` modifier to accept transactions from ProxyPool +contract UpgradeableLockReleaseTokenPool is Initializable, UpgradeableTokenPool, ILiquidityContainer, ITypeAndVersion { using SafeERC20 for IERC20; - + error InsufficientLiquidity(); error LiquidityNotAccepted(); error Unauthorized(address caller); - + + error BridgeLimitExceeded(uint256 bridgeLimit); + error NotEnoughBridgedAmount(); + @@ -53,12 +53,12 @@ index 1a17fa0398..9a30b1e977 100644 + event BridgeLimitAdminUpdated(address indexed oldAdmin, address indexed newAdmin); + string public constant override typeAndVersion = "LockReleaseTokenPool 1.4.0"; - + /// @dev The unique lock release pool flag to signal through EIP 165. -@@ -37,16 +50,55 @@ contract LockReleaseTokenPool is TokenPool, ILiquidityContainer, ITypeAndVersion +@@ -37,16 +50,54 @@ contract LockReleaseTokenPool is TokenPool, ILiquidityContainer, ITypeAndVersion /// @dev Can be address(0) if none is configured. address internal s_rateLimitAdmin; - + + /// @notice Maximum amount of tokens that can be bridged to other chains + uint256 private s_bridgeLimit; + /// @notice Amount of tokens bridged (transferred out) @@ -86,7 +86,7 @@ index 1a17fa0398..9a30b1e977 100644 + ) UpgradeableTokenPool(IERC20(token), armProxy, allowlistEnabled) { i_acceptLiquidity = acceptLiquidity; } - + + /// @dev Initializer + /// @dev The address passed as `owner` must accept ownership after initialization. + /// @dev The `allowlist` is only effective if pool is set to access-controlled mode @@ -100,8 +100,7 @@ index 1a17fa0398..9a30b1e977 100644 + address router, + uint256 bridgeLimit + ) public virtual initializer { -+ if (owner == address(0)) revert ZeroAddressNotAllowed(); -+ if (router == address(0)) revert ZeroAddressNotAllowed(); ++ if (owner == address(0) || router == address(0)) revert ZeroAddressNotAllowed(); + _transferOwnership(owner); + + s_router = IRouter(router); @@ -116,7 +115,7 @@ index 1a17fa0398..9a30b1e977 100644 /// @notice Locks the token in the pool /// @param amount Amount to lock /// @dev The whenHealthy check is important to ensure that even if a ramp is compromised -@@ -66,6 +118,9 @@ contract LockReleaseTokenPool is TokenPool, ILiquidityContainer, ITypeAndVersion +@@ -66,6 +117,9 @@ contract LockReleaseTokenPool is TokenPool, ILiquidityContainer, ITypeAndVersion whenHealthy returns (bytes memory) { @@ -126,7 +125,7 @@ index 1a17fa0398..9a30b1e977 100644 _consumeOutboundRateLimit(remoteChainSelector, amount); emit Locked(msg.sender, amount); return ""; -@@ -83,6 +138,11 @@ contract LockReleaseTokenPool is TokenPool, ILiquidityContainer, ITypeAndVersion +@@ -83,6 +137,11 @@ contract LockReleaseTokenPool is TokenPool, ILiquidityContainer, ITypeAndVersion uint64 remoteChainSelector, bytes memory ) external virtual override onlyOffRamp(remoteChainSelector) whenHealthy { @@ -138,10 +137,10 @@ index 1a17fa0398..9a30b1e977 100644 _consumeInboundRateLimit(remoteChainSelector, amount); getToken().safeTransfer(receiver, amount); emit Released(msg.sender, receiver, amount); -@@ -120,11 +180,48 @@ contract LockReleaseTokenPool is TokenPool, ILiquidityContainer, ITypeAndVersion +@@ -120,11 +179,48 @@ contract LockReleaseTokenPool is TokenPool, ILiquidityContainer, ITypeAndVersion s_rateLimitAdmin = rateLimitAdmin; } - + + /// @notice Sets the bridge limit, the maximum amount of tokens that can be bridged out + /// @dev Only callable by the owner or the bridge limit admin. + /// @dev Bridge limit changes should be carefully managed, specially when reducing below the current bridged amount @@ -178,7 +177,7 @@ index 1a17fa0398..9a30b1e977 100644 function getRateLimitAdmin() external view returns (address) { return s_rateLimitAdmin; } - + + /// @notice Gets the bridge limiter admin address. + function getBridgeLimitAdmin() external view returns (address) { + return s_bridgeLimitAdmin; @@ -187,4 +186,13 @@ index 1a17fa0398..9a30b1e977 100644 /// @notice Checks if the pool can accept liquidity. /// @return true if the pool can accept liquidity, false otherwise. function canAcceptLiquidity() external view returns (bool) { +@@ -151,7 +247,7 @@ contract LockReleaseTokenPool is TokenPool, ILiquidityContainer, ITypeAndVersion + emit LiquidityRemoved(msg.sender, amount); + } + +- /// @notice Sets the rate limiter admin address. ++ /// @notice Sets the chain rate limiter config. + /// @dev Only callable by the owner or the rate limiter admin. NOTE: overwrites the normal + /// onlyAdmin check in the base implementation to also allow the rate limiter admin. + /// @param remoteChainSelector The remote chain selector for which the rate limits apply. ``` diff --git a/contracts/src/v0.8/ccip/pools/GHO/diffs/UpgradeableTokenPool_diff.md b/contracts/src/v0.8/ccip/pools/GHO/diffs/UpgradeableTokenPool_diff.md index fcdc197580..c5bb382c46 100644 --- a/contracts/src/v0.8/ccip/pools/GHO/diffs/UpgradeableTokenPool_diff.md +++ b/contracts/src/v0.8/ccip/pools/GHO/diffs/UpgradeableTokenPool_diff.md @@ -1,41 +1,60 @@ ```diff diff --git a/src/v0.8/ccip/pools/TokenPool.sol b/src/v0.8/ccip/pools/GHO/UpgradeableTokenPool.sol -index b3571bb449..ee359ac1f8 100644 +index b3571bb449..9c294974bc 100644 --- a/src/v0.8/ccip/pools/TokenPool.sol +++ b/src/v0.8/ccip/pools/GHO/UpgradeableTokenPool.sol -@@ -1,21 +1,21 @@ +@@ -1,21 +1,24 @@ // SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.19; +pragma solidity ^0.8.0; - + -import {IPool} from "../interfaces/pools/IPool.sol"; -import {IARM} from "../interfaces/IARM.sol"; -import {IRouter} from "../interfaces/IRouter.sol"; +import {IPool} from "../../interfaces/pools/IPool.sol"; +import {IARM} from "../../interfaces/IARM.sol"; +import {IRouter} from "../../interfaces/IRouter.sol"; - + -import {OwnerIsCreator} from "../../shared/access/OwnerIsCreator.sol"; -import {RateLimiter} from "../libraries/RateLimiter.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"; +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"; - - /// @notice Base abstract class with common functions for all token pools. - /// A token pool serves as isolated place for holding tokens and token specific logic - /// that may execute as tokens move across the bridge. + +-/// @notice Base abstract class with common functions for all token pools. +-/// A token pool serves as isolated place for holding tokens and token specific logic +-/// that may execute as tokens move across the bridge. -abstract contract TokenPool is IPool, OwnerIsCreator, IERC165 { ++/// @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` & `onlyOffRamp` modifier to accept transactions from ProxyPool +abstract contract UpgradeableTokenPool is IPool, OwnerIsCreator, IERC165 { using EnumerableSet for EnumerableSet.AddressSet; using EnumerableSet for EnumerableSet.UintSet; using RateLimiter for RateLimiter.TokenBucket; -@@ -74,23 +74,17 @@ abstract contract TokenPool is IPool, OwnerIsCreator, IERC165 { +@@ -55,6 +58,12 @@ abstract contract TokenPool is IPool, OwnerIsCreator, IERC165 { + 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 +@@ -74,23 +83,17 @@ abstract contract TokenPool is IPool, OwnerIsCreator, IERC165 { EnumerableSet.UintSet internal s_remoteChainSelectors; /// @dev Outbound rate limits. Corresponds to the inbound rate limit for the pool /// on the remote chain. @@ -46,7 +65,7 @@ index b3571bb449..ee359ac1f8 100644 /// degrees and prefer different limits) - mapping(uint64 remoteChainSelector => RateLimiter.TokenBucket) internal s_inboundRateLimits; + mapping(uint64 => RateLimiter.TokenBucket) internal s_inboundRateLimits; - + - constructor(IERC20 token, address[] memory allowlist, address armProxy, address router) { - if (address(token) == address(0) || router == address(0)) revert ZeroAddressNotAllowed(); + constructor(IERC20 token, address armProxy, bool allowlistEnabled) { @@ -62,6 +81,50 @@ index b3571bb449..ee359ac1f8 100644 - } + i_allowlistEnabled = allowlistEnabled; } - + /// @notice Get ARM proxy address +@@ -256,7 +259,9 @@ abstract contract TokenPool is IPool, OwnerIsCreator, IERC165 { + /// is a permissioned onRamp for the given chain on the Router. + modifier onlyOnRamp(uint64 remoteChainSelector) { + if (!isSupportedChain(remoteChainSelector)) revert ChainNotAllowed(remoteChainSelector); +- if (!(msg.sender == s_router.getOnRamp(remoteChainSelector))) revert CallerIsNotARampOnRouter(msg.sender); ++ if (!(msg.sender == getProxyPool() || msg.sender == s_router.getOnRamp(remoteChainSelector))) { ++ revert CallerIsNotARampOnRouter(msg.sender); ++ } + _; + } + +@@ -264,7 +269,9 @@ abstract contract TokenPool is IPool, OwnerIsCreator, IERC165 { + /// is a permissioned offRamp for the given chain on the Router. + modifier onlyOffRamp(uint64 remoteChainSelector) { + if (!isSupportedChain(remoteChainSelector)) revert ChainNotAllowed(remoteChainSelector); +- if (!s_router.isOffRamp(remoteChainSelector, msg.sender)) revert CallerIsNotARampOnRouter(msg.sender); ++ if (!(msg.sender == getProxyPool() || s_router.isOffRamp(remoteChainSelector, msg.sender))) { ++ revert CallerIsNotARampOnRouter(msg.sender); ++ } + _; + } + +@@ -323,4 +330,21 @@ abstract contract TokenPool is IPool, OwnerIsCreator, IERC165 { + if (IARM(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/helpers/interfaces/IEVM2EVMOffRamp.sol b/contracts/src/v0.8/ccip/test/helpers/interfaces/IEVM2EVMOffRamp.sol new file mode 100644 index 0000000000..6b370fbcee --- /dev/null +++ b/contracts/src/v0.8/ccip/test/helpers/interfaces/IEVM2EVMOffRamp.sol @@ -0,0 +1,17 @@ +pragma solidity ^0.8.0; + +import {ITypeAndVersion} from "../../../../shared/interfaces/ITypeAndVersion.sol"; +import {IAny2EVMOffRamp} from "../../../interfaces/IAny2EVMOffRamp.sol"; +import {Internal} from "../../../libraries/Internal.sol"; + +interface IEVM2EVMOffRamp_1_2 is IAny2EVMOffRamp, ITypeAndVersion { + function executeSingleMessage(Internal.EVM2EVMMessage memory message, bytes[] memory offchainTokenData) external; +} + +interface IEVM2EVMOffRamp_1_5 is IAny2EVMOffRamp, ITypeAndVersion { + function executeSingleMessage( + Internal.EVM2EVMMessage calldata message, + bytes[] calldata offchainTokenData, + uint32[] memory tokenGasOverrides + ) external; +} diff --git a/contracts/src/v0.8/ccip/test/helpers/interfaces/IEVM2EVMOnRamp.sol b/contracts/src/v0.8/ccip/test/helpers/interfaces/IEVM2EVMOnRamp.sol new file mode 100644 index 0000000000..4fa742dae4 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/helpers/interfaces/IEVM2EVMOnRamp.sol @@ -0,0 +1,46 @@ +pragma solidity ^0.8.0; + +import {ITypeAndVersion} from "../../../../shared/interfaces/ITypeAndVersion.sol"; +import {IEVM2AnyOnRamp} from "../../../interfaces/IEVM2AnyOnRamp.sol"; + +interface IEVM2EVMOnRamp_1_2 is IEVM2AnyOnRamp, ITypeAndVersion {} + +interface IEVM2EVMOnRamp_1_5 is IEVM2AnyOnRamp, ITypeAndVersion { + struct TokenTransferFeeConfig { + uint32 minFeeUSDCents; // ──────────╮ Minimum fee to charge per token transfer, multiples of 0.01 USD + uint32 maxFeeUSDCents; // │ Maximum fee to charge per token transfer, multiples of 0.01 USD + uint16 deciBps; // │ Basis points charged on token transfers, multiples of 0.1bps, or 1e-5 + uint32 destGasOverhead; // │ Gas charged to execute the token transfer on the destination chain + // │ Extra data availability bytes that are returned from the source pool and sent + uint32 destBytesOverhead; // │ to the destination pool. Must be >= Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES + bool aggregateRateLimitEnabled; // │ Whether this transfer token is to be included in Aggregate Rate Limiting + bool isEnabled; // ─────────────────╯ Whether this token has custom transfer fees + } + + struct DynamicConfig { + address router; // ──────────────────────────╮ Router address + uint16 maxNumberOfTokensPerMsg; // │ Maximum number of distinct ERC20 token transferred per message + uint32 destGasOverhead; // │ Gas charged on top of the gasLimit to cover destination chain costs + uint16 destGasPerPayloadByte; // │ Destination chain gas charged for passing each byte of `data` payload to receiver + uint32 destDataAvailabilityOverheadGas; // ──╯ Extra data availability gas charged on top of the message, e.g. for OCR + uint16 destGasPerDataAvailabilityByte; // ───╮ Amount of gas to charge per byte of message data that needs availability + uint16 destDataAvailabilityMultiplierBps; // │ Multiplier for data availability gas, multiples of bps, or 0.0001 + address priceRegistry; // │ Price registry address + uint32 maxDataBytes; // │ Maximum payload data size in bytes + uint32 maxPerMsgGasLimit; // ────────────────╯ Maximum gas limit for messages targeting EVMs + // │ + // The following three properties are defaults, they can be overridden by setting the TokenTransferFeeConfig for a token + uint16 defaultTokenFeeUSDCents; // ──────────╮ Default token fee charged per token transfer + uint32 defaultTokenDestGasOverhead; // │ Default gas charged to execute the token transfer on the destination chain + bool enforceOutOfOrder; // ──────────────────╯ Whether to enforce the allowOutOfOrderExecution extraArg value to be true. + } + + /// @notice Gets the transfer fee config for a given token. + function getTokenTransferFeeConfig( + address token + ) external view returns (TokenTransferFeeConfig memory tokenTransferFeeConfig); + + /// @notice Returns the dynamic onRamp config. + /// @return dynamicConfig the configuration. + function getDynamicConfig() external view returns (DynamicConfig memory dynamicConfig); +} diff --git a/contracts/src/v0.8/ccip/test/pools/GHO/GhoTokenPoolRemote.t.sol b/contracts/src/v0.8/ccip/test/pools/GHO/GhoTokenPoolRemote.t.sol index 773528c715..deba3b745b 100644 --- a/contracts/src/v0.8/ccip/test/pools/GHO/GhoTokenPoolRemote.t.sol +++ b/contracts/src/v0.8/ccip/test/pools/GHO/GhoTokenPoolRemote.t.sol @@ -376,3 +376,25 @@ contract GhoTokenPoolRemote_setRateLimitAdmin is GhoTokenPoolRemoteSetup { s_pool.setRateLimitAdmin(STRANGER); } } + +contract GhoTokenPoolRemote_proxyPool is GhoTokenPoolRemoteSetup { + function testSetProxyPoolAdminReverts() public { + vm.startPrank(STRANGER); + vm.expectRevert("Only callable by owner"); + s_pool.setProxyPool(makeAddr("proxyPool")); + } + + function testSetProxyPoolZeroAddressReverts() public { + vm.startPrank(AAVE_DAO); + vm.expectRevert(UpgradeableTokenPool.ZeroAddressNotAllowed.selector); + s_pool.setProxyPool(address(0)); + } + + function testSetProxyPoolSuccess(address proxyPool) public { + vm.assume(proxyPool != address(0)); + changePrank(AAVE_DAO); + s_pool.setProxyPool(proxyPool); + + assertEq(s_pool.getProxyPool(), proxyPool); + } +} diff --git a/contracts/src/v0.8/ccip/test/pools/GHO/fork/GhoTokenPoolMigrate1_4To1_5/ForkBase.t.sol b/contracts/src/v0.8/ccip/test/pools/GHO/fork/GhoTokenPoolMigrate1_4To1_5/ForkBase.t.sol new file mode 100644 index 0000000000..a3c97a0b64 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/pools/GHO/fork/GhoTokenPoolMigrate1_4To1_5/ForkBase.t.sol @@ -0,0 +1,450 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.19; + +import {Test} from "forge-std/Test.sol"; +import {IERC20} from "../../../../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {ITypeAndVersion} from "../../../../../../shared/interfaces/ITypeAndVersion.sol"; +import {IRouterClient} from "../../../../../interfaces/IRouterClient.sol"; +import {IEVM2AnyOnRamp} from "../../../../../interfaces/IEVM2AnyOnRamp.sol"; +import {IRouter as IRouterBase} from "../../../../../interfaces/IRouter.sol"; +import {Client} from "../../../../../libraries/Client.sol"; +import {Internal} from "../../../../../libraries/Internal.sol"; +import {IEVM2EVMOffRamp_1_2, IEVM2EVMOffRamp_1_5} from "../../../../helpers/interfaces/IEVM2EVMOffRamp.sol"; +import {IEVM2EVMOnRamp_1_2, IEVM2EVMOnRamp_1_5} from "../../../../helpers/interfaces/IEVM2EVMOnRamp.sol"; +import {UpgradeableLockReleaseTokenPool_Sepolia} from "./LegacyTestnetTokenPools/UpgradeableLockReleaseTokenPool_Sepolia.sol"; +import {UpgradeableBurnMintTokenPool_ArbSepolia} from "./LegacyTestnetTokenPools/UpgradeableBurnMintTokenPool_ArbSepolia.sol"; + +interface IRouter is IRouterClient, IRouterBase { + struct OffRamp { + uint64 sourceChainSelector; + address offRamp; + } + function getWrappedNative() external view returns (address); + function isOffRamp(uint64, address) external view returns (bool); + function getOffRamps() external view returns (OffRamp[] memory); +} + +struct SourceTokenData { + bytes sourcePoolAddress; + bytes destTokenAddress; + bytes extraData; + uint32 destGasAmount; +} + +contract ForkBase is Test { + error CallerIsNotARampOnRouter(address caller); + + event CCIPSendRequested(Internal.EVM2EVMMessage message); + 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); + + struct L1 { + UpgradeableLockReleaseTokenPool_Sepolia tokenPool; + IRouter router; + IERC20 token; + IEVM2EVMOnRamp_1_2 EVM2EVMOnRamp1_2; + IEVM2EVMOnRamp_1_5 EVM2EVMOnRamp1_5; + IEVM2EVMOffRamp_1_2 EVM2EVMOffRamp1_2; + IEVM2EVMOffRamp_1_5 EVM2EVMOffRamp1_5; + address proxyPool; + uint64 chainSelector; + bytes32 metadataHash; + uint256 forkId; + } + struct L2 { + UpgradeableBurnMintTokenPool_ArbSepolia tokenPool; + IRouter router; + IERC20 token; + IEVM2EVMOnRamp_1_2 EVM2EVMOnRamp1_2; + IEVM2EVMOnRamp_1_5 EVM2EVMOnRamp1_5; + IEVM2EVMOffRamp_1_2 EVM2EVMOffRamp1_2; + IEVM2EVMOffRamp_1_5 EVM2EVMOffRamp1_5; + address proxyPool; + uint64 chainSelector; + bytes32 metadataHash; + uint256 forkId; + } + + L1 internal l1; + L2 internal l2; + + address internal alice = makeAddr("alice"); + + uint256 internal constant BLOCK_AFTER_MIGRATION_L1 = 6884195; + uint256 internal constant BLOCK_AFTER_MIGRATION_L2 = 89058935; + + function setUp() public virtual { + l1.forkId = vm.createFork(vm.rpcUrl("sepolia"), BLOCK_AFTER_MIGRATION_L1); + l2.forkId = vm.createFork(vm.rpcUrl("arb_sepolia"), BLOCK_AFTER_MIGRATION_L2); + + vm.selectFork(l1.forkId); + l1.tokenPool = UpgradeableLockReleaseTokenPool_Sepolia(0x7768248E1Ff75612c18324bad06bb393c1206980); + l1.proxyPool = 0x14A3298f667CCB3ad4B77878d80b353f6A10F183; + l1.router = IRouter(l1.tokenPool.getRouter()); + l2.chainSelector = l1.tokenPool.getSupportedChains()[0]; + l1.token = l1.tokenPool.getToken(); + l1.EVM2EVMOnRamp1_2 = IEVM2EVMOnRamp_1_2(0xe4Dd3B16E09c016402585a8aDFdB4A18f772a07e); // legacy on ramp + l1.EVM2EVMOnRamp1_5 = IEVM2EVMOnRamp_1_5(l1.router.getOnRamp(l2.chainSelector)); + l1.EVM2EVMOffRamp1_2 = IEVM2EVMOffRamp_1_2(0xF18896AB20a09A29e64fdEbA99FDb8EC328f43b1); + l1.EVM2EVMOffRamp1_5 = IEVM2EVMOffRamp_1_5(0xD2f5edfD4561d6E7599F6c6888Bd353cAFd0c55E); + vm.prank(alice); + l1.token.approve(address(l1.router), type(uint256).max); + deal(address(l1.token), alice, 1000e18); + deal(alice, 1000e18); + + vm.selectFork(l2.forkId); + l2.tokenPool = UpgradeableBurnMintTokenPool_ArbSepolia(0x3eC2b6F818B72442fc36561e9F930DD2b60957D2); + l2.proxyPool = 0x2BDbDCC0957E8d9f5Eb1Fe8E1Bc0d7F57AD1C897; + l2.router = IRouter(l2.tokenPool.getRouter()); + l1.chainSelector = l2.tokenPool.getSupportedChains()[0]; + l2.token = l2.tokenPool.getToken(); + l2.EVM2EVMOnRamp1_2 = IEVM2EVMOnRamp_1_2(0x4205E1Ca0202A248A5D42F5975A8FE56F3E302e9); // legacy on ramp + l2.EVM2EVMOnRamp1_5 = IEVM2EVMOnRamp_1_5(l2.router.getOnRamp(l1.chainSelector)); + l2.EVM2EVMOffRamp1_2 = IEVM2EVMOffRamp_1_2(0x1c71f141b4630EBE52d6aF4894812960abE207eB); + l2.EVM2EVMOffRamp1_5 = IEVM2EVMOffRamp_1_5(0xBed6e9131916d724418C8a6FE810F727302a5c00); + vm.prank(alice); + l2.token.approve(address(l2.router), type(uint256).max); + deal(address(l2.token), alice, 1000e18); + deal(alice, 1000e18); + + l1.metadataHash = _generateMetadataHash(l1.chainSelector, l1.EVM2EVMOnRamp1_5); + l2.metadataHash = _generateMetadataHash(l2.chainSelector, l2.EVM2EVMOnRamp1_5); + + vm.selectFork(l1.forkId); + assertEq(l1.chainSelector, 16015286601757825753); + assertEq(address(l1.token), 0xc4bF5CbDaBE595361438F8c6a187bDc330539c60); + assertEq(l1.token.balanceOf(alice), 1000e18); + assertEq(ITypeAndVersion(address(l1.router)).typeAndVersion(), "Router 1.2.0"); + assertEq(ITypeAndVersion(l1.proxyPool).typeAndVersion(), "LockReleaseTokenPoolAndProxy 1.5.0"); + assertEq(l1.EVM2EVMOnRamp1_2.typeAndVersion(), "EVM2EVMOnRamp 1.2.0"); + assertEq(l1.EVM2EVMOnRamp1_5.typeAndVersion(), "EVM2EVMOnRamp 1.5.0"); + assertEq(l1.EVM2EVMOffRamp1_2.typeAndVersion(), "EVM2EVMOffRamp 1.2.0"); + assertEq(l1.EVM2EVMOffRamp1_5.typeAndVersion(), "EVM2EVMOffRamp 1.5.0"); + assertTrue(l1.router.isOffRamp(l2.chainSelector, address(l1.EVM2EVMOffRamp1_2))); + assertTrue(l1.router.isOffRamp(l2.chainSelector, address(l1.EVM2EVMOffRamp1_5))); + + vm.selectFork(l2.forkId); + assertEq(l2.chainSelector, 3478487238524512106); + assertEq(address(l2.token), 0xb13Cfa6f8B2Eed2C37fB00fF0c1A59807C585810); + assertEq(l2.token.balanceOf(alice), 1000e18); + assertEq(ITypeAndVersion(address(l2.router)).typeAndVersion(), "Router 1.2.0"); + assertEq(ITypeAndVersion(l2.proxyPool).typeAndVersion(), "BurnMintTokenPoolAndProxy 1.5.0"); + assertEq(l2.EVM2EVMOnRamp1_2.typeAndVersion(), "EVM2EVMOnRamp 1.2.0"); + assertEq(l2.EVM2EVMOnRamp1_5.typeAndVersion(), "EVM2EVMOnRamp 1.5.0"); + assertEq(l2.EVM2EVMOffRamp1_2.typeAndVersion(), "EVM2EVMOffRamp 1.2.0"); + assertEq(l2.EVM2EVMOffRamp1_5.typeAndVersion(), "EVM2EVMOffRamp 1.5.0"); + assertTrue(l2.router.isOffRamp(l1.chainSelector, address(l2.EVM2EVMOffRamp1_2))); + assertTrue(l2.router.isOffRamp(l1.chainSelector, address(l2.EVM2EVMOffRamp1_5))); + + _label(); + } + + function _generateMessage( + address receiver, + uint256 tokenAmountsLength + ) internal pure returns (Client.EVM2AnyMessage memory) { + return + Client.EVM2AnyMessage({ + receiver: abi.encode(receiver), + data: "", + tokenAmounts: new Client.EVMTokenAmount[](tokenAmountsLength), + feeToken: address(0), + extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: 0})) + }); + } + + function _messageToEvent( + Client.EVM2AnyMessage memory message, + address onRamp, + uint256 feeTokenAmount, + address originalSender, + bool isL1 + ) public view returns (Internal.EVM2EVMMessage memory) { + // Slicing is only available for calldata. So we have to build a new bytes array. + bytes memory args = new bytes(message.extraArgs.length - 4); + for (uint256 i = 4; i < message.extraArgs.length; ++i) { + args[i - 4] = message.extraArgs[i]; + } + Client.EVMExtraArgsV1 memory extraArgs = abi.decode(args, (Client.EVMExtraArgsV1)); + Internal.EVM2EVMMessage memory messageEvent = Internal.EVM2EVMMessage({ + sequenceNumber: IEVM2AnyOnRamp(onRamp).getExpectedNextSequenceNumber(), + feeTokenAmount: feeTokenAmount, + sender: originalSender, + nonce: IEVM2AnyOnRamp(onRamp).getSenderNonce(originalSender) + 1, + gasLimit: extraArgs.gasLimit, + strict: false, + sourceChainSelector: isL1 ? l1.chainSelector : l2.chainSelector, + receiver: abi.decode(message.receiver, (address)), + data: message.data, + tokenAmounts: message.tokenAmounts, + sourceTokenData: new bytes[](message.tokenAmounts.length), + feeToken: isL1 ? l1.router.getWrappedNative() : l2.router.getWrappedNative(), + messageId: "" + }); + + if (_hasMigrated(isL1, onRamp)) { + for (uint256 i; i < message.tokenAmounts.length; ++i) { + // change introduced in 1.5 upgrade + messageEvent.sourceTokenData[i] = abi.encode( + SourceTokenData({ + sourcePoolAddress: abi.encode(isL1 ? l1.proxyPool : l2.proxyPool), + destTokenAddress: abi.encode(address(isL1 ? l2.token : l1.token)), + extraData: "", + destGasAmount: _getDestGasAmount(onRamp, message.tokenAmounts[i].token) + }) + ); + } + } + + messageEvent.messageId = Internal._hash(messageEvent, isL1 ? l1.metadataHash : l2.metadataHash); + return messageEvent; + } + + function _getDestGasAmount(address onRamp, address token) internal view returns (uint32) { + IEVM2EVMOnRamp_1_5.TokenTransferFeeConfig memory config = IEVM2EVMOnRamp_1_5(onRamp).getTokenTransferFeeConfig( + token + ); + return + config.isEnabled + ? config.destGasOverhead + : IEVM2EVMOnRamp_1_5(onRamp).getDynamicConfig().defaultTokenDestGasOverhead; + } + + function _hasMigrated(bool isL1, address onRamp) internal view returns (bool) { + return isL1 ? onRamp == address(l1.EVM2EVMOnRamp1_5) : onRamp == address(l2.EVM2EVMOnRamp1_5); + } + + function _generateMetadataHash(uint64 sourceChainSelector, IEVM2AnyOnRamp onRamp) internal view returns (bytes32) { + uint64 destChainSelector = sourceChainSelector == l1.chainSelector ? l2.chainSelector : l1.chainSelector; + return + keccak256(abi.encode(Internal.EVM_2_EVM_MESSAGE_HASH, sourceChainSelector, destChainSelector, address(onRamp))); + } + + function _label() internal { + vm.label(address(l1.tokenPool), "l1.tokenPool"); + vm.label(address(l1.token), "l1.token"); + vm.label(address(l1.router), "l1.router"); + vm.label(address(l1.proxyPool), "l1.proxyPool"); + vm.label(address(l1.EVM2EVMOnRamp1_2), "l1.EVM2EVMOnRamp1_2"); + vm.label(address(l1.EVM2EVMOnRamp1_5), "l1.EVM2EVMOnRamp1_5"); + vm.label(address(l1.EVM2EVMOffRamp1_2), "l1.EVM2EVMOffRamp1_2"); + vm.label(address(l1.EVM2EVMOffRamp1_5), "l1.EVM2EVMOffRamp1_5"); + + vm.label(address(l2.tokenPool), "l2.tokenPool"); + vm.label(address(l2.token), "l2.token"); + vm.label(address(l2.router), "l2.router"); + vm.label(address(l2.proxyPool), "l2.proxyPool"); + vm.label(address(l2.EVM2EVMOnRamp1_2), "l2.EVM2EVMOnRamp1_2"); + vm.label(address(l2.EVM2EVMOnRamp1_5), "l2.EVM2EVMOnRamp1_5"); + vm.label(address(l2.EVM2EVMOffRamp1_2), "l2.EVM2EVMOffRamp1_2"); + vm.label(address(l2.EVM2EVMOffRamp1_5), "l2.EVM2EVMOffRamp1_5"); + } +} + +contract ForkPoolAfterMigration is ForkBase { + function setUp() public override { + super.setUp(); + } + + /// @dev Tests current version of token pools do not work with legacy on-ramps post 1.5 CCIP Migration + /// Only lockOrBurn is incompatible post migration since the new proxyPool becomes a 'wrapped' router + /// for the existing token pool, releaseOrMint is still compatible with legacy on-ramps + /// see more: https://github.com/smartcontractkit/ccip/blob/11c275959902783a3c4eaddbfaa5ce5f8707e01f/contracts/src/v0.8/ccip/test/legacy/TokenPoolAndProxy.t.sol#L130-L192 + function testSendViaLegacyRouterReverts() public { + uint256 amount = 10e18; + // generate lockOrBurn message for lockRelease token pool on L1 + Client.EVM2AnyMessage memory message = _generateMessage(alice, 1); + message.tokenAmounts[0] = Client.EVMTokenAmount({token: address(l1.token), amount: amount}); + + vm.selectFork(l1.forkId); + uint256 feeTokenAmount = l1.router.getFee(l2.chainSelector, message); + + // validate send reverts with onRamp caller as proxyPool + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector(CallerIsNotARampOnRouter.selector, l1.proxyPool)); + l1.router.ccipSend{value: feeTokenAmount}(l2.chainSelector, message); + + vm.selectFork(l2.forkId); + // modify generated lockOrBurn message for burnMint tokenPool on L2 + message.tokenAmounts[0].token = address(l2.token); + feeTokenAmount = l2.router.getFee(l1.chainSelector, message); + + // validate send reverts with onRamp caller as proxyPool + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector(CallerIsNotARampOnRouter.selector, l2.proxyPool)); + l2.router.ccipSend{value: feeTokenAmount}(l1.chainSelector, message); + } +} + +contract ForkPoolBeforeMigration is ForkBase { + uint256 internal constant BLOCK_BEFORE_MIGRATION_L1 = 6673089; + uint256 internal constant BLOCK_BEFORE_MIGRATION_L2 = 79570677; + + function setUp() public override { + l1.forkId = vm.createFork(vm.rpcUrl("sepolia"), BLOCK_BEFORE_MIGRATION_L1); + l2.forkId = vm.createFork(vm.rpcUrl("arb_sepolia"), BLOCK_BEFORE_MIGRATION_L2); + + vm.selectFork(l1.forkId); + l1.tokenPool = UpgradeableLockReleaseTokenPool_Sepolia(0x7768248E1Ff75612c18324bad06bb393c1206980); + l1.router = IRouter(l1.tokenPool.getRouter()); + l2.chainSelector = l1.tokenPool.getSupportedChains()[0]; + l1.token = l1.tokenPool.getToken(); + l1.EVM2EVMOnRamp1_2 = IEVM2EVMOnRamp_1_2(l1.router.getOnRamp(l2.chainSelector)); + l1.EVM2EVMOffRamp1_2 = IEVM2EVMOffRamp_1_2(0xdb92e73d1D630B5B7aC96840c4df0c591c7Ad23E); + vm.prank(alice); + l1.token.approve(address(l1.router), type(uint256).max); + deal(address(l1.token), alice, 1000e18); + deal(alice, 1000e18); + + vm.selectFork(l2.forkId); + l2.tokenPool = UpgradeableBurnMintTokenPool_ArbSepolia(0x3eC2b6F818B72442fc36561e9F930DD2b60957D2); + l2.router = IRouter(l2.tokenPool.getRouter()); + l1.chainSelector = l2.tokenPool.getSupportedChains()[0]; + l2.token = l2.tokenPool.getToken(); + l2.EVM2EVMOnRamp1_2 = IEVM2EVMOnRamp_1_2(l2.router.getOnRamp(l1.chainSelector)); + l2.EVM2EVMOffRamp1_2 = IEVM2EVMOffRamp_1_2(0xFf5e1c597c5DFfC896Ab8c7b9d876D513518c4b7); + vm.prank(alice); + l2.token.approve(address(l2.router), type(uint256).max); + deal(address(l2.token), alice, 1000e18); + deal(alice, 1000e18); + + vm.selectFork(l1.forkId); + assertEq(l1.chainSelector, 16015286601757825753); + assertEq(address(l1.token), 0xc4bF5CbDaBE595361438F8c6a187bDc330539c60); + assertEq(ITypeAndVersion(address(l1.router)).typeAndVersion(), "Router 1.2.0"); + assertEq(ITypeAndVersion(address(l1.EVM2EVMOnRamp1_2)).typeAndVersion(), "EVM2EVMOnRamp 1.2.0"); + assertEq(l1.EVM2EVMOffRamp1_2.typeAndVersion(), "EVM2EVMOffRamp 1.2.0"); + assertTrue(l1.router.isOffRamp(l2.chainSelector, address(l1.EVM2EVMOffRamp1_2))); + // assert only one off ramp is set + IRouter.OffRamp[] memory offRamps = l1.router.getOffRamps(); + for (uint256 i; i < offRamps.length; ++i) { + if (offRamps[i].sourceChainSelector == l2.chainSelector) { + assertEq(address(l1.EVM2EVMOffRamp1_2), offRamps[i].offRamp); + } + } + + vm.selectFork(l2.forkId); + assertEq(l2.chainSelector, 3478487238524512106); + assertEq(address(l2.token), 0xb13Cfa6f8B2Eed2C37fB00fF0c1A59807C585810); + assertEq(ITypeAndVersion(address(l2.router)).typeAndVersion(), "Router 1.2.0"); + assertEq(ITypeAndVersion(address(l2.EVM2EVMOnRamp1_2)).typeAndVersion(), "EVM2EVMOnRamp 1.2.0"); + assertEq(l2.EVM2EVMOffRamp1_2.typeAndVersion(), "EVM2EVMOffRamp 1.2.0"); + assertTrue(l2.router.isOffRamp(l1.chainSelector, address(l2.EVM2EVMOffRamp1_2))); + // assert only one off ramp is set + offRamps = l2.router.getOffRamps(); + for (uint256 i; i < offRamps.length; ++i) { + if (offRamps[i].sourceChainSelector == l1.chainSelector) { + assertEq(address(l2.EVM2EVMOffRamp1_2), offRamps[i].offRamp); + } + } + } + + function testSendViaRouter() public { + uint256 amount = 10e18; + Client.EVM2AnyMessage memory message = _generateMessage(alice, 1); + message.tokenAmounts[0] = Client.EVMTokenAmount({token: address(l1.token), amount: amount}); + + { + vm.selectFork(l1.forkId); + uint256 feeTokenAmount = l1.router.getFee(l2.chainSelector, message); + + vm.expectEmit(); + emit Locked(address(l1.EVM2EVMOnRamp1_2), amount); + vm.prank(alice); + l1.router.ccipSend{value: feeTokenAmount}(l2.chainSelector, message); + } + { + vm.selectFork(l2.forkId); + message.tokenAmounts[0].token = address(l2.token); + uint256 feeTokenAmount = l2.router.getFee(l1.chainSelector, message); + + vm.expectEmit(); + emit Burned(address(l2.EVM2EVMOnRamp1_2), amount); + vm.prank(alice); + l2.router.ccipSend{value: feeTokenAmount}(l1.chainSelector, message); + } + } + + function testLockOrBurnVia1_2OnRamp() public { + uint256 amount = 10e18; + { + vm.selectFork(l1.forkId); + + vm.expectEmit(); + emit Locked(address(l1.EVM2EVMOnRamp1_2), amount); + vm.prank(address(l1.EVM2EVMOnRamp1_2)); + l1.tokenPool.lockOrBurn(alice, abi.encode(alice), amount, l2.chainSelector, ""); + } + { + vm.selectFork(l2.forkId); + // router is responsible for transferring liquidity, so we mock router.token.transferFrom(user, tokenPool) + deal(address(l2.token), address(l2.tokenPool), amount); + + vm.expectEmit(); + emit Burned(address(l2.EVM2EVMOnRamp1_2), amount); + vm.prank(address(l2.EVM2EVMOnRamp1_2)); + l2.tokenPool.lockOrBurn(alice, abi.encode(alice), amount, l1.chainSelector, ""); + } + } + + function testReleaseOrMintVia1_2OffRamp() public { + uint256 amount = 10e18; + Client.EVM2AnyMessage memory message = _generateMessage(alice, 1); + + // off ramp on L1 + { + // build message + vm.selectFork(l2.forkId); + message.tokenAmounts[0] = Client.EVMTokenAmount({token: address(l2.token), amount: amount}); + uint256 feeTokenAmount = l2.router.getFee(l1.chainSelector, message); + Internal.EVM2EVMMessage memory eventArg = _messageToEvent( + message, + address(l2.EVM2EVMOnRamp1_2), + feeTokenAmount, + alice, + false + ); + + // test off ramp + vm.selectFork(l1.forkId); + + uint256 balanceBefore = l1.token.balanceOf(alice); + + vm.expectEmit(address(l1.tokenPool)); + emit Released(address(l1.EVM2EVMOffRamp1_2), alice, amount); + vm.prank(address(l1.EVM2EVMOffRamp1_2)); + l1.EVM2EVMOffRamp1_2.executeSingleMessage(eventArg, new bytes[](message.tokenAmounts.length)); + + assertEq(l1.token.balanceOf(alice), balanceBefore + amount); + } + + // off ramp on L2 + { + // build message + vm.selectFork(l1.forkId); + message.tokenAmounts[0] = Client.EVMTokenAmount({token: address(l1.token), amount: amount}); + uint256 feeTokenAmount = l1.router.getFee(l2.chainSelector, message); + Internal.EVM2EVMMessage memory eventArg = _messageToEvent( + message, + address(l1.EVM2EVMOnRamp1_2), + feeTokenAmount, + alice, + true + ); + + // test off ramp + vm.selectFork(l2.forkId); + + uint256 balanceBefore = l2.token.balanceOf(alice); + + vm.expectEmit(address(l2.tokenPool)); + emit Minted(address(l2.EVM2EVMOffRamp1_2), alice, amount); + vm.prank(address(l2.EVM2EVMOffRamp1_2)); + l2.EVM2EVMOffRamp1_2.executeSingleMessage(eventArg, new bytes[](message.tokenAmounts.length)); + + assertEq(l2.token.balanceOf(alice), balanceBefore + amount); + } + } +} diff --git a/contracts/src/v0.8/ccip/test/pools/GHO/fork/GhoTokenPoolMigrate1_4To1_5/LegacyTestnetTokenPools/UpgradeableBurnMintTokenPool_ArbSepolia.sol b/contracts/src/v0.8/ccip/test/pools/GHO/fork/GhoTokenPoolMigrate1_4To1_5/LegacyTestnetTokenPools/UpgradeableBurnMintTokenPool_ArbSepolia.sol new file mode 100644 index 0000000000..3fbf062b63 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/pools/GHO/fork/GhoTokenPoolMigrate1_4To1_5/LegacyTestnetTokenPools/UpgradeableBurnMintTokenPool_ArbSepolia.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {ITypeAndVersion} from "../../../../../../../shared/interfaces/ITypeAndVersion.sol"; +import {IBurnMintERC20} from "../../../../../../../shared/token/ERC20/IBurnMintERC20.sol"; +import {UpgradeableTokenPool} from "../../../../../../pools/GHO/UpgradeableTokenPool.sol"; +import {UpgradeableBurnMintTokenPoolAbstract} from "../../../../../../pools/GHO/UpgradeableBurnMintTokenPoolAbstract.sol"; +import {IRouter} from "../../../../../../interfaces/IRouter.sol"; +import {VersionedInitializable} from "./VersionedInitializable.sol"; + +/// @title UpgradeableBurnMintTokenPool_ArbSepolia +/// @author Aave Labs +/// @notice Upgradeable version of Chainlink's CCIP BurnMintTokenPool +/// @dev Contract adaptations: +/// - Implementation of VersionedInitializable to allow upgrades +/// - Move of allowlist and router definition to initialization stage +contract UpgradeableBurnMintTokenPool_ArbSepolia is + VersionedInitializable, + UpgradeableBurnMintTokenPoolAbstract, + ITypeAndVersion +{ + string public constant override typeAndVersion = "BurnMintTokenPool 1.4.0"; + + /// @dev Constructor + /// @param token The bridgeable token that is managed by this pool. + /// @param armProxy The address of the arm proxy + /// @param allowlistEnabled True if pool is set to access-controlled mode, false otherwise + constructor( + address token, + address armProxy, + bool allowlistEnabled + ) UpgradeableTokenPool(IBurnMintERC20(token), armProxy, allowlistEnabled) {} + + /// @dev Initializer + /// @dev The address passed as `owner` must accept ownership after initialization. + /// @dev The `allowlist` is only effective if pool is set to access-controlled mode + /// @param owner The address of the owner + /// @param allowlist A set of addresses allowed to trigger lockOrBurn as original senders + /// @param router The address of the router + function initialize(address owner, address[] memory allowlist, address router) public virtual initializer { + if (owner == address(0)) revert ZeroAddressNotAllowed(); + if (router == address(0)) revert ZeroAddressNotAllowed(); + _transferOwnership(owner); + + s_router = IRouter(router); + + // Pool can be set as permissioned or permissionless at deployment time only to save hot-path gas. + if (i_allowlistEnabled) { + _applyAllowListUpdates(new address[](0), allowlist); + } + } + + /// @inheritdoc UpgradeableBurnMintTokenPoolAbstract + function _burn(uint256 amount) internal virtual override { + IBurnMintERC20(address(i_token)).burn(amount); + } + + /// @notice Returns the revision number + /// @return The revision number + function REVISION() public pure virtual returns (uint256) { + return 1; + } + + /// @inheritdoc VersionedInitializable + function getRevision() internal pure virtual override returns (uint256) { + return REVISION(); + } +} diff --git a/contracts/src/v0.8/ccip/test/pools/GHO/fork/GhoTokenPoolMigrate1_4To1_5/LegacyTestnetTokenPools/UpgradeableLockReleaseTokenPool_Sepolia.sol b/contracts/src/v0.8/ccip/test/pools/GHO/fork/GhoTokenPoolMigrate1_4To1_5/LegacyTestnetTokenPools/UpgradeableLockReleaseTokenPool_Sepolia.sol new file mode 100644 index 0000000000..748ebd7d84 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/pools/GHO/fork/GhoTokenPoolMigrate1_4To1_5/LegacyTestnetTokenPools/UpgradeableLockReleaseTokenPool_Sepolia.sol @@ -0,0 +1,278 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {ITypeAndVersion} from "../../../../../../../shared/interfaces/ITypeAndVersion.sol"; + +import {ILiquidityContainer} from "../../../../../../../rebalancer/interfaces/ILiquidityContainer.sol"; + +import {UpgradeableTokenPool} from "../../../../../../pools/GHO/UpgradeableTokenPool.sol"; +import {RateLimiter} from "../../../../../../libraries/RateLimiter.sol"; + +import {IERC20} from "../../../../../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "../../../../../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; + +import {IRouter} from "../../../../../../interfaces/IRouter.sol"; +import {VersionedInitializable} from "./VersionedInitializable.sol"; + +/// @title UpgradeableLockReleaseTokenPool_Sepolia +/// @author Aave Labs +/// @notice Upgradeable version of Chainlink's CCIP LockReleaseTokenPool +/// @dev Contract adaptations: +/// - Implementation of VersionedInitializable to allow upgrades +/// - Move of allowlist and router definition to initialization stage +/// - Addition of a bridge limit to regulate the maximum amount of tokens that can be transferred out (burned/locked) +contract UpgradeableLockReleaseTokenPool_Sepolia is + VersionedInitializable, + UpgradeableTokenPool, + ILiquidityContainer, + ITypeAndVersion +{ + using SafeERC20 for IERC20; + + error InsufficientLiquidity(); + error LiquidityNotAccepted(); + error Unauthorized(address caller); + + error BridgeLimitExceeded(uint256 bridgeLimit); + error NotEnoughBridgedAmount(); + event BridgeLimitUpdated(uint256 oldBridgeLimit, uint256 newBridgeLimit); + + string public constant override typeAndVersion = "LockReleaseTokenPool 1.4.0"; + + /// @dev The unique lock release pool flag to signal through EIP 165. + bytes4 private constant LOCK_RELEASE_INTERFACE_ID = bytes4(keccak256("LockReleaseTokenPool")); + + /// @dev Whether or not the pool accepts liquidity. + /// External liquidity is not required when there is one canonical token deployed to a chain, + /// and CCIP is facilitating mint/burn on all the other chains, in which case the invariant + /// balanceOf(pool) on home chain == sum(totalSupply(mint/burn "wrapped" token) on all remote chains) should always hold + bool internal immutable i_acceptLiquidity; + /// @notice The address of the rebalancer. + address internal s_rebalancer; + /// @notice The address of the rate limiter admin. + /// @dev Can be address(0) if none is configured. + address internal s_rateLimitAdmin; + + /// @notice Maximum amount of tokens that can be bridged to other chains + uint256 private s_bridgeLimit; + /// @notice Amount of tokens bridged (transferred out) + /// @dev Must always be equal to or below the bridge limit + uint256 private s_currentBridged; + /// @notice The address of the bridge limit admin. + /// @dev Can be address(0) if none is configured. + address internal s_bridgeLimitAdmin; + + /// @dev Constructor + /// @param token The bridgeable token that is managed by this pool. + /// @param armProxy The address of the arm proxy + /// @param allowlistEnabled True if pool is set to access-controlled mode, false otherwise + /// @param acceptLiquidity True if the pool accepts liquidity, false otherwise + constructor( + address token, + address armProxy, + bool allowlistEnabled, + bool acceptLiquidity + ) UpgradeableTokenPool(IERC20(token), armProxy, allowlistEnabled) { + i_acceptLiquidity = acceptLiquidity; + } + + /// @dev Initializer + /// @dev The address passed as `owner` must accept ownership after initialization. + /// @dev The `allowlist` is only effective if pool is set to access-controlled mode + /// @param owner The address of the owner + /// @param allowlist A set of addresses allowed to trigger lockOrBurn as original senders + /// @param router The address of the router + /// @param bridgeLimit The maximum amount of tokens that can be bridged to other chains + function initialize( + address owner, + address[] memory allowlist, + address router, + uint256 bridgeLimit + ) public virtual initializer { + if (owner == address(0)) revert ZeroAddressNotAllowed(); + if (router == address(0)) revert ZeroAddressNotAllowed(); + _transferOwnership(owner); + + s_router = IRouter(router); + + // Pool can be set as permissioned or permissionless at deployment time only to save hot-path gas. + if (i_allowlistEnabled) { + _applyAllowListUpdates(new address[](0), allowlist); + } + s_bridgeLimit = bridgeLimit; + } + + /// @notice Locks the token in the pool + /// @param amount Amount to lock + /// @dev The whenHealthy check is important to ensure that even if a ramp is compromised + /// we're able to stop token movement via ARM. + function lockOrBurn( + address originalSender, + bytes calldata, + uint256 amount, + uint64 remoteChainSelector, + bytes calldata + ) + external + virtual + override + onlyOnRamp(remoteChainSelector) + checkAllowList(originalSender) + whenHealthy + returns (bytes memory) + { + // Increase bridged amount because tokens are leaving the source chain + if ((s_currentBridged += amount) > s_bridgeLimit) revert BridgeLimitExceeded(s_bridgeLimit); + + _consumeOutboundRateLimit(remoteChainSelector, amount); + emit Locked(msg.sender, amount); + return ""; + } + + /// @notice Release tokens from the pool to the recipient + /// @param receiver Recipient address + /// @param amount Amount to release + /// @dev The whenHealthy check is important to ensure that even if a ramp is compromised + /// we're able to stop token movement via ARM. + function releaseOrMint( + bytes memory, + address receiver, + uint256 amount, + uint64 remoteChainSelector, + bytes memory + ) external virtual override onlyOffRamp(remoteChainSelector) whenHealthy { + // This should never occur. Amount should never exceed the current bridged amount + if (amount > s_currentBridged) revert NotEnoughBridgedAmount(); + // Reduce bridged amount because tokens are back to source chain + s_currentBridged -= amount; + + _consumeInboundRateLimit(remoteChainSelector, amount); + getToken().safeTransfer(receiver, amount); + emit Released(msg.sender, receiver, amount); + } + + /// @notice returns the lock release interface flag used for EIP165 identification. + function getLockReleaseInterfaceId() public pure returns (bytes4) { + return LOCK_RELEASE_INTERFACE_ID; + } + + // @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) public pure virtual override returns (bool) { + return + interfaceId == LOCK_RELEASE_INTERFACE_ID || + interfaceId == type(ILiquidityContainer).interfaceId || + super.supportsInterface(interfaceId); + } + + /// @notice Gets Rebalancer, can be address(0) if none is configured. + /// @return The current liquidity manager. + function getRebalancer() external view returns (address) { + return s_rebalancer; + } + + /// @notice Sets the Rebalancer address. + /// @dev Only callable by the owner. + function setRebalancer(address rebalancer) external onlyOwner { + s_rebalancer = rebalancer; + } + + /// @notice Sets the rate limiter admin address. + /// @dev Only callable by the owner. + /// @param rateLimitAdmin The new rate limiter admin address. + function setRateLimitAdmin(address rateLimitAdmin) external onlyOwner { + s_rateLimitAdmin = rateLimitAdmin; + } + + /// @notice Sets the bridge limit, the maximum amount of tokens that can be bridged out + /// @dev Only callable by the owner or the bridge limit admin. + /// @dev Bridge limit changes should be carefully managed, specially when reducing below the current bridged amount + /// @param newBridgeLimit The new bridge limit + function setBridgeLimit(uint256 newBridgeLimit) external { + if (msg.sender != s_bridgeLimitAdmin && msg.sender != owner()) revert Unauthorized(msg.sender); + uint256 oldBridgeLimit = s_bridgeLimit; + s_bridgeLimit = newBridgeLimit; + emit BridgeLimitUpdated(oldBridgeLimit, newBridgeLimit); + } + + /// @notice Sets the bridge limit admin address. + /// @dev Only callable by the owner. + /// @param bridgeLimitAdmin The new bridge limit admin address. + function setBridgeLimitAdmin(address bridgeLimitAdmin) external onlyOwner { + s_bridgeLimitAdmin = bridgeLimitAdmin; + } + + /// @notice Gets the bridge limit + /// @return The maximum amount of tokens that can be transferred out to other chains + function getBridgeLimit() external view virtual returns (uint256) { + return s_bridgeLimit; + } + + /// @notice Gets the current bridged amount to other chains + /// @return The amount of tokens transferred out to other chains + function getCurrentBridgedAmount() external view virtual returns (uint256) { + return s_currentBridged; + } + + /// @notice Gets the rate limiter admin address. + function getRateLimitAdmin() external view returns (address) { + return s_rateLimitAdmin; + } + + /// @notice Gets the bridge limiter admin address. + function getBridgeLimitAdmin() external view returns (address) { + return s_bridgeLimitAdmin; + } + + /// @notice Checks if the pool can accept liquidity. + /// @return true if the pool can accept liquidity, false otherwise. + function canAcceptLiquidity() external view returns (bool) { + return i_acceptLiquidity; + } + + /// @notice Adds liquidity to the pool. The tokens should be approved first. + /// @param amount The amount of liquidity to provide. + function provideLiquidity(uint256 amount) external { + if (!i_acceptLiquidity) revert LiquidityNotAccepted(); + if (s_rebalancer != msg.sender) revert Unauthorized(msg.sender); + + i_token.safeTransferFrom(msg.sender, address(this), amount); + emit LiquidityAdded(msg.sender, amount); + } + + /// @notice Removed liquidity to the pool. The tokens will be sent to msg.sender. + /// @param amount The amount of liquidity to remove. + function withdrawLiquidity(uint256 amount) external { + if (s_rebalancer != msg.sender) revert Unauthorized(msg.sender); + + if (i_token.balanceOf(address(this)) < amount) revert InsufficientLiquidity(); + i_token.safeTransfer(msg.sender, amount); + emit LiquidityRemoved(msg.sender, amount); + } + + /// @notice Sets the rate limiter admin address. + /// @dev Only callable by the owner or the rate limiter admin. NOTE: overwrites the normal + /// onlyAdmin check in the base implementation to also allow the rate limiter admin. + /// @param remoteChainSelector The remote chain selector for which the rate limits apply. + /// @param outboundConfig The new outbound rate limiter config. + /// @param inboundConfig The new inbound rate limiter config. + function setChainRateLimiterConfig( + uint64 remoteChainSelector, + RateLimiter.Config memory outboundConfig, + RateLimiter.Config memory inboundConfig + ) external override { + if (msg.sender != s_rateLimitAdmin && msg.sender != owner()) revert Unauthorized(msg.sender); + + _setRateLimitConfig(remoteChainSelector, outboundConfig, inboundConfig); + } + + /// @notice Returns the revision number + /// @return The revision number + function REVISION() public pure virtual returns (uint256) { + return 1; + } + + /// @inheritdoc VersionedInitializable + function getRevision() internal pure virtual override returns (uint256) { + return REVISION(); + } +} diff --git a/contracts/src/v0.8/ccip/test/pools/GHO/fork/GhoTokenPoolMigrate1_4To1_5/LegacyTestnetTokenPools/VersionedInitializable.sol b/contracts/src/v0.8/ccip/test/pools/GHO/fork/GhoTokenPoolMigrate1_4To1_5/LegacyTestnetTokenPools/VersionedInitializable.sol new file mode 100644 index 0000000000..b9fb054fa0 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/pools/GHO/fork/GhoTokenPoolMigrate1_4To1_5/LegacyTestnetTokenPools/VersionedInitializable.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +/** + * @title VersionedInitializable + * @author Aave, inspired by the OpenZeppelin Initializable contract + * @notice Helper contract to implement initializer functions. To use it, replace + * the constructor with a function that has the `initializer` modifier. + * @dev WARNING: Unlike constructors, initializer functions must be manually + * invoked. This applies both to deploying an Initializable contract, as well + * as extending an Initializable contract via inheritance. + * WARNING: When used with inheritance, manual care must be taken to not invoke + * a parent initializer twice, or ensure that all initializers are idempotent, + * because this is not dealt with automatically as with constructors. + */ +abstract contract VersionedInitializable { + /** + * @dev Indicates that the contract has been initialized. + */ + uint256 private lastInitializedRevision = 0; + + /** + * @dev Indicates that the contract is in the process of being initialized. + */ + bool private initializing; + + /** + * @dev Modifier to use in the initializer function of a contract. + */ + modifier initializer() { + uint256 revision = getRevision(); + require( + initializing || isConstructor() || revision > lastInitializedRevision, + "Contract instance has already been initialized" + ); + + bool isTopLevelCall = !initializing; + if (isTopLevelCall) { + initializing = true; + lastInitializedRevision = revision; + } + + _; + + if (isTopLevelCall) { + initializing = false; + } + } + + /** + * @notice Returns the revision number of the contract + * @dev Needs to be defined in the inherited class as a constant. + * @return The revision number + */ + function getRevision() internal pure virtual returns (uint256); + + /** + * @notice Returns true if and only if the function is running in the constructor + * @return True if the function is running in the constructor + */ + function isConstructor() private view returns (bool) { + // extcodesize checks the size of the code stored in an address, and + // address returns the current address. Since the code is still not + // deployed when running a constructor, any checks on its code size will + // yield zero, making it an effective way to detect if a contract is + // under construction or not. + uint256 cs; + //solium-disable-next-line + assembly { + cs := extcodesize(address()) + } + return cs == 0; + } + + // Reserved storage space to allow for layout changes in the future. + uint256[50] private ______gap; +} diff --git a/contracts/src/v0.8/ccip/test/pools/GHO/fork/GhoTokenPoolMigrate1_4To1_5/TokenPoolsUpgrade.t.sol b/contracts/src/v0.8/ccip/test/pools/GHO/fork/GhoTokenPoolMigrate1_4To1_5/TokenPoolsUpgrade.t.sol new file mode 100644 index 0000000000..a649167773 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/pools/GHO/fork/GhoTokenPoolMigrate1_4To1_5/TokenPoolsUpgrade.t.sol @@ -0,0 +1,239 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.19; + +import {TransparentUpgradeableProxy} from "solidity-utils/contracts/transparent-proxy/TransparentUpgradeableProxy.sol"; +import {ProxyAdmin} from "solidity-utils/contracts/transparent-proxy/ProxyAdmin.sol"; +import {Client} from "../../../../../libraries/Client.sol"; +import {Internal} from "../../../../../libraries/Internal.sol"; +import {ForkBase} from "./ForkBase.t.sol"; +import {UpgradeableLockReleaseTokenPool_Sepolia} from "./LegacyTestnetTokenPools/UpgradeableLockReleaseTokenPool_Sepolia.sol"; +import {UpgradeableBurnMintTokenPool_ArbSepolia} from "./LegacyTestnetTokenPools/UpgradeableBurnMintTokenPool_ArbSepolia.sol"; + +contract ForkPoolUpgradeAfterMigration is ForkBase { + function setUp() public override { + super.setUp(); + + // #1: deploy new implementation & upgrade token pools + vm.selectFork(l1.forkId); + _upgradeExistingLockReleaseTokenPool(); + + vm.selectFork(l2.forkId); + _upgradeExistingBurnMintTokenPool(); + + // #2: setProxyPool + vm.selectFork(l1.forkId); + vm.prank(l1.tokenPool.owner()); + l1.tokenPool.setProxyPool(l1.proxyPool); + + vm.selectFork(l2.forkId); + vm.prank(l2.tokenPool.owner()); + l2.tokenPool.setProxyPool(l2.proxyPool); + } + + function testLockOrBurnViaRouter() public { + uint256 amount = 10e18; + Client.EVM2AnyMessage memory message = _generateMessage(alice, 1); + + { + vm.selectFork(l1.forkId); + + message.tokenAmounts[0] = Client.EVMTokenAmount({token: address(l1.token), amount: amount}); + uint256 feeTokenAmount = l1.router.getFee(l2.chainSelector, message); + + // router uses 1_5 onRamp + assertEq(l1.router.getOnRamp(l2.chainSelector), address(l1.EVM2EVMOnRamp1_5)); + vm.expectEmit(); + emit CCIPSendRequested(_messageToEvent(message, address(l1.EVM2EVMOnRamp1_5), feeTokenAmount, alice, true)); + vm.prank(alice); + l1.router.ccipSend{value: feeTokenAmount}(l2.chainSelector, message); + } + + { + vm.selectFork(l2.forkId); + + message.tokenAmounts[0] = Client.EVMTokenAmount({token: address(l2.token), amount: amount}); + uint256 feeTokenAmount = l2.router.getFee(l1.chainSelector, message); + + // router uses 1_5 onRamp + assertEq(l2.router.getOnRamp(l1.chainSelector), address(l2.EVM2EVMOnRamp1_5)); + vm.expectEmit(); + emit CCIPSendRequested(_messageToEvent(message, address(l2.EVM2EVMOnRamp1_5), feeTokenAmount, alice, false)); + vm.prank(alice); + l2.router.ccipSend{value: feeTokenAmount}(l1.chainSelector, message); + } + } + + function testRevertLockOrBurnVia1_2OnRamp() public { + uint256 amount = 10e18; + { + vm.selectFork(l1.forkId); + vm.expectRevert(abi.encodeWithSelector(CallerIsNotARampOnRouter.selector, address(l1.EVM2EVMOnRamp1_2))); + vm.prank(address(l1.EVM2EVMOnRamp1_2)); + l1.tokenPool.lockOrBurn(alice, abi.encode(alice), amount, l2.chainSelector, ""); + } + { + vm.selectFork(l2.forkId); + vm.expectRevert(abi.encodeWithSelector(CallerIsNotARampOnRouter.selector, address(l2.EVM2EVMOnRamp1_2))); + vm.prank(address(l2.EVM2EVMOnRamp1_2)); + l2.tokenPool.lockOrBurn(alice, abi.encode(alice), amount, l1.chainSelector, ""); + } + } + + function testReleaseOrMintVia1_2OffRamp() public { + uint256 amount = 10e18; + Client.EVM2AnyMessage memory message = _generateMessage(alice, 1); + + // off ramp on L1 + { + // build message + vm.selectFork(l2.forkId); + message.tokenAmounts[0] = Client.EVMTokenAmount({token: address(l2.token), amount: amount}); + uint256 feeTokenAmount = l2.router.getFee(l1.chainSelector, message); + Internal.EVM2EVMMessage memory eventArg = _messageToEvent( + message, + address(l2.EVM2EVMOnRamp1_5), + feeTokenAmount, + alice, + false + ); + + // test off ramp + vm.selectFork(l1.forkId); + + uint256 balanceBefore = l1.token.balanceOf(alice); + + vm.expectEmit(address(l1.tokenPool)); + emit Released(address(l1.EVM2EVMOffRamp1_2), alice, amount); + vm.prank(address(l1.EVM2EVMOffRamp1_2)); + l1.EVM2EVMOffRamp1_2.executeSingleMessage(eventArg, new bytes[](message.tokenAmounts.length)); + + assertEq(l1.token.balanceOf(alice), balanceBefore + amount); + } + + // off ramp on L2 + { + // build message + vm.selectFork(l1.forkId); + message.tokenAmounts[0] = Client.EVMTokenAmount({token: address(l1.token), amount: amount}); + uint256 feeTokenAmount = l1.router.getFee(l2.chainSelector, message); + Internal.EVM2EVMMessage memory eventArg = _messageToEvent( + message, + address(l1.EVM2EVMOnRamp1_5), + feeTokenAmount, + alice, + true + ); + + // test off ramp + vm.selectFork(l2.forkId); + + uint256 balanceBefore = l2.token.balanceOf(alice); + + vm.expectEmit(address(l2.tokenPool)); + emit Minted(address(l2.EVM2EVMOffRamp1_2), alice, amount); + vm.prank(address(l2.EVM2EVMOffRamp1_2)); + l2.EVM2EVMOffRamp1_2.executeSingleMessage(eventArg, new bytes[](message.tokenAmounts.length)); + + assertEq(l2.token.balanceOf(alice), balanceBefore + amount); + } + } + + function testReleaseOrMintVia1_5OffRamp() public { + uint256 amount = 10e18; + Client.EVM2AnyMessage memory message = _generateMessage(alice, 1); + + // off ramp on L1 + { + // build message + vm.selectFork(l2.forkId); + message.tokenAmounts[0] = Client.EVMTokenAmount({token: address(l2.token), amount: amount}); + uint256 feeTokenAmount = l2.router.getFee(l1.chainSelector, message); + Internal.EVM2EVMMessage memory eventArg = _messageToEvent( + message, + address(l2.EVM2EVMOnRamp1_5), + feeTokenAmount, + alice, + false + ); + + // test off ramp + vm.selectFork(l1.forkId); + + uint256 balanceBefore = l1.token.balanceOf(alice); + + vm.expectEmit(address(l1.tokenPool)); + emit Released(address(l1.proxyPool), alice, amount); + vm.expectEmit(address(l1.proxyPool)); + emit Released(address(l1.EVM2EVMOffRamp1_5), alice, amount); + vm.prank(address(l1.EVM2EVMOffRamp1_5)); + l1.EVM2EVMOffRamp1_5.executeSingleMessage(eventArg, new bytes[](message.tokenAmounts.length), new uint32[](0)); + + assertEq(l1.token.balanceOf(alice), balanceBefore + amount); + } + + // off ramp on L2 + { + // build message + vm.selectFork(l1.forkId); + message.tokenAmounts[0] = Client.EVMTokenAmount({token: address(l1.token), amount: amount}); + uint256 feeTokenAmount = l1.router.getFee(l2.chainSelector, message); + Internal.EVM2EVMMessage memory eventArg = _messageToEvent( + message, + address(l1.EVM2EVMOnRamp1_5), + feeTokenAmount, + alice, + true + ); + + // test off ramp + vm.selectFork(l2.forkId); + + uint256 balanceBefore = l2.token.balanceOf(alice); + + vm.expectEmit(address(l2.tokenPool)); + emit Minted(address(l2.proxyPool), alice, amount); + vm.expectEmit(address(l2.proxyPool)); + emit Minted(address(l2.EVM2EVMOffRamp1_5), alice, amount); + vm.prank(address(l2.EVM2EVMOffRamp1_5)); + l2.EVM2EVMOffRamp1_5.executeSingleMessage(eventArg, new bytes[](message.tokenAmounts.length), new uint32[](0)); + + assertEq(l2.token.balanceOf(alice), balanceBefore + amount); + } + } + + function _upgradeExistingLockReleaseTokenPool() internal { + UpgradeableLockReleaseTokenPool_Sepolia poolImpl = new UpgradeableLockReleaseTokenPool_Sepolia( + address(l1.token), + l1.tokenPool.getArmProxy(), + l1.tokenPool.getAllowListEnabled(), + l1.tokenPool.canAcceptLiquidity() + ); + _upgradeProxy(TransparentUpgradeableProxy(payable(address(l1.tokenPool))), address(poolImpl)); + } + + function _upgradeExistingBurnMintTokenPool() internal { + UpgradeableBurnMintTokenPool_ArbSepolia poolImpl = new UpgradeableBurnMintTokenPool_ArbSepolia( + address(l2.token), + l2.tokenPool.getArmProxy(), + l2.tokenPool.getAllowListEnabled() + ); + _upgradeProxy(TransparentUpgradeableProxy(payable(address(l2.tokenPool))), address(poolImpl)); + } + + function _upgradeProxy(TransparentUpgradeableProxy proxy, address impl) private { + address proxyAdminAddress = address( + uint160(uint256(vm.load(address(proxy), bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1)))) + ); + assertNotEq(proxyAdminAddress, address(0), "version mismatch: proxyAdmin"); + if (proxyAdminAddress.code.length != 0) { + ProxyAdmin proxyAdmin = ProxyAdmin(proxyAdminAddress); + assertEq(proxyAdmin.getProxyAdmin(proxy), address(proxyAdmin)); + vm.prank(proxyAdmin.owner()); + proxyAdmin.upgrade(proxy, address(impl)); + } else { + // sepolia has proxy admin as an eoa + vm.prank(proxyAdminAddress); + proxy.upgradeTo(address(impl)); + } + } +}