From a6c6a4cb834f30c9d6d3641950ad8f6f06288951 Mon Sep 17 00:00:00 2001 From: Josh Date: Mon, 12 Aug 2024 11:18:38 -0400 Subject: [PATCH] Add transferLiquidity function back in --- contracts/gas-snapshots/ccip.gas-snapshot | 13 ++--- .../IMigratableMultiMechanismTokenPool.sol | 5 ++ .../MultiMechanismUSDCTokenPool.sol | 40 +++++++++++----- contracts/src/v0.8/ccip/pools/TokenPool.sol | 2 +- .../pools/MultiMechanismUSDCTokenPool.t.sol | 48 +++++++++++++++++++ 5 files changed, 88 insertions(+), 20 deletions(-) create mode 100644 contracts/src/v0.8/ccip/pools/MultiMechanismPools/IMigratableMultiMechanismTokenPool.sol diff --git a/contracts/gas-snapshots/ccip.gas-snapshot b/contracts/gas-snapshots/ccip.gas-snapshot index 21d24c09fa..e4c928c199 100644 --- a/contracts/gas-snapshots/ccip.gas-snapshot +++ b/contracts/gas-snapshots/ccip.gas-snapshot @@ -942,17 +942,18 @@ TokenProxy_getFee:test_GetFeeGasShouldBeZero_Revert() (gas: 16827) TokenProxy_getFee:test_GetFeeInvalidToken_Revert() (gas: 12658) TokenProxy_getFee:test_GetFeeNoDataAllowed_Revert() (gas: 15849) TokenProxy_getFee:test_GetFee_Success() (gas: 86702) -USDCTokenPoolAltMechanismTests:test_LockOrBurn_PrimaryMechanism_Success() (gas: 136896) -USDCTokenPoolAltMechanismTests:test_LockOrBurn_WhileMigrationPause_Revert() (gas: 98179) -USDCTokenPoolAltMechanismTests:test_LockOrBurn_altMechanism_then_switchToPrimary_Success() (gas: 209580) -USDCTokenPoolAltMechanismTests:test_LockOrBurn_onAltMechanism_Success() (gas: 143718) +USDCTokenPoolAltMechanismTests:test_LockOrBurn_PrimaryMechanism_Success() (gas: 135152) +USDCTokenPoolAltMechanismTests:test_LockOrBurn_WhileMigrationPause_Revert() (gas: 97433) +USDCTokenPoolAltMechanismTests:test_LockOrBurn_altMechanism_then_switchToPrimary_Success() (gas: 207691) +USDCTokenPoolAltMechanismTests:test_LockOrBurn_onAltMechanism_Success() (gas: 143689) USDCTokenPoolAltMechanismTests:test_MintOrRelease_OnAltMechanism_Success() (gas: 212557) USDCTokenPoolAltMechanismTests:test_MintOrRelease_altMechanism_then_switchToPrimary_Success() (gas: 423280) USDCTokenPoolAltMechanismTests:test_MintOrRelease_incomingMessageWithPrimaryMechanism() (gas: 270078) USDCTokenPoolAltMechanismTests:test_withdrawLiquidity_Success() (gas: 118475) USDCTokenPoolMigrationTests:test_burnLockedUSDC_invalidPermissions_Revert() (gas: 39951) -USDCTokenPoolMigrationTests:test_lockOrBurn_then_BurnInCCTPMigration_Success() (gas: 246269) -USDCTokenPoolMigrationTests:test_proposeCCTPMigration_withInvalidSelector_Revert() (gas: 35556) +USDCTokenPoolMigrationTests:test_lockOrBurn_then_BurnInCCTPMigration_Success() (gas: 246296) +USDCTokenPoolMigrationTests:test_proposeCCTPMigration_withInvalidSelector_Revert() (gas: 35644) +USDCTokenPoolMigrationTests:test_transferLiquidity_Success() (gas: 152643) USDCTokenPool__validateMessage:test_ValidateInvalidMessage_Revert() (gas: 25290) USDCTokenPool_lockOrBurn:test_CallerIsNotARampOnRouter_Revert() (gas: 35322) USDCTokenPool_lockOrBurn:test_LockOrBurnWithAllowList_Revert() (gas: 30073) diff --git a/contracts/src/v0.8/ccip/pools/MultiMechanismPools/IMigratableMultiMechanismTokenPool.sol b/contracts/src/v0.8/ccip/pools/MultiMechanismPools/IMigratableMultiMechanismTokenPool.sol new file mode 100644 index 0000000000..de5ddf0dee --- /dev/null +++ b/contracts/src/v0.8/ccip/pools/MultiMechanismPools/IMigratableMultiMechanismTokenPool.sol @@ -0,0 +1,5 @@ +pragma solidity ^0.8.0; + +interface IMigratableMultiMechanismTokenPool { + function withdrawLiquidity(uint64 remoteChainSelector, uint256 amount) external; +} diff --git a/contracts/src/v0.8/ccip/pools/MultiMechanismPools/MultiMechanismUSDCTokenPool.sol b/contracts/src/v0.8/ccip/pools/MultiMechanismPools/MultiMechanismUSDCTokenPool.sol index a3bb82be44..ef5af3eb4a 100644 --- a/contracts/src/v0.8/ccip/pools/MultiMechanismPools/MultiMechanismUSDCTokenPool.sol +++ b/contracts/src/v0.8/ccip/pools/MultiMechanismPools/MultiMechanismUSDCTokenPool.sol @@ -3,9 +3,9 @@ pragma solidity 0.8.24; import {ILiquidityContainer} from "../../../liquiditymanager/interfaces/ILiquidityContainer.sol"; import {ITokenMessenger} from "../USDC/ITokenMessenger.sol"; +import {IMigratableMultiMechanismTokenPool} from "./IMigratableMultiMechanismTokenPool.sol"; import {Pool} from "../../libraries/Pool.sol"; - import {TokenPool} from "../TokenPool.sol"; import {USDCTokenPool} from "../USDC/USDCTokenPool.sol"; import {MultiMechanismPoolManager} from "./MultiMechanismPoolManager.sol"; @@ -28,6 +28,8 @@ contract MultiMechanismUSDCTokenPool is USDCTokenPool, MultiMechanismPoolManager /// balanceOf(pool) on home chain >= sum(totalSupply(mint/burn "wrapped" token) on all remote chains) should always hold address internal s_rebalancer; + event LiquidityTransferred(address indexed from, uint64 indexed remoteChainSelector, uint256 amount); + error LanePausedForCCTPMigration(uint64 remoteChainSelector); constructor( @@ -48,7 +50,11 @@ contract MultiMechanismUSDCTokenPool is USDCTokenPool, MultiMechanismPoolManager { // If the alternative mechanism (L/R) for chains which have it enabled if (shouldUseAltMechForOutgoingMessage(lockOrBurnIn.remoteChainSelector)) { - return _altMechOutgoingMessage(lockOrBurnIn); + if (s_proposedUSDCMigrationChain != 0 && s_proposedUSDCMigrationChain == lockOrBurnIn.remoteChainSelector) { + revert LanePausedForCCTPMigration(s_proposedUSDCMigrationChain); + } else { + return _altMechOutgoingMessage(lockOrBurnIn); + } } else { // Otherwise, use native CCTP functionality return super.lockOrBurn(lockOrBurnIn); @@ -107,17 +113,6 @@ contract MultiMechanismUSDCTokenPool is USDCTokenPool, MultiMechanismPoolManager return Pool.LockOrBurnOutV1({destTokenAddress: getRemoteToken(lockOrBurnIn.remoteChainSelector), destPoolData: ""}); } - /// @notice Circle's migration policies requires a pausable supply-lock during the migration process. - /// This override adds an additional condition to valid lockOrBurns that ensures the destination chain - /// is not currently undergoing a migration by reverting, as well as all other validations - function _validateLockOrBurn(Pool.LockOrBurnInV1 memory lockOrBurnIn) internal override { - if (s_proposedUSDCMigrationChain != 0 && s_proposedUSDCMigrationChain == lockOrBurnIn.remoteChainSelector) { - revert LanePausedForCCTPMigration(s_proposedUSDCMigrationChain); - } - - super._validateLockOrBurn(lockOrBurnIn); - } - /// @notice Gets LiquidityManager, can be address(0) if none is configured. /// @return The current liquidity manager. function getRebalancer() external view returns (address) { @@ -158,4 +153,23 @@ contract MultiMechanismUSDCTokenPool is USDCTokenPool, MultiMechanismPoolManager i_token.safeTransfer(msg.sender, amount); emit ILiquidityContainer.LiquidityRemoved(msg.sender, amount); } + + /// @notice This function can be used to transfer liquidity from an older version of the pool to this pool. To do so + /// this pool will have to be set as the rebalancer in the older version of the pool. This allows it to transfer the + /// funds in the old pool to the new pool. + /// @dev When upgrading a LockRelease pool, this function can be called at the same time as the pool is changed in the + /// TokenAdminRegistry. This allows for a smooth transition of both liquidity and transactions to the new pool. + /// Alternatively, when no multicall is available, a portion of the funds can be transferred to the new pool before + /// changing which pool CCIP uses, to ensure both pools can operate. Then the pool should be changed in the + /// TokenAdminRegistry, which will activate the new pool. All new transactions will use the new pool and its + /// liquidity. Finally, the remaining liquidity can be transferred to the new pool using this function one more time. + /// @param from The address of the old pool. + /// @param amount The amount of liquidity to transfer. + function transferLiquidity(address from, uint64 remoteChainSelector, uint256 amount) external onlyOwner { + IMigratableMultiMechanismTokenPool(from).withdrawLiquidity(remoteChainSelector, amount); + + s_lockedTokensByChainSelector[remoteChainSelector] += amount; + + emit LiquidityTransferred(from, remoteChainSelector, amount); + } } diff --git a/contracts/src/v0.8/ccip/pools/TokenPool.sol b/contracts/src/v0.8/ccip/pools/TokenPool.sol index 0c179f07c2..c0c6f2198d 100644 --- a/contracts/src/v0.8/ccip/pools/TokenPool.sol +++ b/contracts/src/v0.8/ccip/pools/TokenPool.sol @@ -157,7 +157,7 @@ abstract contract TokenPool is IPoolV1, OwnerIsCreator { /// @param lockOrBurnIn The input to validate. /// @dev This function should always be called before executing a lock or burn. Not doing so would allow /// for various exploits. - function _validateLockOrBurn(Pool.LockOrBurnInV1 memory lockOrBurnIn) internal virtual { + function _validateLockOrBurn(Pool.LockOrBurnInV1 memory lockOrBurnIn) internal { if (!isSupportedToken(lockOrBurnIn.localToken)) revert InvalidToken(lockOrBurnIn.localToken); if (IRMN(i_rmnProxy).isCursed(bytes16(uint128(lockOrBurnIn.remoteChainSelector)))) revert CursedByRMN(); _checkAllowList(lockOrBurnIn.originalSender); diff --git a/contracts/src/v0.8/ccip/test/pools/MultiMechanismUSDCTokenPool.t.sol b/contracts/src/v0.8/ccip/test/pools/MultiMechanismUSDCTokenPool.t.sol index bea4475401..bc57ebb4c5 100644 --- a/contracts/src/v0.8/ccip/test/pools/MultiMechanismUSDCTokenPool.t.sol +++ b/contracts/src/v0.8/ccip/test/pools/MultiMechanismUSDCTokenPool.t.sol @@ -54,6 +54,7 @@ contract USDCTokenPoolSetup is BaseTest { Router internal s_router; MultiMechanismUSDCTokenPool internal s_usdcTokenPool; + MultiMechanismUSDCTokenPool internal s_usdcTokenPoolTransferLiquidity; address[] internal s_allowedList; function setUp() public virtual override { @@ -71,6 +72,9 @@ contract USDCTokenPoolSetup is BaseTest { s_usdcTokenPool = new MultiMechanismUSDCTokenPool(s_mockUSDC, s_token, new address[](0), address(s_mockRMN), address(s_router)); + s_usdcTokenPoolTransferLiquidity = + new MultiMechanismUSDCTokenPool(s_mockUSDC, s_token, new address[](0), address(s_mockRMN), address(s_router)); + usdcToken.grantMintAndBurnRoles(address(s_mockUSDC)); usdcToken.grantMintAndBurnRoles(address(s_usdcTokenPool)); @@ -592,4 +596,48 @@ contract USDCTokenPoolMigrationTests is USDCTokenPoolSetup { vm.expectRevert(abi.encodeWithSelector(USDCBridgeMigrator.ExistingMigrationProposal.selector)); s_usdcTokenPool.burnLockedUSDC(); } + + function test_transferLiquidity_Success() public { + // Set as the OWNER so we can provide liquidity + vm.startPrank(OWNER); + s_usdcTokenPoolTransferLiquidity.setRebalancer(OWNER); + + s_token.approve(address(s_usdcTokenPoolTransferLiquidity), type(uint256).max); + + uint256 liquidityAmount = 1e9; + + // Provide 1000 USDC as liquidity + s_usdcTokenPoolTransferLiquidity.provideLiquidity(DEST_CHAIN_SELECTOR, liquidityAmount); + + // Set the new token pool as the rebalancer + s_usdcTokenPoolTransferLiquidity.setRebalancer(address(s_usdcTokenPool)); + + vm.expectEmit(); + emit ILiquidityContainer.LiquidityRemoved(address(s_usdcTokenPool), liquidityAmount); + + vm.expectEmit(); + emit MultiMechanismUSDCTokenPool.LiquidityTransferred( + address(s_usdcTokenPoolTransferLiquidity), DEST_CHAIN_SELECTOR, liquidityAmount + ); + + s_usdcTokenPool.transferLiquidity(address(s_usdcTokenPoolTransferLiquidity), DEST_CHAIN_SELECTOR, liquidityAmount); + + assertEq( + s_usdcTokenPool.getLockedTokensForChain(DEST_CHAIN_SELECTOR), + liquidityAmount, + "Tokens locked for dest chain doesn't match expected amount in storage" + ); + + assertEq( + s_usdcTokenPoolTransferLiquidity.getLockedTokensForChain(DEST_CHAIN_SELECTOR), + 0, + "Tokens locked for dest chain in old token pool doesn't match expected amount in storage" + ); + + assertEq( + s_token.balanceOf(address(s_usdcTokenPool)), + liquidityAmount, + "Liquidity amount of tokens should be new in new pool, but aren't" + ); + } }