diff --git a/contracts/.changeset/chilly-news-wink.md b/contracts/.changeset/chilly-news-wink.md new file mode 100644 index 00000000000..2ccdbb483ca --- /dev/null +++ b/contracts/.changeset/chilly-news-wink.md @@ -0,0 +1,10 @@ +--- +'@chainlink/contracts': minor +--- + +#feature Add two new pool types: Siloed-LockRelease and BurnToAddress and fix bug in HybridUSDCTokenPool for transferLiqudity #bugfix + + +PR issue: CCIP-4723 + +Solidity Review issue: CCIP-3966 \ No newline at end of file diff --git a/contracts/gas-snapshots/ccip.gas-snapshot b/contracts/gas-snapshots/ccip.gas-snapshot index 942e75dc516..f1eefe78fa8 100644 --- a/contracts/gas-snapshots/ccip.gas-snapshot +++ b/contracts/gas-snapshots/ccip.gas-snapshot @@ -4,6 +4,9 @@ BurnMintTokenPool_lockOrBurn:test_PoolBurn() (gas: 236872) BurnMintTokenPool_lockOrBurn:test_Setup() (gas: 17819) BurnMintTokenPool_releaseOrMint:test_PoolMint() (gas: 102527) BurnMintWithLockReleaseFlagTokenPool_lockOrBurn:test_LockOrBurn_CorrectReturnData() (gas: 237292) +BurnToAddressMintTokenPool_lockOrBurn:test_LockOrBurn() (gas: 257956) +BurnToAddressMintTokenPool_releaseOrMint:test_releaseOrMint() (gas: 126048) +BurnToAddressMintTokenPool_setOutstandingokens:test_setOutstandingTokens() (gas: 37793) BurnWithFromMintTokenPool_lockOrBurn:test_PoolBurn() (gas: 239012) BurnWithFromMintTokenPool_lockOrBurn:test_Setup() (gas: 24169) CCIPClientExample_sanity:test_ImmutableExamples() (gas: 2073613) @@ -126,12 +129,11 @@ FeeQuoter_updateTokenPriceFeeds:test_SingleFeedUpdate() (gas: 53171) FeeQuoter_updateTokenPriceFeeds:test_ZeroFeeds() (gas: 12471) FeeQuoter_validateDestFamilyAddress:test_ValidEVMAddress() (gas: 6767) FeeQuoter_validateDestFamilyAddress:test_ValidNonEVMAddress() (gas: 6492) -HybridLockReleaseUSDCTokenPool_TransferLiquidity:test_transferLiquidity() (gas: 167013) -HybridLockReleaseUSDCTokenPool_lockOrBurn:test_PrimaryMechanism() (gas: 130356) -HybridLockReleaseUSDCTokenPool_lockOrBurn:test_onLockReleaseMechanism() (gas: 140104) +HybridLockReleaseUSDCTokenPool_lockOrBurn:test_PrimaryMechanism() (gas: 130339) +HybridLockReleaseUSDCTokenPool_lockOrBurn:test_onLockReleaseMechanism() (gas: 140169) HybridLockReleaseUSDCTokenPool_lockOrBurn:test_onLockReleaseMechanism_thenSwitchToPrimary() (gas: 202967) -HybridLockReleaseUSDCTokenPool_releaseOrMint:test_OnLockReleaseMechanism() (gas: 206218) -HybridLockReleaseUSDCTokenPool_releaseOrMint:test_incomingMessageWithPrimaryMechanism() (gas: 260387) +HybridLockReleaseUSDCTokenPool_releaseOrMint:test_OnLockReleaseMechanism() (gas: 206350) +HybridLockReleaseUSDCTokenPool_releaseOrMint:test_incomingMessageWithPrimaryMechanism() (gas: 260423) LockReleaseTokenPool_canAcceptLiquidity:test_CanAcceptLiquidity() (gas: 3222607) LockReleaseTokenPool_lockOrBurn:test_LockOrBurnWithAllowList() (gas: 72828) LockReleaseTokenPool_releaseOrMint:test_ReleaseOrMint() (gas: 217898) @@ -348,6 +350,22 @@ Router_recoverTokens:test_RecoverTokens() (gas: 52668) Router_routeMessage:test_routeMessage_AutoExec() (gas: 38071) Router_routeMessage:test_routeMessage_ExecutionEvent() (gas: 153593) Router_routeMessage:test_routeMessage_ManualExec() (gas: 31120) +SiloedLockReleaseTokenPool_lockOrBurn:test_lockOrBurn_SiloedFunds() (gas: 76874) +SiloedLockReleaseTokenPool_lockOrBurn:test_lockOrBurn_UnsiloedFunds() (gas: 76104) +SiloedLockReleaseTokenPool_provideLiqudity:test_ProvideLiquidity_LegacyProvideLiquiditySelector() (gas: 91873) +SiloedLockReleaseTokenPool_provideLiqudity:test_ProvideLiquidity_SiloedChain() (gas: 82416) +SiloedLockReleaseTokenPool_provideLiqudity:test_ProvideLiquidity_UnsiloedChain() (gas: 84036) +SiloedLockReleaseTokenPool_releaseOrMint:test_ReleaseOrMint_RevertsWhen_InsufficientLiquidity_SiloedChain() (gas: 110002) +SiloedLockReleaseTokenPool_releaseOrMint:test_ReleaseOrMint_RevertsWhen_InsufficientLiquidity_UnsiloedChain() (gas: 115718) +SiloedLockReleaseTokenPool_releaseOrMint:test_ReleaseOrMint_SiloedChain() (gas: 262340) +SiloedLockReleaseTokenPool_releaseOrMint:test_ReleaseOrMint_UnsiloedChain() (gas: 263392) +SiloedLockReleaseTokenPool_setRebalancer:test_setRebalancer_UnsiloedChains() (gas: 24429) +SiloedLockReleaseTokenPool_setRebalancer:test_setSiloRebalancer() (gas: 32165) +SiloedLockReleaseTokenPool_updateSiloDesignations:test_updateSiloDesignations() (gas: 105825) +SiloedLockReleaseTokenPool_withdrawLiqudity:test_withdrawLiquidity_RevertsWhen_LegacyFunctionSelectorUnauthorized() (gas: 18244) +SiloedLockReleaseTokenPool_withdrawLiqudity:test_withdrawLiquidity_SiloedFunds() (gas: 70948) +SiloedLockReleaseTokenPool_withdrawLiqudity:test_withdrawLiquidity_UnsiloedFunds_LegacyFunctionSelector() (gas: 76391) +SiloedLockReleaseTokenPool_withdrawLiqudity:test_withdrawSiloedLiquidity_UnsiloedFunds() (gas: 71945) TokenAdminRegistry_acceptAdminRole:test_acceptAdminRole() (gas: 44236) TokenAdminRegistry_addRegistryModule:test_addRegistryModule() (gas: 67093) TokenAdminRegistry_getAllConfiguredTokens:test_getAllConfiguredTokens_outOfBounds() (gas: 11363) @@ -387,22 +405,22 @@ TokenPool_parseRemoteDecimals:test_parseRemoteDecimals() (gas: 14030) TokenPool_parseRemoteDecimals:test_parseRemoteDecimals_NoDecimalsDefaultsToLocalDecimals() (gas: 9705) TokenPool_removeRemotePool:test_removeRemotePool() (gas: 188402) TokenPool_setRateLimitAdmin:test_SetRateLimitAdmin() (gas: 37630) -USDCBridgeMigrator_BurnLockedUSDC:test_PrimaryMechanism() (gas: 130520) -USDCBridgeMigrator_BurnLockedUSDC:test_lockOrBurn_then_BurnInCCTPMigration() (gas: 303986) -USDCBridgeMigrator_BurnLockedUSDC:test_onLockReleaseMechanism() (gas: 140171) +USDCBridgeMigrator_BurnLockedUSDC:test_PrimaryMechanism() (gas: 130502) +USDCBridgeMigrator_BurnLockedUSDC:test_lockOrBurn_then_BurnInCCTPMigration() (gas: 303967) +USDCBridgeMigrator_BurnLockedUSDC:test_onLockReleaseMechanism() (gas: 140236) USDCBridgeMigrator_BurnLockedUSDC:test_onLockReleaseMechanism_thenSwitchToPrimary() (gas: 203330) -USDCBridgeMigrator_cancelMigrationProposal:test_cancelExistingCCTPMigrationProposal() (gas: 56117) -USDCBridgeMigrator_provideLiquidity:test_PrimaryMechanism() (gas: 130538) -USDCBridgeMigrator_provideLiquidity:test_lockOrBurn_then_BurnInCCTPMigration() (gas: 303986) -USDCBridgeMigrator_provideLiquidity:test_onLockReleaseMechanism() (gas: 140260) +USDCBridgeMigrator_cancelMigrationProposal:test_cancelExistingCCTPMigrationProposal() (gas: 56100) +USDCBridgeMigrator_provideLiquidity:test_PrimaryMechanism() (gas: 130520) +USDCBridgeMigrator_provideLiquidity:test_lockOrBurn_then_BurnInCCTPMigration() (gas: 303967) +USDCBridgeMigrator_provideLiquidity:test_onLockReleaseMechanism() (gas: 140325) USDCBridgeMigrator_provideLiquidity:test_onLockReleaseMechanism_thenSwitchToPrimary() (gas: 203331) -USDCBridgeMigrator_releaseOrMint:test_OnLockReleaseMechanism() (gas: 206251) -USDCBridgeMigrator_releaseOrMint:test_incomingMessageWithPrimaryMechanism() (gas: 260440) -USDCBridgeMigrator_releaseOrMint:test_unstickManualTxAfterMigration_destChain() (gas: 142763) -USDCBridgeMigrator_releaseOrMint:test_unstickManualTxAfterMigration_homeChain() (gas: 505520) -USDCBridgeMigrator_updateChainSelectorMechanism:test_PrimaryMechanism() (gas: 130520) -USDCBridgeMigrator_updateChainSelectorMechanism:test_lockOrBurn_then_BurnInCCTPMigration() (gas: 303968) -USDCBridgeMigrator_updateChainSelectorMechanism:test_onLockReleaseMechanism() (gas: 140260) +USDCBridgeMigrator_releaseOrMint:test_OnLockReleaseMechanism() (gas: 206383) +USDCBridgeMigrator_releaseOrMint:test_incomingMessageWithPrimaryMechanism() (gas: 260476) +USDCBridgeMigrator_releaseOrMint:test_unstickManualTxAfterMigration_destChain() (gas: 142853) +USDCBridgeMigrator_releaseOrMint:test_unstickManualTxAfterMigration_homeChain() (gas: 505540) +USDCBridgeMigrator_updateChainSelectorMechanism:test_PrimaryMechanism() (gas: 130502) +USDCBridgeMigrator_updateChainSelectorMechanism:test_lockOrBurn_then_BurnInCCTPMigration() (gas: 303949) +USDCBridgeMigrator_updateChainSelectorMechanism:test_onLockReleaseMechanism() (gas: 140325) USDCBridgeMigrator_updateChainSelectorMechanism:test_onLockReleaseMechanism_thenSwitchToPrimary() (gas: 203312) USDCTokenPool_lockOrBurn:test_LockOrBurn() (gas: 128094) USDCTokenPool_releaseOrMint:test_ReleaseOrMintRealTx() (gas: 260189) diff --git a/contracts/src/v0.8/ccip/pools/BurnMintTokenPoolAbstract.sol b/contracts/src/v0.8/ccip/pools/BurnMintTokenPoolAbstract.sol index 91472a5f5c8..aba9852d639 100644 --- a/contracts/src/v0.8/ccip/pools/BurnMintTokenPoolAbstract.sol +++ b/contracts/src/v0.8/ccip/pools/BurnMintTokenPoolAbstract.sol @@ -35,7 +35,7 @@ abstract contract BurnMintTokenPoolAbstract is TokenPool { /// @dev The _validateReleaseOrMint check is an essential security check function releaseOrMint( Pool.ReleaseOrMintInV1 calldata releaseOrMintIn - ) external virtual override returns (Pool.ReleaseOrMintOutV1 memory) { + ) public virtual override returns (Pool.ReleaseOrMintOutV1 memory) { _validateReleaseOrMint(releaseOrMintIn); // Calculate the local amount diff --git a/contracts/src/v0.8/ccip/pools/BurnToAddressMintTokenPool.sol b/contracts/src/v0.8/ccip/pools/BurnToAddressMintTokenPool.sol new file mode 100644 index 00000000000..563375f099a --- /dev/null +++ b/contracts/src/v0.8/ccip/pools/BurnToAddressMintTokenPool.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.24; + +import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol"; +import {IBurnMintERC20} from "../../shared/token/ERC20/IBurnMintERC20.sol"; + +import {Pool} from "../libraries/Pool.sol"; +import {BurnMintTokenPoolAbstract} from "./BurnMintTokenPoolAbstract.sol"; +import {TokenPool} from "./TokenPool.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"; + +/// @notice This pool mints and burns a 3rd-party token by sending tokens to an address which is unrecoverable. +/// @dev The pool is designed to have an immutable burn address. If the tokens at the burn address become recoverable, +/// for example, a quantum computer calculating a private key for the zero address, the pool will need to be replaced +/// with a new pool with a different burn address. +contract BurnToAddressMintTokenPool is BurnMintTokenPoolAbstract, ITypeAndVersion { + using SafeERC20 for IERC20; + + event OutstandingTokensSet(uint256 newMintedTokenAmount, uint256 oldMintedTokenAmount); + + error InsufficientOutstandingTokens(); + + string public constant override typeAndVersion = "BurnToAddressTokenPool 1.5.1"; + + /// @notice The address where tokens are sent during a call to lockOrBurn, functionally burning but without decreasing + /// total supply. This address is expected to have no ability to recover the tokens sent to it, and will thus be locked forever. + /// This can be either an EOA without a corresponding private key, or a contract which does not have the ability to transfer the tokens. + address public immutable i_burnAddress; + + /// @notice Minted Tokens is a safety mechanism to ensure that more tokens cannot be sent out of the bridge + /// than were originally sent in via CCIP. On incoming messages the value is increased, and on outgoing messages, + /// the value is decreased. For pools with existing tokens in circulation, the value may not be known at deployment + /// time, and thus should be set later using the setoutstandingTokens() function. + uint256 internal s_outstandingTokens; + + /// @dev Since burnAddress is expected to make the tokens unrecoverable, no check for the zero address needs to be + /// performed, as it is a valid input. + constructor( + IBurnMintERC20 token, + uint8 localTokenDecimals, + address[] memory allowlist, + address rmnProxy, + address router, + address burnAddress + ) TokenPool(token, localTokenDecimals, allowlist, rmnProxy, router) { + i_burnAddress = burnAddress; + } + + /// @notice Mint tokens from the pool to the recipient, updating the internal accounting for an outflow of tokens. + /// @dev If the amount of tokens to be + function releaseOrMint( + Pool.ReleaseOrMintInV1 calldata releaseOrMintIn + ) public virtual override returns (Pool.ReleaseOrMintOutV1 memory) { + // When minting tokens, the local outstanding supply increases. These tokens will be burned + // when they are sent back to the pool on an outgoing message. + s_outstandingTokens += releaseOrMintIn.amount; + + return super.releaseOrMint(releaseOrMintIn); + } + + /// @inheritdoc BurnMintTokenPoolAbstract + /// @notice Tokens are burned by sending to an address which can never transfer them, + /// making the tokens unrecoverable without reducing the total supply. + function _burn( + uint256 amount + ) internal virtual override { + if (amount > s_outstandingTokens) { + revert InsufficientOutstandingTokens(); + } + + // When tokens are burned, the amount outstanding decreases. This ensures that more tokens cannot be sent out + // of the bridge than were originally sent in via CCIP. + s_outstandingTokens -= amount; + + getToken().safeTransfer(i_burnAddress, amount); + } + + /// @notice Returns the address where tokens are sent during a call to lockOrBurn + /// @return burnAddress the address which receives the tokens. + function getBurnAddress() public view returns (address burnAddress) { + return i_burnAddress; + } + + /// @notice Return the amount of tokens which were minted by this contract and not yet burned. + /// @return outstandingTokens The amount of tokens which were minted by this token pool and not yet burned. + function getOutstandingTokens() public view returns (uint256 outstandingTokens) { + return s_outstandingTokens; + } + + /// @notice Set the amount of tokens which were minted by this contract and not yet burned. + /// @param amount The new amount of tokens which were minted by this token pool and not yet burned. + function setOutstandingTokens( + uint256 amount + ) external onlyOwner { + uint256 currentOutstandingTokens = s_outstandingTokens; + + s_outstandingTokens = amount; + + emit OutstandingTokensSet(amount, currentOutstandingTokens); + } +} diff --git a/contracts/src/v0.8/ccip/pools/SiloedLockReleaseTokenPool.sol b/contracts/src/v0.8/ccip/pools/SiloedLockReleaseTokenPool.sol new file mode 100644 index 00000000000..adb3a452957 --- /dev/null +++ b/contracts/src/v0.8/ccip/pools/SiloedLockReleaseTokenPool.sol @@ -0,0 +1,299 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.24; + +import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol"; + +import {Pool} from "../libraries/Pool.sol"; +import {TokenPool} from "./TokenPool.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"; + +/// @notice A variation on Lock Release token pools where liquidity is shared among some chains, and stored independently +/// for others. Chains which do not share liquidity are known as siloed chains. +contract SiloedLockReleaseTokenPool is TokenPool, ITypeAndVersion { + using SafeERC20 for IERC20; + + error InsufficientLiquidity(uint256 availableLiquidity, uint256 requestedAmount); + error ChainNotSiloed(uint64 remoteChainSelector); + error InvalidChainSelector(uint64 remoteChainSelector); + + event LiquidityAdded(uint64 remoteChainSelector, address indexed provider, uint256 amount); + event LiquidityRemoved(uint64 remoteChainSelector, address indexed provider, uint256 amount); + event ChainUnsiloed(uint64 remoteChainSelector, uint256 amountUnsiloed); + event ChainSiloed(uint64 remoteChainSelector, address rebalancer); + event SiloRebalancerSet(uint64 indexed remoteChainSelector, address oldRebalancer, address newRebalancer); + event UnsiloedRebalancerSet(address oldRebalancer, address newRebalancer); + + string public constant override typeAndVersion = "SiloedLockReleaseTokenPool 1.6.0-dev"; + + /// @notice The amount of tokens available for remote chains which are not siloed as an additional security precaution. + uint256 internal s_unsiloedTokenBalance; + + /// @notice The rebalancer for unsiloed chains, which can add liquidity to the shared pool. + address internal s_rebalancer; + + struct SiloConfigUpdate { + uint64 remoteChainSelector; + address rebalancer; + } + + struct SiloConfig { + uint256 tokenBalance; // The amount of tokens available for incoming messages, either locked or as liquidity. + address rebalancer; // ─╮ The address allowed to add liquidity for the given siloed chain. + bool isSiloed; // ──────╯ Whether funds should be isolated from all other chains or shared amongst all non-siloed chains. + } + + /// @notice The configuration for each chain that is siloed, or not. By default chains are not siloed. + mapping(uint64 remoteChainSelector => SiloConfig) internal s_chainConfigs; + + constructor( + IERC20 token, + uint8 localTokenDecimals, + address[] memory allowlist, + address rmnProxy, + address router + ) TokenPool(token, localTokenDecimals, allowlist, rmnProxy, router) {} + + /// @notice Locks the token in the pool + /// @dev The _validateLockOrBurn check is an essential security check + function lockOrBurn( + Pool.LockOrBurnInV1 calldata lockOrBurnIn + ) external virtual override returns (Pool.LockOrBurnOutV1 memory) { + _validateLockOrBurn(lockOrBurnIn); + + // If funds need to be siloed, update internal accounting; + if (s_chainConfigs[lockOrBurnIn.remoteChainSelector].isSiloed) { + s_chainConfigs[lockOrBurnIn.remoteChainSelector].tokenBalance += lockOrBurnIn.amount; + } + // If the messages is going to a chain without siloed funds, update state accounting accordingly. + else { + s_unsiloedTokenBalance += lockOrBurnIn.amount; + } + + emit Locked(msg.sender, lockOrBurnIn.amount); + + return Pool.LockOrBurnOutV1({ + destTokenAddress: getRemoteToken(lockOrBurnIn.remoteChainSelector), + destPoolData: _encodeLocalDecimals() + }); + } + + /// @notice Release tokens from the pool to the recipient + /// @dev The _validateReleaseOrMint check is an essential security check + /// @dev If the releaseOrMintIn amount is greater than available liquidity, the function will revert as a security + /// measure to prevent funds from a Silo being released by another chain. + function releaseOrMint( + Pool.ReleaseOrMintInV1 calldata releaseOrMintIn + ) external virtual override returns (Pool.ReleaseOrMintOutV1 memory) { + _validateReleaseOrMint(releaseOrMintIn); + + // Calculate the local amount + uint256 localAmount = + _calculateLocalAmount(releaseOrMintIn.amount, _parseRemoteDecimals(releaseOrMintIn.sourcePoolData)); + + // Save gas by using storage instead of memory as a value may need to be updated. + SiloConfig storage remoteConfig = s_chainConfigs[releaseOrMintIn.remoteChainSelector]; + + // Prevent A silent underflow by explicitly ensuring that enough funds are available to release + uint256 availableLiquidity = remoteConfig.isSiloed ? remoteConfig.tokenBalance : s_unsiloedTokenBalance; + if (localAmount > availableLiquidity) revert InsufficientLiquidity(availableLiquidity, localAmount); + + // Tracking balances independently by chain is a security measure to prevent liquidity for one chain from being + // released by another chain. + if (remoteConfig.isSiloed) { + remoteConfig.tokenBalance -= localAmount; + } else { + s_unsiloedTokenBalance -= localAmount; + } + + // Release to the recipient + getToken().safeTransfer(releaseOrMintIn.receiver, localAmount); + + emit Released(msg.sender, releaseOrMintIn.receiver, localAmount); + + return Pool.ReleaseOrMintOutV1({destinationAmount: localAmount}); + } + + /// @notice Returns whether the tokens locked for a given remote chain should be siloed independently + /// from all other remote chains. + /// @param remoteChainSelector the CCIP specific selector for the remote chain being interacted with. + /// @return isSiloed Whether the funds should be isolated from all the others. + function isSiloed( + uint64 remoteChainSelector + ) external view returns (bool) { + return s_chainConfigs[remoteChainSelector].isSiloed; + } + + /// @notice Returns the amount of tokens in the token pool that were siloed for a specific remote chain selector. + /// @param remoteChainSelector the CCIP specific selector for the remote chain being interacted with. + /// @return lockedTokens The tokens locked into this token pool for the given selector. If the chain is not siloed, + /// the amount will be the amount of liquidity shared among all unsiloed chains. + function getAvailableTokens( + uint64 remoteChainSelector + ) external view returns (uint256 lockedTokens) { + if (s_chainConfigs[remoteChainSelector].isSiloed) { + return s_chainConfigs[remoteChainSelector].tokenBalance; + } + + return s_unsiloedTokenBalance; + } + + /// @notice Returns the amount of tokens in the token pool that are shared among all unsiloed chains. + /// @return unsiloedTokens amount of tokens available to all unsiloed chains. + function getUnsiloedLiquidity() external view returns (uint256) { + return s_unsiloedTokenBalance; + } + + /// @notice Updates designations for chains on whether to mark funds as Siloed or not + /// @param removes A list of chain selectors to disable Siloing. Their funds will be moved into the unsiloed pool. + /// If a chain is not siloed, and attempted to be removed, the function will revert. + /// @param adds A list of chain selectors to enable Siloing. Adding a chain to siloing will not set the rebalancer. + /// The rebalancer will need to be set separately. + function updateSiloDesignations(uint64[] calldata removes, SiloConfigUpdate[] calldata adds) external onlyOwner { + for (uint256 i = 0; i < removes.length; ++i) { + if (!s_chainConfigs[removes[i]].isSiloed) revert ChainNotSiloed(removes[i]); + + // When a chain is removed from siloing, the funds are moved to the accounting pool shared by all unsiloed chain. + uint256 amountUnsiloed = s_chainConfigs[removes[i]].tokenBalance; + + s_unsiloedTokenBalance += amountUnsiloed; + + delete s_chainConfigs[removes[i]]; + + // Emit a removal event which includes the amount of funds moved to the general silo. + emit ChainUnsiloed(removes[i], amountUnsiloed); + } + + for (uint256 i = 0; i < adds.length; ++i) { + // Since the zero chain selector is used to designate unsiloed chains, it should never be used for siloed chains. + if (adds[i].remoteChainSelector == 0) { + revert InvalidChainSelector(0); + } + + SiloConfig memory newConfig = SiloConfig({tokenBalance: 0, rebalancer: adds[i].rebalancer, isSiloed: true}); + + s_chainConfigs[adds[i].remoteChainSelector] = newConfig; + + emit ChainSiloed(adds[i].remoteChainSelector, adds[i].rebalancer); + } + } + + /// @notice Gets the rebalancer able to provide liquidity for a remote chain selector + /// @param remoteChainSelector The CCIP specific selector for the remote chain being interacted with. + /// @return The current liquidity manager, contract owner if the chain's funds are not siloed. + function getSiloRebalancer( + uint64 remoteChainSelector + ) public view returns (address) { + SiloConfig memory remoteConfig = s_chainConfigs[remoteChainSelector]; + if (remoteConfig.isSiloed) { + return remoteConfig.rebalancer; + } + + return s_rebalancer; + } + + /// @notice Sets the Rebalancer address for a given remoteChainSelector. + /// @dev Only callable by the owner. + /// @param remoteChainSelector the remote chain to set. + /// @param newRebalancer the address allowed to add liquidity for the given siloed chain. + function setSiloRebalancer(uint64 remoteChainSelector, address newRebalancer) external onlyOwner { + SiloConfig memory remoteConfig = s_chainConfigs[remoteChainSelector]; + + if (!remoteConfig.isSiloed) revert ChainNotSiloed(remoteChainSelector); + + address oldRebalancer = remoteConfig.rebalancer; + + s_chainConfigs[remoteChainSelector].rebalancer = newRebalancer; + + emit SiloRebalancerSet(remoteChainSelector, newRebalancer, oldRebalancer); + } + + /// @notice Sets the Rebalancer address for unsiloed chains. + /// @dev Only callable by the owner. + /// @param newRebalancer the address allowed to add liquidity for the given siloed chain. + function setRebalancer( + address newRebalancer + ) external onlyOwner { + address oldRebalancer = s_rebalancer; + + s_rebalancer = newRebalancer; + + emit UnsiloedRebalancerSet(newRebalancer, oldRebalancer); + } + + /// @notice Adds liquidity to the pool. The tokens should be approved first. + /// @param remoteChainSelector the remote chain to set. If the chain is not siloed, the liquidity will be shared among all + /// non-siloed chains. + /// @param amount The amount of liquidity to provide. + /// @dev Only the rebalancer for the chain can add liquidity + function provideSiloedLiquidity(uint64 remoteChainSelector, uint256 amount) external { + _provideLiquidity(remoteChainSelector, amount); + } + + /// @notice Adds liquidity to the pool for unsiloed chains. Function is used to support legacy liquidity operations + /// by using a function selector available to previous L/R pools. + /// @dev Since the remoteChainSelector 0 should never be applied to a real chain, it is used to designate unsiloed chains. + /// @param amount The amount of liquidity to provide. + function provideLiquidity( + uint256 amount + ) external { + _provideLiquidity(0, amount); + } + + function _provideLiquidity(uint64 remoteChainSelector, uint256 amount) internal { + if (msg.sender != getSiloRebalancer(remoteChainSelector)) revert Unauthorized(msg.sender); + + // Storage is used instead of memory to save gas, as the state may need to be updated if the chain is siloed. + SiloConfig storage remoteConfig = s_chainConfigs[remoteChainSelector]; + + if (remoteConfig.isSiloed) { + remoteConfig.tokenBalance += amount; + } else { + s_unsiloedTokenBalance += amount; + } + + i_token.safeTransferFrom(msg.sender, address(this), amount); + emit LiquidityAdded(remoteChainSelector, msg.sender, amount); + } + + /// @notice Removes liquidity from the pool for unsiloed chains. Function is used to support legacy liquidity operations + /// by using a function selector available to previous L/R pools. + /// @dev Since the remoteChainSelector 0 should never be applied to a real chain, it is used to designate unsiloed chains. + /// @param amount The amount of liquidity to remove. + function withdrawLiquidity( + uint256 amount + ) external { + _withdrawLiquidity(0, amount); + } + + /// @notice Removed liquidity to the pool. The tokens will be sent to msg.sender. + /// @dev Only the rebalancer can remove liquidity from the contract, for both siloed and unsiloed chains. + /// @param remoteChainSelector the remote chain to set. If the chain is not siloed, then no accounting will be updated, + /// which can be considered the liquidity for all non-siloed chains sharing liquidity. + /// @param amount The amount of liquidity to remove. + function withdrawSiloedLiquidity(uint64 remoteChainSelector, uint256 amount) external { + _withdrawLiquidity(remoteChainSelector, amount); + } + + function _withdrawLiquidity(uint64 remoteChainSelector, uint256 amount) internal { + if (msg.sender != getSiloRebalancer(remoteChainSelector)) revert Unauthorized(msg.sender); + + // Save gas by using storage as multiple values may need to be read/written. + SiloConfig storage remoteConfig = s_chainConfigs[remoteChainSelector]; + + // Prevent A silent underflow by explicitly ensuring that enough funds are available to withdraw + uint256 availableLiquidity = remoteConfig.isSiloed ? remoteConfig.tokenBalance : s_unsiloedTokenBalance; + if (amount > availableLiquidity) revert InsufficientLiquidity(availableLiquidity, amount); + + // If funds are siloed by chain, prevent more than has been locked from being removed from the token pool. + if (remoteConfig.isSiloed) { + remoteConfig.tokenBalance -= amount; + } else { + s_unsiloedTokenBalance -= amount; + } + + i_token.safeTransfer(msg.sender, amount); + emit LiquidityRemoved(remoteChainSelector, msg.sender, amount); + } +} diff --git a/contracts/src/v0.8/ccip/pools/USDC/HybridLockReleaseUSDCTokenPool.sol b/contracts/src/v0.8/ccip/pools/USDC/HybridLockReleaseUSDCTokenPool.sol index 89e609fcc5e..3f94a1b719a 100644 --- a/contracts/src/v0.8/ccip/pools/USDC/HybridLockReleaseUSDCTokenPool.sol +++ b/contracts/src/v0.8/ccip/pools/USDC/HybridLockReleaseUSDCTokenPool.sol @@ -4,7 +4,6 @@ pragma solidity ^0.8.24; import {ILiquidityContainer} from "../../../liquiditymanager/interfaces/ILiquidityContainer.sol"; import {ITokenMessenger} from "../USDC/ITokenMessenger.sol"; -import {Ownable2StepMsgSender} from "../../../shared/access/Ownable2StepMsgSender.sol"; import {Pool} from "../../libraries/Pool.sol"; import {TokenPool} from "../TokenPool.sol"; import {USDCTokenPool} from "../USDC/USDCTokenPool.sol"; @@ -209,31 +208,6 @@ contract HybridLockReleaseUSDCTokenPool is USDCTokenPool, USDCBridgeMigrator { 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 must be the owner of the old pool. Since the pool uses two-step ownership transfer, the old pool must - /// first propose the ownership transfer, and then this pool must accept it. This function can only be called after - /// the ownership transfer has been proposed, as it will accept it and then make the call to withdrawLiquidity - /// @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. - /// @param from The address of the old pool. - /// @param remoteChainSelector The chain for which liquidity is being transferred. - function transferLiquidity(address from, uint64 remoteChainSelector) external onlyOwner { - Ownable2StepMsgSender(from).acceptOwnership(); - - // Withdraw all available liquidity from the old pool. No check is needed for pending migrations, as the old pool - // will revert if the migration has begun. - uint256 withdrawAmount = HybridLockReleaseUSDCTokenPool(from).getLockedTokensForChain(remoteChainSelector); - HybridLockReleaseUSDCTokenPool(from).withdrawLiquidity(remoteChainSelector, withdrawAmount); - - s_lockedTokensByChainSelector[remoteChainSelector] += withdrawAmount; - - emit LiquidityTransferred(from, remoteChainSelector, withdrawAmount); - } - // ================================================================ // │ Alt Mechanism Logic | // ================================================================ diff --git a/contracts/src/v0.8/ccip/test/helpers/MaybeRevertingBurnMintTokenPool.sol b/contracts/src/v0.8/ccip/test/helpers/MaybeRevertingBurnMintTokenPool.sol index 058398e4c08..2ec5219b36c 100644 --- a/contracts/src/v0.8/ccip/test/helpers/MaybeRevertingBurnMintTokenPool.sol +++ b/contracts/src/v0.8/ccip/test/helpers/MaybeRevertingBurnMintTokenPool.sol @@ -60,7 +60,7 @@ contract MaybeRevertingBurnMintTokenPool is BurnMintTokenPool { /// @notice Reverts depending on the value of `s_revertReason` function releaseOrMint( Pool.ReleaseOrMintInV1 calldata releaseOrMintIn - ) external virtual override returns (Pool.ReleaseOrMintOutV1 memory) { + ) public virtual override returns (Pool.ReleaseOrMintOutV1 memory) { _validateReleaseOrMint(releaseOrMintIn); bytes memory revertReason = s_revertReason; diff --git a/contracts/src/v0.8/ccip/test/pools/BurnToAddressMintTokenPool/BurnToAddressMintTokenPool.lockOrBurn.t.sol b/contracts/src/v0.8/ccip/test/pools/BurnToAddressMintTokenPool/BurnToAddressMintTokenPool.lockOrBurn.t.sol new file mode 100644 index 00000000000..c5e559b2d9e --- /dev/null +++ b/contracts/src/v0.8/ccip/test/pools/BurnToAddressMintTokenPool/BurnToAddressMintTokenPool.lockOrBurn.t.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.24; + +import {Pool} from "../../../libraries/Pool.sol"; +import {BurnToAddressMintTokenPool} from "../../../pools/BurnToAddressMintTokenPool.sol"; +import {BurnToAddressMintTokenPoolSetup} from "./BurnToAddressMintTokenPoolSetup.t.sol"; + +import {IERC20} from "../../../../vendor/openzeppelin-solidity/v4.8.3/contracts/interfaces/IERC20.sol"; + +contract BurnToAddressMintTokenPool_lockOrBurn is BurnToAddressMintTokenPoolSetup { + uint256 public constant AMOUNT = 1e24; + + function test_LockOrBurn() public { + s_pool.setOutstandingTokens(AMOUNT); + + deal(address(s_burnMintERC20), address(s_pool), AMOUNT); + assertEq(s_burnMintERC20.balanceOf(address(s_pool)), AMOUNT); + + vm.startPrank(s_burnMintOnRamp); + + vm.expectEmit(); + emit IERC20.Transfer(address(s_pool), BURN_ADDRESS, AMOUNT); + + vm.expectCall(address(s_burnMintERC20), abi.encodeWithSelector(IERC20.transfer.selector, BURN_ADDRESS, AMOUNT)); + + s_pool.lockOrBurn( + Pool.LockOrBurnInV1({ + originalSender: OWNER, + receiver: bytes(""), + amount: AMOUNT, + remoteChainSelector: DEST_CHAIN_SELECTOR, + localToken: address(s_burnMintERC20) + }) + ); + + assertEq(s_burnMintERC20.balanceOf(s_pool.getBurnAddress()), AMOUNT); + assertEq(s_burnMintERC20.balanceOf(address(s_pool)), 0); + assertEq(s_pool.getOutstandingTokens(), 0); + } + + // Reverts + + function test_LockOrBurn_RevertWhen_InsufficientOutstandingTokens() public { + s_pool.setOutstandingTokens(AMOUNT - 1); + + deal(address(s_burnMintERC20), address(s_pool), AMOUNT); + assertEq(s_burnMintERC20.balanceOf(address(s_pool)), AMOUNT); + + vm.startPrank(s_burnMintOnRamp); + + vm.expectRevert(BurnToAddressMintTokenPool.InsufficientOutstandingTokens.selector); + + s_pool.lockOrBurn( + Pool.LockOrBurnInV1({ + originalSender: OWNER, + receiver: bytes(""), + amount: AMOUNT, + remoteChainSelector: DEST_CHAIN_SELECTOR, + localToken: address(s_burnMintERC20) + }) + ); + } +} diff --git a/contracts/src/v0.8/ccip/test/pools/BurnToAddressMintTokenPool/BurnToAddressMintTokenPool.releaseOrMint.t.sol b/contracts/src/v0.8/ccip/test/pools/BurnToAddressMintTokenPool/BurnToAddressMintTokenPool.releaseOrMint.t.sol new file mode 100644 index 00000000000..508ed6bb787 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/pools/BurnToAddressMintTokenPool/BurnToAddressMintTokenPool.releaseOrMint.t.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.24; + +import {Pool} from "../../../libraries/Pool.sol"; +import {BurnToAddressMintTokenPoolSetup} from "./BurnToAddressMintTokenPoolSetup.t.sol"; + +import {IERC20} from "../../../../vendor/openzeppelin-solidity/v4.8.3/contracts/interfaces/IERC20.sol"; + +contract BurnToAddressMintTokenPool_releaseOrMint is BurnToAddressMintTokenPoolSetup { + function test_releaseOrMint() public { + uint256 amount = 1e24; + address receiver = makeAddr("RECEIVER_ADDRESS"); + + vm.startPrank(s_burnMintOffRamp); + + vm.expectEmit(); + emit IERC20.Transfer(address(0), receiver, amount); + + s_pool.releaseOrMint( + Pool.ReleaseOrMintInV1({ + originalSender: bytes(""), + receiver: receiver, + amount: amount, + localToken: address(s_burnMintERC20), + remoteChainSelector: DEST_CHAIN_SELECTOR, + sourcePoolAddress: abi.encode(s_remoteBurnMintPool), + sourcePoolData: "", + offchainTokenData: "" + }) + ); + + assertEq(s_burnMintERC20.balanceOf(receiver), amount); + assertEq(s_pool.getOutstandingTokens(), amount); + } +} diff --git a/contracts/src/v0.8/ccip/test/pools/BurnToAddressMintTokenPool/BurnToAddressMintTokenPool.setMintedTokens.t.sol b/contracts/src/v0.8/ccip/test/pools/BurnToAddressMintTokenPool/BurnToAddressMintTokenPool.setMintedTokens.t.sol new file mode 100644 index 00000000000..11c615d8b2b --- /dev/null +++ b/contracts/src/v0.8/ccip/test/pools/BurnToAddressMintTokenPool/BurnToAddressMintTokenPool.setMintedTokens.t.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.24; + +import {BurnToAddressMintTokenPool} from "../../../pools/BurnToAddressMintTokenPool.sol"; +import {BurnToAddressMintTokenPoolSetup} from "./BurnToAddressMintTokenPoolSetup.t.sol"; + +contract BurnToAddressMintTokenPool_setOutstandingokens is BurnToAddressMintTokenPoolSetup { + function test_setOutstandingTokens() public { + uint256 amount = 1e18; + + assertEq(s_pool.getOutstandingTokens(), 0); + + vm.expectEmit(); + emit BurnToAddressMintTokenPool.OutstandingTokensSet(amount, 0); + + s_pool.setOutstandingTokens(amount); + + assertEq(s_pool.getOutstandingTokens(), amount); + } +} diff --git a/contracts/src/v0.8/ccip/test/pools/BurnToAddressMintTokenPool/BurnToAddressMintTokenPoolSetup.t.sol b/contracts/src/v0.8/ccip/test/pools/BurnToAddressMintTokenPool/BurnToAddressMintTokenPoolSetup.t.sol new file mode 100644 index 00000000000..a27c0a0f8c4 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/pools/BurnToAddressMintTokenPool/BurnToAddressMintTokenPoolSetup.t.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.24; + +import {BurnToAddressMintTokenPool} from "../../../pools/BurnToAddressMintTokenPool.sol"; +import {BurnMintSetup} from "../BurnMintTokenPool/BurnMintSetup.t.sol"; + +contract BurnToAddressMintTokenPoolSetup is BurnMintSetup { + BurnToAddressMintTokenPool internal s_pool; + + address public constant BURN_ADDRESS = address(0xdead); + + function setUp() public virtual override { + BurnMintSetup.setUp(); + + s_pool = new BurnToAddressMintTokenPool( + s_burnMintERC20, + DEFAULT_TOKEN_DECIMALS, + new address[](0), + address(s_mockRMNRemote), + address(s_sourceRouter), + BURN_ADDRESS + ); + + s_burnMintERC20.grantMintAndBurnRoles(address(s_pool)); + + _applyChainUpdates(address(s_pool)); + } +} diff --git a/contracts/src/v0.8/ccip/test/pools/SiloedLockReleaseTokenPool/SiloedLockReleaseTokenPool.lockOrBurn.t.sol b/contracts/src/v0.8/ccip/test/pools/SiloedLockReleaseTokenPool/SiloedLockReleaseTokenPool.lockOrBurn.t.sol new file mode 100644 index 00000000000..2cf51a045e9 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/pools/SiloedLockReleaseTokenPool/SiloedLockReleaseTokenPool.lockOrBurn.t.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.24; + +import {Pool} from "../../../libraries/Pool.sol"; +import {RateLimiter} from "../../../libraries/RateLimiter.sol"; +import {TokenPool} from "../../../pools/TokenPool.sol"; +import {SiloedLockReleaseTokenPoolSetup} from "./SiloedLockReleaseTokenPoolSetup.t.sol"; + +contract SiloedLockReleaseTokenPool_lockOrBurn is SiloedLockReleaseTokenPoolSetup { + uint256 public constant AMOUNT = 10e18; + + function test_lockOrBurn_SiloedFunds() public { + assertTrue(s_siloedLockReleaseTokenPool.isSiloed(SILOED_CHAIN_SELECTOR)); + + vm.startPrank(s_allowedOnRamp); + + vm.expectEmit(); + emit RateLimiter.TokensConsumed(AMOUNT); + vm.expectEmit(); + emit TokenPool.Locked(s_allowedOnRamp, AMOUNT); + + s_siloedLockReleaseTokenPool.lockOrBurn( + Pool.LockOrBurnInV1({ + originalSender: STRANGER, + receiver: bytes(""), + amount: AMOUNT, + remoteChainSelector: SILOED_CHAIN_SELECTOR, + localToken: address(s_token) + }) + ); + + assertEq(s_siloedLockReleaseTokenPool.getAvailableTokens(SILOED_CHAIN_SELECTOR), AMOUNT); + } + + function test_lockOrBurn_UnsiloedFunds() public { + vm.startPrank(s_allowedOnRamp); + + assertFalse(s_siloedLockReleaseTokenPool.isSiloed(DEST_CHAIN_SELECTOR)); + + vm.expectEmit(); + emit RateLimiter.TokensConsumed(AMOUNT); + vm.expectEmit(); + emit TokenPool.Locked(s_allowedOnRamp, AMOUNT); + + s_siloedLockReleaseTokenPool.lockOrBurn( + Pool.LockOrBurnInV1({ + originalSender: STRANGER, + receiver: bytes(""), + amount: AMOUNT, + remoteChainSelector: DEST_CHAIN_SELECTOR, + localToken: address(s_token) + }) + ); + + assertEq(s_siloedLockReleaseTokenPool.getAvailableTokens(DEST_CHAIN_SELECTOR), AMOUNT); + } +} diff --git a/contracts/src/v0.8/ccip/test/pools/SiloedLockReleaseTokenPool/SiloedLockReleaseTokenPool.provideLiquidity.t.sol b/contracts/src/v0.8/ccip/test/pools/SiloedLockReleaseTokenPool/SiloedLockReleaseTokenPool.provideLiquidity.t.sol new file mode 100644 index 00000000000..6af10971018 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/pools/SiloedLockReleaseTokenPool/SiloedLockReleaseTokenPool.provideLiquidity.t.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.24; + +import {SiloedLockReleaseTokenPool} from "../../../pools/SiloedLockReleaseTokenPool.sol"; +import {TokenPool} from "../../../pools/TokenPool.sol"; +import {SiloedLockReleaseTokenPoolSetup} from "./SiloedLockReleaseTokenPoolSetup.t.sol"; + +contract SiloedLockReleaseTokenPool_provideLiqudity is SiloedLockReleaseTokenPoolSetup { + address public UNAUTHORIZED_ADDRESS = address(0xdeadbeef); + + function setUp() public override { + super.setUp(); + + s_siloedLockReleaseTokenPool.setSiloRebalancer(SILOED_CHAIN_SELECTOR, OWNER); + } + + function test_ProvideLiquidity_UnsiloedChain() public { + uint256 amount = 1e24; + + vm.expectEmit(); + emit SiloedLockReleaseTokenPool.LiquidityAdded(DEST_CHAIN_SELECTOR, OWNER, amount); + + s_siloedLockReleaseTokenPool.provideSiloedLiquidity(DEST_CHAIN_SELECTOR, amount); + + assertEq(s_token.balanceOf(address(s_siloedLockReleaseTokenPool)), amount); + + // Since the funds for the destination chain are not siloed, + // the locked token amount should not be increased + assertEq(s_siloedLockReleaseTokenPool.getAvailableTokens(DEST_CHAIN_SELECTOR), amount); + assertEq(s_siloedLockReleaseTokenPool.getUnsiloedLiquidity(), amount); + } + + function test_ProvideLiquidity_SiloedChain() public { + uint256 amount = 1e24; + + vm.expectEmit(); + emit SiloedLockReleaseTokenPool.LiquidityAdded(SILOED_CHAIN_SELECTOR, OWNER, amount); + + s_siloedLockReleaseTokenPool.provideSiloedLiquidity(SILOED_CHAIN_SELECTOR, amount); + + assertEq(s_token.balanceOf(address(s_siloedLockReleaseTokenPool)), amount); + + assertEq(s_siloedLockReleaseTokenPool.getAvailableTokens(SILOED_CHAIN_SELECTOR), amount); + + // Since the funds for the destination chain are not siloed, the locked token amount should not be increased + assertEq(s_siloedLockReleaseTokenPool.getUnsiloedLiquidity(), 0); + } + + function test_ProvideLiquidity_LegacyProvideLiquiditySelector() public { + uint256 amount = 1e24; + + vm.expectEmit(); + emit SiloedLockReleaseTokenPool.LiquidityAdded(0, OWNER, amount); + + s_siloedLockReleaseTokenPool.provideLiquidity(amount); + + assertEq(s_token.balanceOf(address(s_siloedLockReleaseTokenPool)), amount); + + // Since the funds for the destination chain are not siloed, + // the locked token amount should not be increased + assertEq(s_siloedLockReleaseTokenPool.getAvailableTokens(DEST_CHAIN_SELECTOR), amount); + assertEq(s_siloedLockReleaseTokenPool.getUnsiloedLiquidity(), amount); + assertEq(s_siloedLockReleaseTokenPool.getAvailableTokens(SILOED_CHAIN_SELECTOR), 0); + } + + // Reverts + + function test_ProvideLiquidity_RevertWhen_UnauthorizedForSiloedChain() public { + vm.startPrank(UNAUTHORIZED_ADDRESS); + + vm.expectRevert(abi.encodeWithSelector(TokenPool.Unauthorized.selector, UNAUTHORIZED_ADDRESS)); + + s_siloedLockReleaseTokenPool.provideSiloedLiquidity(SILOED_CHAIN_SELECTOR, 1); + } + + function test_ProvideLiquidity_RevertWhen_UnauthorizedForUnsiloedChain() public { + vm.startPrank(UNAUTHORIZED_ADDRESS); + + vm.expectRevert(abi.encodeWithSelector(TokenPool.Unauthorized.selector, UNAUTHORIZED_ADDRESS)); + + s_siloedLockReleaseTokenPool.provideSiloedLiquidity(DEST_CHAIN_SELECTOR, 1); + } + + function test_ProvideLiquidity_RevertWhen_LegacyFunctionSelector_Unauthorized() public { + vm.startPrank(UNAUTHORIZED_ADDRESS); + + vm.expectRevert(abi.encodeWithSelector(TokenPool.Unauthorized.selector, UNAUTHORIZED_ADDRESS)); + + s_siloedLockReleaseTokenPool.provideLiquidity(1); + } +} diff --git a/contracts/src/v0.8/ccip/test/pools/SiloedLockReleaseTokenPool/SiloedLockReleaseTokenPool.releaseOrMint.t.sol b/contracts/src/v0.8/ccip/test/pools/SiloedLockReleaseTokenPool/SiloedLockReleaseTokenPool.releaseOrMint.t.sol new file mode 100644 index 00000000000..eec0503b716 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/pools/SiloedLockReleaseTokenPool/SiloedLockReleaseTokenPool.releaseOrMint.t.sol @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.24; + +import {Pool} from "../../../libraries/Pool.sol"; +import {SiloedLockReleaseTokenPool} from "../../../pools/SiloedLockReleaseTokenPool.sol"; +import {SiloedLockReleaseTokenPoolSetup} from "./SiloedLockReleaseTokenPoolSetup.t.sol"; + +import {IERC20} from "../../../../vendor/openzeppelin-solidity/v4.8.3/contracts/interfaces/IERC20.sol"; + +contract SiloedLockReleaseTokenPool_releaseOrMint is SiloedLockReleaseTokenPoolSetup { + function test_ReleaseOrMint_SiloedChain() public { + uint256 amount = 10e18; + + deal(address(s_token), address(s_siloedLockReleaseTokenPool), amount); + vm.startPrank(s_allowedOnRamp); + + // Lock funds so that they can be released without underflowing the internal accounting + s_siloedLockReleaseTokenPool.lockOrBurn( + Pool.LockOrBurnInV1({ + originalSender: STRANGER, + receiver: bytes(""), + amount: amount, + remoteChainSelector: SILOED_CHAIN_SELECTOR, + localToken: address(s_token) + }) + ); + + assertEq(s_siloedLockReleaseTokenPool.getAvailableTokens(SILOED_CHAIN_SELECTOR), amount); + + vm.startPrank(s_allowedOffRamp); + + vm.expectEmit(); + emit IERC20.Transfer(address(s_siloedLockReleaseTokenPool), OWNER, amount); + + s_siloedLockReleaseTokenPool.releaseOrMint( + Pool.ReleaseOrMintInV1({ + originalSender: bytes(""), + receiver: OWNER, + amount: amount, + localToken: address(s_token), + remoteChainSelector: SILOED_CHAIN_SELECTOR, + sourcePoolAddress: abi.encode(s_siloedDestPoolAddress), + sourcePoolData: "", + offchainTokenData: "" + }) + ); + + assertEq(s_siloedLockReleaseTokenPool.getAvailableTokens(SILOED_CHAIN_SELECTOR), 0); + } + + function test_ReleaseOrMint_UnsiloedChain() public { + uint256 amount = 10e18; + + deal(address(s_token), address(s_siloedLockReleaseTokenPool), amount); + vm.startPrank(s_allowedOnRamp); + + // Lock funds for unsiloed chain so they can be released later + s_siloedLockReleaseTokenPool.lockOrBurn( + Pool.LockOrBurnInV1({ + originalSender: STRANGER, + receiver: bytes(""), + amount: amount, + remoteChainSelector: SOURCE_CHAIN_SELECTOR, + localToken: address(s_token) + }) + ); + + assertEq(s_siloedLockReleaseTokenPool.getAvailableTokens(SOURCE_CHAIN_SELECTOR), amount); + assertEq(s_siloedLockReleaseTokenPool.getUnsiloedLiquidity(), amount); + + vm.startPrank(s_allowedOffRamp); + + vm.expectEmit(); + emit IERC20.Transfer(address(s_siloedLockReleaseTokenPool), OWNER, amount); + + s_siloedLockReleaseTokenPool.releaseOrMint( + Pool.ReleaseOrMintInV1({ + originalSender: bytes(""), + receiver: OWNER, + amount: amount, + localToken: address(s_token), + remoteChainSelector: SOURCE_CHAIN_SELECTOR, + sourcePoolAddress: abi.encode(s_siloedDestPoolAddress), + sourcePoolData: "", + offchainTokenData: "" + }) + ); + + assertEq(s_siloedLockReleaseTokenPool.getAvailableTokens(SOURCE_CHAIN_SELECTOR), 0); + assertEq(s_siloedLockReleaseTokenPool.getUnsiloedLiquidity(), 0); + } + + // Reverts + + function test_ReleaseOrMint_RevertsWhen_InsufficientLiquidity_SiloedChain() public { + uint256 releaseAmount = 10e18; + uint256 liquidityAmount = releaseAmount - 1; + + s_siloedLockReleaseTokenPool.provideSiloedLiquidity(SILOED_CHAIN_SELECTOR, liquidityAmount); + + // Since amount to release is greater than provided liquidity, the function should revert + vm.expectRevert( + abi.encodeWithSelector(SiloedLockReleaseTokenPool.InsufficientLiquidity.selector, liquidityAmount, releaseAmount) + ); + + vm.startPrank(s_allowedOffRamp); + + s_siloedLockReleaseTokenPool.releaseOrMint( + Pool.ReleaseOrMintInV1({ + originalSender: bytes(""), + receiver: OWNER, + amount: releaseAmount, + localToken: address(s_token), + remoteChainSelector: SILOED_CHAIN_SELECTOR, + sourcePoolAddress: abi.encode(s_siloedDestPoolAddress), + sourcePoolData: "", + offchainTokenData: "" + }) + ); + } + + function test_ReleaseOrMint_RevertsWhen_InsufficientLiquidity_UnsiloedChain() public { + uint256 releaseAmount = 10e18; + uint256 liquidityAmount = releaseAmount - 1; + + // Call the provide liquidity function which provides to unsiloed chains. + s_siloedLockReleaseTokenPool.provideLiquidity(liquidityAmount); + + // Since amount to release is greater than provided liquidity, the function should revert + vm.expectRevert( + abi.encodeWithSelector(SiloedLockReleaseTokenPool.InsufficientLiquidity.selector, liquidityAmount, releaseAmount) + ); + + vm.startPrank(s_allowedOffRamp); + + s_siloedLockReleaseTokenPool.releaseOrMint( + Pool.ReleaseOrMintInV1({ + originalSender: bytes(""), + receiver: OWNER, + amount: releaseAmount, + localToken: address(s_token), + remoteChainSelector: SOURCE_CHAIN_SELECTOR, + sourcePoolAddress: abi.encode(s_siloedDestPoolAddress), + sourcePoolData: "", + offchainTokenData: "" + }) + ); + } +} diff --git a/contracts/src/v0.8/ccip/test/pools/SiloedLockReleaseTokenPool/SiloedLockReleaseTokenPool.setRebalancer.t.sol b/contracts/src/v0.8/ccip/test/pools/SiloedLockReleaseTokenPool/SiloedLockReleaseTokenPool.setRebalancer.t.sol new file mode 100644 index 00000000000..ea676d53015 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/pools/SiloedLockReleaseTokenPool/SiloedLockReleaseTokenPool.setRebalancer.t.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.24; + +import {SiloedLockReleaseTokenPool} from "../../../pools/SiloedLockReleaseTokenPool.sol"; +import {SiloedLockReleaseTokenPoolSetup} from "./SiloedLockReleaseTokenPoolSetup.t.sol"; + +contract SiloedLockReleaseTokenPool_setRebalancer is SiloedLockReleaseTokenPoolSetup { + address public constant REBALANCER_ADDRESS = address(0xdeadbeef); + + function test_setSiloRebalancer() public { + vm.expectEmit(); + emit SiloedLockReleaseTokenPool.SiloRebalancerSet(SILOED_CHAIN_SELECTOR, REBALANCER_ADDRESS, OWNER); + + s_siloedLockReleaseTokenPool.setSiloRebalancer(SILOED_CHAIN_SELECTOR, REBALANCER_ADDRESS); + + assertEq(s_siloedLockReleaseTokenPool.getSiloRebalancer(SILOED_CHAIN_SELECTOR), REBALANCER_ADDRESS); + assertEq(s_siloedLockReleaseTokenPool.getSiloRebalancer(DEST_CHAIN_SELECTOR), OWNER); + } + + function test_setRebalancer_UnsiloedChains() public { + vm.expectEmit(); + emit SiloedLockReleaseTokenPool.UnsiloedRebalancerSet(REBALANCER_ADDRESS, OWNER); + + s_siloedLockReleaseTokenPool.setRebalancer(REBALANCER_ADDRESS); + + assertEq(s_siloedLockReleaseTokenPool.getSiloRebalancer(DEST_CHAIN_SELECTOR), REBALANCER_ADDRESS); + } + + // Reverts + + function test_setSiloRebalancer_RevertWhen_ChainNotSiloed() public { + vm.expectRevert(abi.encodeWithSelector(SiloedLockReleaseTokenPool.ChainNotSiloed.selector, DEST_CHAIN_SELECTOR)); + + s_siloedLockReleaseTokenPool.setSiloRebalancer(DEST_CHAIN_SELECTOR, REBALANCER_ADDRESS); + } +} diff --git a/contracts/src/v0.8/ccip/test/pools/SiloedLockReleaseTokenPool/SiloedLockReleaseTokenPool.updateSiloDesignations.t.sol b/contracts/src/v0.8/ccip/test/pools/SiloedLockReleaseTokenPool/SiloedLockReleaseTokenPool.updateSiloDesignations.t.sol new file mode 100644 index 00000000000..506b1373ceb --- /dev/null +++ b/contracts/src/v0.8/ccip/test/pools/SiloedLockReleaseTokenPool/SiloedLockReleaseTokenPool.updateSiloDesignations.t.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.24; + +import {SiloedLockReleaseTokenPool} from "../../../pools/SiloedLockReleaseTokenPool.sol"; +import {SiloedLockReleaseTokenPoolSetup} from "./SiloedLockReleaseTokenPoolSetup.t.sol"; + +contract SiloedLockReleaseTokenPool_updateSiloDesignations is SiloedLockReleaseTokenPoolSetup { + function test_updateSiloDesignations() public { + uint256 amount = 1e18; + + SiloedLockReleaseTokenPool.SiloConfigUpdate[] memory chainSelectors = + new SiloedLockReleaseTokenPool.SiloConfigUpdate[](1); + + chainSelectors[0] = + SiloedLockReleaseTokenPool.SiloConfigUpdate({remoteChainSelector: SILOED_CHAIN_SELECTOR, rebalancer: OWNER}); + + vm.expectEmit(); + emit SiloedLockReleaseTokenPool.ChainSiloed(SILOED_CHAIN_SELECTOR, OWNER); + + s_siloedLockReleaseTokenPool.updateSiloDesignations(new uint64[](0), chainSelectors); + + // Assert that the funds are siloed correctly + assertTrue(s_siloedLockReleaseTokenPool.isSiloed(SILOED_CHAIN_SELECTOR)); + assertEq(s_siloedLockReleaseTokenPool.getAvailableTokens(SILOED_CHAIN_SELECTOR), 0); + assertEq(s_siloedLockReleaseTokenPool.getSiloRebalancer(SILOED_CHAIN_SELECTOR), OWNER); + + // Provide some Liquidity so that we can then check that it gets removed. + s_siloedLockReleaseTokenPool.setSiloRebalancer(SILOED_CHAIN_SELECTOR, OWNER); + s_siloedLockReleaseTokenPool.provideSiloedLiquidity(SILOED_CHAIN_SELECTOR, amount); + assertEq(s_siloedLockReleaseTokenPool.getAvailableTokens(SILOED_CHAIN_SELECTOR), amount); + + vm.expectEmit(); + emit SiloedLockReleaseTokenPool.ChainUnsiloed(SILOED_CHAIN_SELECTOR, amount); + + assertEq(s_siloedLockReleaseTokenPool.getUnsiloedLiquidity(), 0); + + uint64[] memory removableChainSelectors = new uint64[](1); + removableChainSelectors[0] = SILOED_CHAIN_SELECTOR; + + s_siloedLockReleaseTokenPool.updateSiloDesignations( + removableChainSelectors, new SiloedLockReleaseTokenPool.SiloConfigUpdate[](0) + ); + + // Check that the locked funds accounting was cleared when the funds were un-siloed. + assertFalse(s_siloedLockReleaseTokenPool.isSiloed(SILOED_CHAIN_SELECTOR)); + assertEq(s_siloedLockReleaseTokenPool.getAvailableTokens(SILOED_CHAIN_SELECTOR), amount); + + // Assert that the available liquidity moved from being siloed to unsiloed. + assertEq(s_siloedLockReleaseTokenPool.getUnsiloedLiquidity(), amount); + } + + // Reverts + + function test_updateSiloDesignations_RevertWhen_ChainNotSiloed() public { + uint64[] memory removableChainSelectors = new uint64[](1); + removableChainSelectors[0] = DEST_CHAIN_SELECTOR; + + vm.expectRevert(abi.encodeWithSelector(SiloedLockReleaseTokenPool.ChainNotSiloed.selector, DEST_CHAIN_SELECTOR)); + + s_siloedLockReleaseTokenPool.updateSiloDesignations( + removableChainSelectors, new SiloedLockReleaseTokenPool.SiloConfigUpdate[](0) + ); + } + + function test_updateSiloDesignations_RevertWhen_InvalidChainSelector() public { + SiloedLockReleaseTokenPool.SiloConfigUpdate[] memory adds = new SiloedLockReleaseTokenPool.SiloConfigUpdate[](1); + adds[0] = SiloedLockReleaseTokenPool.SiloConfigUpdate({remoteChainSelector: 0, rebalancer: OWNER}); + + vm.expectRevert(abi.encodeWithSelector(SiloedLockReleaseTokenPool.InvalidChainSelector.selector, 0)); + + s_siloedLockReleaseTokenPool.updateSiloDesignations(new uint64[](0), adds); + } +} diff --git a/contracts/src/v0.8/ccip/test/pools/SiloedLockReleaseTokenPool/SiloedLockReleaseTokenPool.withdrawLiquidity.t.sol b/contracts/src/v0.8/ccip/test/pools/SiloedLockReleaseTokenPool/SiloedLockReleaseTokenPool.withdrawLiquidity.t.sol new file mode 100644 index 00000000000..7a82f0954a0 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/pools/SiloedLockReleaseTokenPool/SiloedLockReleaseTokenPool.withdrawLiquidity.t.sol @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.24; + +import {SiloedLockReleaseTokenPool} from "../../../pools/SiloedLockReleaseTokenPool.sol"; +import {TokenPool} from "../../../pools/TokenPool.sol"; +import {SiloedLockReleaseTokenPoolSetup} from "./SiloedLockReleaseTokenPoolSetup.t.sol"; + +contract SiloedLockReleaseTokenPool_withdrawLiqudity is SiloedLockReleaseTokenPoolSetup { + address public UNAUTHORIZED_ADDRESS = address(0xdeadbeef); + + function setUp() public override { + super.setUp(); + + s_siloedLockReleaseTokenPool.setSiloRebalancer(SILOED_CHAIN_SELECTOR, OWNER); + } + + function test_withdrawLiquidity_SiloedFunds() public { + uint256 amount = 1e24; + + uint256 balanceBefore = s_token.balanceOf(OWNER); + + // Provide the Liquidity first + s_siloedLockReleaseTokenPool.provideSiloedLiquidity(SILOED_CHAIN_SELECTOR, amount); + + vm.expectEmit(); + emit SiloedLockReleaseTokenPool.LiquidityRemoved(SILOED_CHAIN_SELECTOR, OWNER, amount); + + // Remove the Liquidity + s_siloedLockReleaseTokenPool.withdrawSiloedLiquidity(SILOED_CHAIN_SELECTOR, amount); + + assertEq(s_token.balanceOf(OWNER), balanceBefore); + assertEq(s_token.balanceOf(address(s_siloedLockReleaseTokenPool)), 0); + assertEq(s_siloedLockReleaseTokenPool.getAvailableTokens(SILOED_CHAIN_SELECTOR), 0); + assertEq(s_siloedLockReleaseTokenPool.getUnsiloedLiquidity(), 0); + } + + function test_withdrawSiloedLiquidity_UnsiloedFunds() public { + uint256 amount = 1e24; + + uint256 balanceBefore = s_token.balanceOf(OWNER); + + // Provide the Liquidity first + s_siloedLockReleaseTokenPool.provideSiloedLiquidity(DEST_CHAIN_SELECTOR, amount); + + assertEq(s_siloedLockReleaseTokenPool.getUnsiloedLiquidity(), amount); + + vm.expectEmit(); + emit SiloedLockReleaseTokenPool.LiquidityRemoved(DEST_CHAIN_SELECTOR, OWNER, amount); + + // Remove the Liquidity + s_siloedLockReleaseTokenPool.withdrawSiloedLiquidity(DEST_CHAIN_SELECTOR, amount); + + assertEq(s_token.balanceOf(OWNER), balanceBefore); + assertEq(s_token.balanceOf(address(s_siloedLockReleaseTokenPool)), 0); + assertEq(s_siloedLockReleaseTokenPool.getUnsiloedLiquidity(), 0); + } + + function test_withdrawLiquidity_UnsiloedFunds_LegacyFunctionSelector() public { + uint256 amount = 1e24; + + uint256 balanceBefore = s_token.balanceOf(OWNER); + + // Provide the Liquidity first + s_siloedLockReleaseTokenPool.provideSiloedLiquidity(DEST_CHAIN_SELECTOR, amount); + + assertEq(s_siloedLockReleaseTokenPool.getUnsiloedLiquidity(), amount); + + vm.expectEmit(); + emit SiloedLockReleaseTokenPool.LiquidityRemoved(0, OWNER, amount); + + // Remove the Liquidity + s_siloedLockReleaseTokenPool.withdrawLiquidity(amount); + + assertEq(s_token.balanceOf(OWNER), balanceBefore); + assertEq(s_token.balanceOf(address(s_siloedLockReleaseTokenPool)), 0); + assertEq(s_siloedLockReleaseTokenPool.getUnsiloedLiquidity(), 0); + assertEq(s_siloedLockReleaseTokenPool.getAvailableTokens(DEST_CHAIN_SELECTOR), 0); + } + + // Reverts + + function test_withdrawLiquidity_RevertWhen_SiloedFunds_NotEnoughLiquidity() public { + uint256 liquidityAmount = 1e24; + uint256 withdrawAmount = liquidityAmount + 1; + + s_siloedLockReleaseTokenPool.provideSiloedLiquidity(SILOED_CHAIN_SELECTOR, liquidityAmount); + + // Call should revert due to underflow error due to trying to burn more tokens than are locked via CCIP. + vm.expectRevert( + abi.encodeWithSelector(SiloedLockReleaseTokenPool.InsufficientLiquidity.selector, liquidityAmount, withdrawAmount) + ); + + s_siloedLockReleaseTokenPool.withdrawSiloedLiquidity(SILOED_CHAIN_SELECTOR, withdrawAmount); + } + + function test_withdrawSiloedLiquidity_RevertWhen_UnsiloedFunds_NotEnoughLiquidity() public { + uint256 liquidityAmount = 1e24; + uint256 withdrawAmount = liquidityAmount + 1; + + s_siloedLockReleaseTokenPool.provideSiloedLiquidity(DEST_CHAIN_SELECTOR, liquidityAmount); + + // Call should revert due to underflow error due to trying to burn more tokens than are locked via CCIP. + vm.expectRevert( + abi.encodeWithSelector(SiloedLockReleaseTokenPool.InsufficientLiquidity.selector, liquidityAmount, withdrawAmount) + ); + + // Test withdrawing funds from unsiloed liquidity pool but underflow + s_siloedLockReleaseTokenPool.withdrawSiloedLiquidity(DEST_CHAIN_SELECTOR, withdrawAmount); + } + + function test_withdrawSiloedLiqudity_RevertWhen_UnauthorizedOnlyUnsiloedRebalancer() public { + vm.startPrank(UNAUTHORIZED_ADDRESS); + + vm.expectRevert(abi.encodeWithSelector(TokenPool.Unauthorized.selector, UNAUTHORIZED_ADDRESS)); + + s_siloedLockReleaseTokenPool.withdrawSiloedLiquidity(DEST_CHAIN_SELECTOR, 1); + } + + function test_withdrawLiquidity_RevertsWhen_LegacyFunctionSelectorUnauthorized() public { + vm.startPrank(UNAUTHORIZED_ADDRESS); + + vm.expectRevert(abi.encodeWithSelector(TokenPool.Unauthorized.selector, UNAUTHORIZED_ADDRESS)); + + s_siloedLockReleaseTokenPool.withdrawLiquidity(1); + } +} diff --git a/contracts/src/v0.8/ccip/test/pools/SiloedLockReleaseTokenPool/SiloedLockReleaseTokenPoolSetup.t.sol b/contracts/src/v0.8/ccip/test/pools/SiloedLockReleaseTokenPool/SiloedLockReleaseTokenPoolSetup.t.sol new file mode 100644 index 00000000000..e7724663e05 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/pools/SiloedLockReleaseTokenPool/SiloedLockReleaseTokenPoolSetup.t.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.24; + +import {BurnMintERC20} from "../../../../shared/token/ERC20/BurnMintERC20.sol"; +import {Router} from "../../../Router.sol"; +import {SiloedLockReleaseTokenPool} from "../../../pools/SiloedLockReleaseTokenPool.sol"; +import {TokenPool} from "../../../pools/TokenPool.sol"; +import {BaseTest} from "../../BaseTest.t.sol"; + +import {IERC20} from "../../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; + +contract SiloedLockReleaseTokenPoolSetup is BaseTest { + IERC20 internal s_token; + SiloedLockReleaseTokenPool internal s_siloedLockReleaseTokenPool; + address[] internal s_allowedList; + + address internal s_allowedOnRamp = address(123); + address internal s_allowedOffRamp = address(234); + + address internal s_destPoolAddress = address(2736782345); + address internal s_sourcePoolAddress = address(53852352095); + + address internal s_siloedDestPoolAddress = address(4245234524); + uint64 internal constant SILOED_CHAIN_SELECTOR = DEST_CHAIN_SELECTOR + 1; + + function setUp() public virtual override { + super.setUp(); + s_token = new BurnMintERC20("LINK", "LNK", 18, 0, 0); + deal(address(s_token), OWNER, type(uint256).max); + + s_siloedLockReleaseTokenPool = new SiloedLockReleaseTokenPool( + s_token, DEFAULT_TOKEN_DECIMALS, new address[](0), address(s_mockRMNRemote), address(s_sourceRouter) + ); + + s_siloedLockReleaseTokenPool.setRebalancer(OWNER); + + s_token.approve(address(s_siloedLockReleaseTokenPool), type(uint256).max); + + bytes[] memory remotePoolAddresses = new bytes[](2); + remotePoolAddresses[0] = abi.encode(s_destPoolAddress); + remotePoolAddresses[1] = abi.encode(s_siloedDestPoolAddress); + + TokenPool.ChainUpdate[] memory chainUpdates = new TokenPool.ChainUpdate[](3); + chainUpdates[0] = TokenPool.ChainUpdate({ + remoteChainSelector: DEST_CHAIN_SELECTOR, + remotePoolAddresses: remotePoolAddresses, + remoteTokenAddress: abi.encode(address(2)), + outboundRateLimiterConfig: _getOutboundRateLimiterConfig(), + inboundRateLimiterConfig: _getInboundRateLimiterConfig() + }); + + chainUpdates[1] = TokenPool.ChainUpdate({ + remoteChainSelector: SILOED_CHAIN_SELECTOR, + remotePoolAddresses: remotePoolAddresses, + remoteTokenAddress: abi.encode(address(2)), + outboundRateLimiterConfig: _getOutboundRateLimiterConfig(), + inboundRateLimiterConfig: _getInboundRateLimiterConfig() + }); + + chainUpdates[2] = TokenPool.ChainUpdate({ + remoteChainSelector: SOURCE_CHAIN_SELECTOR, + remotePoolAddresses: remotePoolAddresses, + remoteTokenAddress: abi.encode(address(2)), + outboundRateLimiterConfig: _getOutboundRateLimiterConfig(), + inboundRateLimiterConfig: _getInboundRateLimiterConfig() + }); + + s_siloedLockReleaseTokenPool.applyChainUpdates(new uint64[](0), chainUpdates); + + Router.OnRamp[] memory onRampUpdates = new Router.OnRamp[](3); + Router.OffRamp[] memory offRampUpdates = new Router.OffRamp[](2); + + onRampUpdates[0] = Router.OnRamp({destChainSelector: DEST_CHAIN_SELECTOR, onRamp: s_allowedOnRamp}); + offRampUpdates[0] = Router.OffRamp({sourceChainSelector: SOURCE_CHAIN_SELECTOR, offRamp: s_allowedOffRamp}); + + onRampUpdates[1] = Router.OnRamp({destChainSelector: SILOED_CHAIN_SELECTOR, onRamp: s_allowedOnRamp}); + offRampUpdates[1] = Router.OffRamp({sourceChainSelector: SILOED_CHAIN_SELECTOR, offRamp: s_allowedOffRamp}); + + onRampUpdates[2] = Router.OnRamp({destChainSelector: SOURCE_CHAIN_SELECTOR, onRamp: s_allowedOnRamp}); + + s_sourceRouter.applyRampUpdates(onRampUpdates, new Router.OffRamp[](0), offRampUpdates); + + // Apply Siloeing Rules + SiloedLockReleaseTokenPool.SiloConfigUpdate[] memory adds = new SiloedLockReleaseTokenPool.SiloConfigUpdate[](1); + + adds[0] = + SiloedLockReleaseTokenPool.SiloConfigUpdate({remoteChainSelector: SILOED_CHAIN_SELECTOR, rebalancer: OWNER}); + + s_siloedLockReleaseTokenPool.updateSiloDesignations(new uint64[](0), adds); + + assertTrue(s_siloedLockReleaseTokenPool.isSiloed(SILOED_CHAIN_SELECTOR)); + assertFalse(s_siloedLockReleaseTokenPool.isSiloed(DEST_CHAIN_SELECTOR)); + + s_siloedLockReleaseTokenPool.setSiloRebalancer(SILOED_CHAIN_SELECTOR, OWNER); + } +} diff --git a/contracts/src/v0.8/ccip/test/pools/USDC/HybridLockReleaseUSDCTokenPool/HybridLockReleaseUSDCTokenPool.transferLiquidity.t.sol b/contracts/src/v0.8/ccip/test/pools/USDC/HybridLockReleaseUSDCTokenPool/HybridLockReleaseUSDCTokenPool.transferLiquidity.t.sol deleted file mode 100644 index d0600d26b88..00000000000 --- a/contracts/src/v0.8/ccip/test/pools/USDC/HybridLockReleaseUSDCTokenPool/HybridLockReleaseUSDCTokenPool.transferLiquidity.t.sol +++ /dev/null @@ -1,95 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.24; - -import {ILiquidityContainer} from "../../../../../liquiditymanager/interfaces/ILiquidityContainer.sol"; - -import {HybridLockReleaseUSDCTokenPool} from "../../../../pools/USDC/HybridLockReleaseUSDCTokenPool.sol"; -import {HybridLockReleaseUSDCTokenPoolSetup} from "./HybridLockReleaseUSDCTokenPoolSetup.t.sol"; - -contract HybridLockReleaseUSDCTokenPool_TransferLiquidity is HybridLockReleaseUSDCTokenPoolSetup { - function test_transferLiquidity() public { - // Set as the OWNER so we can provide liquidity - vm.startPrank(OWNER); - - s_usdcTokenPool.setLiquidityProvider(DEST_CHAIN_SELECTOR, OWNER); - s_token.approve(address(s_usdcTokenPool), type(uint256).max); - - uint256 liquidityAmount = 1e9; - - // Provide some liquidity to the pool - s_usdcTokenPool.provideLiquidity(DEST_CHAIN_SELECTOR, liquidityAmount); - - // Set the new token pool as the rebalancer - s_usdcTokenPool.transferOwnership(address(s_usdcTokenPoolTransferLiquidity)); - - vm.expectEmit(); - emit ILiquidityContainer.LiquidityRemoved(address(s_usdcTokenPoolTransferLiquidity), liquidityAmount); - - vm.expectEmit(); - emit HybridLockReleaseUSDCTokenPool.LiquidityTransferred( - address(s_usdcTokenPool), DEST_CHAIN_SELECTOR, liquidityAmount - ); - - s_usdcTokenPoolTransferLiquidity.transferLiquidity(address(s_usdcTokenPool), DEST_CHAIN_SELECTOR); - - assertEq( - s_usdcTokenPool.owner(), - address(s_usdcTokenPoolTransferLiquidity), - "Ownership of the old pool should be transferred to the new pool" - ); - - assertEq( - s_usdcTokenPoolTransferLiquidity.getLockedTokensForChain(DEST_CHAIN_SELECTOR), - liquidityAmount, - "Tokens locked for dest chain doesn't match expected amount in storage" - ); - - assertEq( - s_usdcTokenPool.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_usdcTokenPoolTransferLiquidity)), - liquidityAmount, - "Liquidity amount of tokens should be new in new pool, but aren't" - ); - - assertEq( - s_token.balanceOf(address(s_usdcTokenPool)), - 0, - "Liquidity amount of tokens should be zero in old pool, but aren't" - ); - } - - function test_RevertWhen_cannotTransferLiquidityDuringPendingMigration() public { - // Set as the OWNER so we can provide liquidity - vm.startPrank(OWNER); - - // Mark the destination chain as supporting CCTP, so use L/R instead. - uint64[] memory destChainAdds = new uint64[](1); - destChainAdds[0] = DEST_CHAIN_SELECTOR; - - s_usdcTokenPool.updateChainSelectorMechanisms(new uint64[](0), destChainAdds); - - s_usdcTokenPool.setLiquidityProvider(DEST_CHAIN_SELECTOR, OWNER); - s_token.approve(address(s_usdcTokenPool), type(uint256).max); - - uint256 liquidityAmount = 1e9; - - // Provide some liquidity to the pool - s_usdcTokenPool.provideLiquidity(DEST_CHAIN_SELECTOR, liquidityAmount); - - // Set the new token pool as the rebalancer - s_usdcTokenPool.transferOwnership(address(s_usdcTokenPoolTransferLiquidity)); - - s_usdcTokenPool.proposeCCTPMigration(DEST_CHAIN_SELECTOR); - - vm.expectRevert( - abi.encodeWithSelector(HybridLockReleaseUSDCTokenPool.LanePausedForCCTPMigration.selector, DEST_CHAIN_SELECTOR) - ); - - s_usdcTokenPoolTransferLiquidity.transferLiquidity(address(s_usdcTokenPool), DEST_CHAIN_SELECTOR); - } -}