From 983e1e58ec3e9d3a5727c6934a1d33bff67b8fef Mon Sep 17 00:00:00 2001 From: Alkhara Date: Mon, 4 May 2026 13:52:23 -0400 Subject: [PATCH 1/7] USDC Fork Test 2026-05-04 --- .../contracts/test/fork/MCMSForkTest.t.sol | 25 ++++ .../contracts/test/fork/USDCForkTest.t.sol | 132 ++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 chains/evm/contracts/test/fork/MCMSForkTest.t.sol create mode 100644 chains/evm/contracts/test/fork/USDCForkTest.t.sol diff --git a/chains/evm/contracts/test/fork/MCMSForkTest.t.sol b/chains/evm/contracts/test/fork/MCMSForkTest.t.sol new file mode 100644 index 0000000000..1d6f4b70c0 --- /dev/null +++ b/chains/evm/contracts/test/fork/MCMSForkTest.t.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.24; + +import {Test} from "forge-std/Test.sol"; + +contract MCMSForkTest is Test { + struct Call { + address target; + uint256 value; + bytes data; + } + + error TransactionReverted(); + + function _applyPayload(address sender, bytes memory payload) internal { + (MCMSForkTest.Call[] memory calls,,,) = abi.decode(payload, (MCMSForkTest.Call[], bytes32, bytes32, uint256)); + for (uint256 i = 0; i < calls.length; ++i) { + MCMSForkTest.Call memory call = calls[i]; + vm.startPrank(sender); + (bool success,) = call.target.call{value: call.value}(call.data); + if (!success) revert TransactionReverted(); + vm.stopPrank(); + } + } +} \ No newline at end of file diff --git a/chains/evm/contracts/test/fork/USDCForkTest.t.sol b/chains/evm/contracts/test/fork/USDCForkTest.t.sol new file mode 100644 index 0000000000..ec772b54c4 --- /dev/null +++ b/chains/evm/contracts/test/fork/USDCForkTest.t.sol @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.24; + +import {TokenAdminRegistry} from "../../tokenAdminRegistry/TokenAdminRegistry.sol"; +import {HybridLockReleaseUSDCTokenPool} from "../../pools/USDC/HybridLockReleaseUSDCTokenPool.sol"; +import {ERC20LockBox} from "../../pools/ERC20LockBox.sol"; +import {RateLimiter} from "../../libraries/RateLimiter.sol"; +import {MCMSForkTest} from "./MCMSForkTest.t.sol"; +import {IERC20} from "@openzeppelin/contracts@4.8.3/token/ERC20/IERC20.sol"; + +contract USDCForkTest is MCMSForkTest { + address private s_usdc_token_pool_address_sepolia; + + address private s_usdc_token_address_sepolia; + + address private s_timelock_address_sepolia; + + address private s_lockbox_address_adi_testnet; + + address private s_lockbox_address_jovay_testnet; + + address private s_lockbox_address_ink_testnet; + + uint256 private s_adi_withdraw_amount; + uint256 private s_jovay_withdraw_amount; + uint256 private s_ink_withdraw_amount; + + uint256 private s_forkId; + bytes[] private s_payloads; + + function setUp() public { + // Skip test if RPC_URL is not set (e.g., in CI without .env) + string memory rpcUrl = vm.envOr("RPC_URL", string("")); + vm.skip(bytes(rpcUrl).length == 0); + + s_forkId = vm.createFork(rpcUrl); + + // Load payloads dynamically based on PAYLOAD_COUNT + uint256 payloadCount = vm.envUint("PAYLOAD_COUNT"); + s_payloads = new bytes[](payloadCount); + for (uint256 i = 0; i < payloadCount; i++) { + s_payloads[i] = vm.envBytes(string.concat("PAYLOAD_", vm.toString(i + 1))); + } + + s_usdc_token_pool_address_sepolia = vm.envAddress("USDC_TOKEN_POOL_ADDRESS_SEPOLIA"); + s_usdc_token_address_sepolia = vm.envAddress("USDC_TOKEN_ADDRESS_SEPOLIA"); + s_timelock_address_sepolia = vm.envAddress("TIMELOCK_ADDRESS_SEPOLIA"); + s_lockbox_address_adi_testnet = vm.envAddress("LOCKBOX_ADDRESS_ADI_TESTNET"); + s_lockbox_address_jovay_testnet = vm.envAddress("LOCKBOX_ADDRESS_JOVAY_TESTNET"); + s_lockbox_address_ink_testnet = vm.envAddress("LOCKBOX_ADDRESS_INK_TESTNET"); + s_adi_withdraw_amount = vm.envUint("ADI_WITHDRAW_AMOUNT"); + s_jovay_withdraw_amount = vm.envUint("JOVAY_WITHDRAW_AMOUNT"); + s_ink_withdraw_amount = vm.envUint("INK_WITHDRAW_AMOUNT"); + } + + function testFork_Migration() public { + vm.selectFork(s_forkId); + + // Check locked tokens for remote chain selectors (ADI/Jovay/Ink) + // Check balanceOf the timelock address on the USDC token contract + // Check balanceOf each LockBox address on the USDC token contract + // Apply first paylod (withdraw liquidity) + // Re-check all the above + // Apply the second payload (approve lockbox to spend timelock's USDC) + // Apply the third payload (deposit USDC into the lockbox) + // Re-check all the above + + uint64 remoteChainSelectorADI = 9418205736192840573; // ADI Testnet + uint64 remoteChainSelectorJovay = 945045181441419236; // Jovay Testnet + uint64 remoteChainSelectorInk = 9763904284804119144; // Ink Testnet + + uint256 adiTestnetLockedTokens = HybridLockReleaseUSDCTokenPool(s_usdc_token_pool_address_sepolia).getLockedTokensForChain(remoteChainSelectorADI); + uint256 jovayTestnetLockedTokens = HybridLockReleaseUSDCTokenPool(s_usdc_token_pool_address_sepolia).getLockedTokensForChain(remoteChainSelectorJovay); + uint256 inkTestnetLockedTokens = HybridLockReleaseUSDCTokenPool(s_usdc_token_pool_address_sepolia).getLockedTokensForChain(remoteChainSelectorInk); + + uint256 timelockBalance = IERC20(s_usdc_token_address_sepolia).balanceOf(s_timelock_address_sepolia); + + uint256 adiTestnetLockBoxBalance = IERC20(s_usdc_token_address_sepolia).balanceOf(s_lockbox_address_adi_testnet); + uint256 jovayTestnetLockBoxBalance = IERC20(s_usdc_token_address_sepolia).balanceOf(s_lockbox_address_jovay_testnet); + uint256 inkTestnetLockBoxBalance = IERC20(s_usdc_token_address_sepolia).balanceOf(s_lockbox_address_ink_testnet); + + // Locked Tokens should be >= withdraw amounts + assertEq(adiTestnetLockedTokens, s_adi_withdraw_amount, "ADI Testnet locked tokens should be >= withdraw amount"); + assertEq(jovayTestnetLockedTokens, s_jovay_withdraw_amount, "Jovay Testnet locked tokens should be >= withdraw amount"); + assertEq(inkTestnetLockedTokens, s_ink_withdraw_amount, "Ink Testnet locked tokens should be >= withdraw amount"); + + // Apply first payload (withdraw liquidity) + _applyPayload(s_timelock_address_sepolia, s_payloads[0]); + + // Timelock should have the withdraw amounts + assertEq(timelockBalance, s_adi_withdraw_amount + s_jovay_withdraw_amount + s_ink_withdraw_amount, "Timelock balance should be >= withdraw amounts"); + + // LockBoxes should have 0 balance still (no deposit yet) + assertEq(adiTestnetLockBoxBalance, 0, "ADI Testnet lock box balance should be 0"); + assertEq(jovayTestnetLockBoxBalance, 0, "Jovay Testnet lock box balance should be 0"); + assertEq(inkTestnetLockBoxBalance, 0, "Ink Testnet lock box balance should be 0"); + + //Apply the second payload (approve lockbox to spend timelock's USDC) + _applyPayload(s_timelock_address_sepolia, s_payloads[1]); + + // Apply the third payload (deposit USDC into the lockbox) + _applyPayload(s_timelock_address_sepolia, s_payloads[2]); + + // Re-check Locked Tokens + uint256 newadiTestnetLockedTokens = HybridLockReleaseUSDCTokenPool(s_usdc_token_pool_address_sepolia).getLockedTokensForChain(remoteChainSelectorADI); + uint256 newjovayTestnetLockedTokens = HybridLockReleaseUSDCTokenPool(s_usdc_token_pool_address_sepolia).getLockedTokensForChain(remoteChainSelectorJovay); + uint256 newinkTestnetLockedTokens = HybridLockReleaseUSDCTokenPool(s_usdc_token_pool_address_sepolia).getLockedTokensForChain(remoteChainSelectorInk); + + // Locked Tokens should be adiTestnetLockedTokens - s_adi_withdraw_amount + // Jovay Testnet locked tokens should be jovayTestnetLockedTokens - s_jovay_withdraw_amount + // Ink Testnet locked tokens should be inkTestnetLockedTokens - s_ink_withdraw_amount + assertEq(newadiTestnetLockedTokens, adiTestnetLockedTokens - s_adi_withdraw_amount, "ADI Testnet locked tokens should be adiTestnetLockedTokens - s_adi_withdraw_amount"); + assertEq(newjovayTestnetLockedTokens, jovayTestnetLockedTokens - s_jovay_withdraw_amount, "Jovay Testnet locked tokens should be jovayTestnetLockedTokens - s_jovay_withdraw_amount"); + assertEq(newinkTestnetLockedTokens, inkTestnetLockedTokens - s_ink_withdraw_amount, "Ink Testnet locked tokens should be inkTestnetLockedTokens - s_ink_withdraw_amount"); + + // Re-check Timelock balance + uint256 newtimelockBalance = IERC20(s_usdc_token_address_sepolia).balanceOf(s_timelock_address_sepolia); + + // Timelock balance should be 0 + assertEq(newtimelockBalance, 0, "Timelock balance should be 0"); + + // Re-check LockBox balances + uint256 newadiTestnetLockBoxBalance = IERC20(s_usdc_token_address_sepolia).balanceOf(s_lockbox_address_adi_testnet); + uint256 newjovayTestnetLockBoxBalance = IERC20(s_usdc_token_address_sepolia).balanceOf(s_lockbox_address_jovay_testnet); + uint256 newinkTestnetLockBoxBalance = IERC20(s_usdc_token_address_sepolia).balanceOf(s_lockbox_address_ink_testnet); + + // LockBox balances should be s_adi_withdraw_amount + assertEq(newadiTestnetLockBoxBalance, s_adi_withdraw_amount, "ADI Testnet lock box balance should be s_adi_withdraw_amount"); + assertEq(newjovayTestnetLockBoxBalance, s_jovay_withdraw_amount, "Jovay Testnet lock box balance should be s_jovay_withdraw_amount"); + assertEq(newinkTestnetLockBoxBalance, s_ink_withdraw_amount, "Ink Testnet lock box balance should be s_ink_withdraw_amount"); + } +} \ No newline at end of file From ee5b478ff787ed9e70f9aa955797c3c15220e159 Mon Sep 17 00:00:00 2001 From: Alkhara Date: Mon, 4 May 2026 13:56:17 -0400 Subject: [PATCH 2/7] add contract --- .../USDC/HybridLockReleaseUSDCTokenPool.sol | 264 ++++++++++++++++++ 1 file changed, 264 insertions(+) create mode 100644 chains/evm/contracts/pools/USDC/HybridLockReleaseUSDCTokenPool.sol diff --git a/chains/evm/contracts/pools/USDC/HybridLockReleaseUSDCTokenPool.sol b/chains/evm/contracts/pools/USDC/HybridLockReleaseUSDCTokenPool.sol new file mode 100644 index 0000000000..2a91706f3a --- /dev/null +++ b/chains/evm/contracts/pools/USDC/HybridLockReleaseUSDCTokenPool.sol @@ -0,0 +1,264 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.24; + +import {ITokenMessenger} from "./interfaces/ITokenMessenger.sol"; + +import {Pool} from "../../libraries/Pool.sol"; +import {TokenPool} from "../TokenPool.sol"; +import {CCTPMessageTransmitterProxy} from "../USDC/CCTPMessageTransmitterProxy.sol"; +import {USDCTokenPool} from "../USDC/USDCTokenPool.sol"; +import {USDCBridgeMigrator} from "./USDCBridgeMigrator.sol"; + +import {IERC20} from + "@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from + "@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; +import {EnumerableSet} from + "@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/structs/EnumerableSet.sol"; + +// bytes4(keccak256("NO_CCTP_USE_LOCK_RELEASE")) +bytes4 constant LOCK_RELEASE_FLAG = 0xfa7c07de; + +/// @notice A token pool for USDC which uses CCTP for supported chains and Lock/Release for all others +/// @dev The functionality from LockReleaseTokenPool.sol has been duplicated due to lack of compiler support for shared +/// constructors between parents +/// @dev The primary token mechanism in this pool is Burn/Mint with CCTP, with Lock/Release as the +/// secondary, opt in mechanism for chains not currently supporting CCTP. +contract HybridLockReleaseUSDCTokenPool is USDCTokenPool, USDCBridgeMigrator { + using SafeERC20 for IERC20; + using EnumerableSet for EnumerableSet.UintSet; + + event LiquidityTransferred(address indexed from, uint64 indexed remoteChainSelector, uint256 amount); + event LiquidityProviderSet( + address indexed oldProvider, address indexed newProvider, uint64 indexed remoteChainSelector + ); + event LiquidityAdded(address indexed provider, uint256 indexed amount); + event LiquidityRemoved(address indexed provider, uint256 indexed amount); + + event LockReleaseEnabled(uint64 indexed remoteChainSelector); + event LockReleaseDisabled(uint64 indexed remoteChainSelector); + + error LanePausedForCCTPMigration(uint64 remoteChainSelector); + error TokenLockingNotAllowedAfterMigration(uint64 remoteChainSelector); + + /// @notice The address of the liquidity provider for a specific chain. + /// 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 + mapping(uint64 remoteChainSelector => address liquidityProvider) internal s_liquidityProvider; + + constructor( + ITokenMessenger tokenMessenger, + CCTPMessageTransmitterProxy cctpMessageTransmitterProxy, + IERC20 token, + address[] memory allowlist, + address rmnProxy, + address router, + address previousPool + ) + USDCTokenPool(tokenMessenger, cctpMessageTransmitterProxy, token, allowlist, rmnProxy, router, previousPool) + USDCBridgeMigrator(address(token)) + {} + + // ================================================================ + // │ Incoming/Outgoing Mechanisms | + // ================================================================ + + /// @notice Locks the token in the pool + /// @dev The _validateLockOrBurn check is an essential security check + function lockOrBurn( + Pool.LockOrBurnInV1 calldata lockOrBurnIn + ) public virtual override returns (Pool.LockOrBurnOutV1 memory) { + // // If the alternative mechanism (L/R) for chains which have it enabled + if (!shouldUseLockRelease(lockOrBurnIn.remoteChainSelector)) { + return super.lockOrBurn(lockOrBurnIn); + } + + // Circle requires a supply-lock to prevent outgoing messages once the migration process begins. + // This prevents new outgoing messages once the migration has begun to ensure any the procedure runs as expected + if (s_proposedUSDCMigrationChain == lockOrBurnIn.remoteChainSelector) { + revert LanePausedForCCTPMigration(s_proposedUSDCMigrationChain); + } + + return _lockReleaseOutgoingMessage(lockOrBurnIn); + } + + /// @notice Contains the alternative mechanism, in this implementation is "Lock" on outgoing tokens + function _lockReleaseOutgoingMessage( + Pool.LockOrBurnInV1 calldata lockOrBurnIn + ) internal virtual returns (Pool.LockOrBurnOutV1 memory) { + _validateLockOrBurn(lockOrBurnIn); + + // Increase internal accounting of locked tokens for burnLockedUSDC() migration + s_lockedTokensByChainSelector[lockOrBurnIn.remoteChainSelector] += lockOrBurnIn.amount; + + emit LockedOrBurned({ + remoteChainSelector: lockOrBurnIn.remoteChainSelector, + token: address(i_token), + sender: msg.sender, + amount: lockOrBurnIn.amount + }); + + return Pool.LockOrBurnOutV1({ + destTokenAddress: getRemoteToken(lockOrBurnIn.remoteChainSelector), + destPoolData: abi.encode(LOCK_RELEASE_FLAG) + }); + } + + /// @notice Release tokens from the pool to the recipient + /// @dev The _validateReleaseOrMint check is an essential security check + function releaseOrMint( + Pool.ReleaseOrMintInV1 calldata releaseOrMintIn + ) public virtual override returns (Pool.ReleaseOrMintOutV1 memory) { + // Use CCTP Burn/Mint mechanism for chains which have it enabled. The LOCK_RELEASE_FLAG is used in sourcePoolData to + // discern this, since the source-chain will not be a hybrid-pool but a standard burn-mint. In the event of a + // stuck message after a migration has occurred, and the message was not executed properly before the migration + // began, and locked tokens were not released until now, the message will already have been committed to with this + // flag so it is safe to release the tokens. The source USDC pool is trusted to send messages with the correct + // flag as well. + if (bytes4(releaseOrMintIn.sourcePoolData) != LOCK_RELEASE_FLAG) { + return super.releaseOrMint(releaseOrMintIn); + } + + return _lockReleaseIncomingMessage(releaseOrMintIn); + } + + /// @notice Contains the alternative mechanism for incoming tokens, in this implementation is "Release" incoming tokens + function _lockReleaseIncomingMessage( + Pool.ReleaseOrMintInV1 calldata releaseOrMintIn + ) internal virtual returns (Pool.ReleaseOrMintOutV1 memory) { + _validateReleaseOrMint(releaseOrMintIn, releaseOrMintIn.sourceDenominatedAmount); + + // Circle requires a supply-lock to prevent incoming messages once the migration process begins. + // This prevents new incoming messages once the migration has begun to ensure any the procedure runs as expected + if (s_proposedUSDCMigrationChain == releaseOrMintIn.remoteChainSelector) { + revert LanePausedForCCTPMigration(s_proposedUSDCMigrationChain); + } + + // Decrease internal tracking of locked tokens to ensure accurate accounting for burnLockedUSDC() migration + // If the chain has already been migrated, then this mapping would be zero, and the operation would underflow. + // This branch ensures that we're subtracting from the correct mapping. It is also safe to subtract from the + // excluded tokens mapping, as this function would only be invoked in the event of a stuck tx after a migration + if (s_lockedTokensByChainSelector[releaseOrMintIn.remoteChainSelector] == 0) { + s_tokensExcludedFromBurn[releaseOrMintIn.remoteChainSelector] -= releaseOrMintIn.sourceDenominatedAmount; + } else { + s_lockedTokensByChainSelector[releaseOrMintIn.remoteChainSelector] -= releaseOrMintIn.sourceDenominatedAmount; + } + + i_token.safeTransfer(releaseOrMintIn.receiver, releaseOrMintIn.sourceDenominatedAmount); + + emit ReleasedOrMinted({ + remoteChainSelector: releaseOrMintIn.remoteChainSelector, + token: address(i_token), + sender: msg.sender, + recipient: releaseOrMintIn.receiver, + amount: releaseOrMintIn.sourceDenominatedAmount + }); + + return Pool.ReleaseOrMintOutV1({destinationAmount: releaseOrMintIn.sourceDenominatedAmount}); + } + + // ================================================================ + // │ Liquidity Management | + // ================================================================ + + /// @notice Gets LiquidityManager, can be address(0) if none is configured. + /// @return The current liquidity manager for the given chain selector + function getLiquidityProvider( + uint64 remoteChainSelector + ) external view returns (address) { + return s_liquidityProvider[remoteChainSelector]; + } + + /// @notice Sets the LiquidityManager address. + /// @dev Only callable by the owner. + function setLiquidityProvider(uint64 remoteChainSelector, address liquidityProvider) external onlyOwner { + address oldProvider = s_liquidityProvider[remoteChainSelector]; + + s_liquidityProvider[remoteChainSelector] = liquidityProvider; + + emit LiquidityProviderSet(oldProvider, liquidityProvider, remoteChainSelector); + } + + /// @notice Adds liquidity to the pool for a specific chain. The tokens should be approved first. + /// @dev Liquidity is expected to be added on a per chain basis. Parties are expected to provide liquidity for their + /// own chain which implements non canonical USDC and liquidity is not shared across lanes. + /// @dev Once liquidity is added, it is locked in the pool until it is removed by an incoming message on the + /// lock release mechanism. This is a hard requirement by Circle to ensure parity with the destination chain + /// supply is maintained. + /// @param amount The amount of tokens to provide as liquidity. + /// @param remoteChainSelector The chain for which liquidity is provided to. Necessary to ensure there's accurate + /// parity between locked USDC in this contract and the circulating supply on the remote chain + function provideLiquidity(uint64 remoteChainSelector, uint256 amount) external { + if (s_liquidityProvider[remoteChainSelector] != msg.sender) revert TokenPool.Unauthorized(msg.sender); + + // Prevent adding liquidity to a chain which has already been migrated + if (s_migratedChains.contains(remoteChainSelector)) { + revert TokenLockingNotAllowedAfterMigration(remoteChainSelector); + } + + // prevent adding liquidity to a chain which has been proposed for migration + if (remoteChainSelector == s_proposedUSDCMigrationChain) { + revert LanePausedForCCTPMigration(remoteChainSelector); + } + + s_lockedTokensByChainSelector[remoteChainSelector] += amount; + + 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 remoteChainSelector The chain where liquidity is being released. + /// @param amount The amount of liquidity to remove. + /// @dev The function should only be called if non canonical USDC on the remote chain has been burned and is not being + /// withdrawn on this chain, otherwise a mismatch may occur between locked token balance and remote circulating supply + /// which may block a potential future migration of the chain to CCTP. + function withdrawLiquidity(uint64 remoteChainSelector, uint256 amount) external onlyOwner { + // A supply-lock is required to prevent outgoing messages once the migration process begins. + // This prevents new outgoing messages once the migration has begun to ensure any the procedure runs as expected + if (remoteChainSelector == s_proposedUSDCMigrationChain) { + revert LanePausedForCCTPMigration(remoteChainSelector); + } + + s_lockedTokensByChainSelector[remoteChainSelector] -= amount; + + i_token.safeTransfer(msg.sender, amount); + + emit LiquidityRemoved(msg.sender, amount); + } + + // ================================================================ + // │ Alt Mechanism Logic | + // ================================================================ + + /// @notice Return whether a lane should use the alternative L/R mechanism in the token pool. + /// @param remoteChainSelector the remote chain the lane is interacting with + /// @return bool Return true if the alternative L/R mechanism should be used, and is decided by the Owner + function shouldUseLockRelease( + uint64 remoteChainSelector + ) public view virtual returns (bool) { + return s_shouldUseLockRelease[remoteChainSelector]; + } + + /// @notice Updates designations for chains on whether to use primary or alt mechanism on CCIP messages + /// @param removes A list of chain selectors to disable Lock-Release, and enforce BM + /// @param adds A list of chain selectors to enable LR instead of BM. These chains must not have been migrated + /// to CCTP yet or the transaction will revert + function updateChainSelectorMechanisms(uint64[] calldata removes, uint64[] calldata adds) external onlyOwner { + for (uint256 i = 0; i < removes.length; ++i) { + delete s_shouldUseLockRelease[removes[i]]; + emit LockReleaseDisabled(removes[i]); + } + + for (uint256 i = 0; i < adds.length; ++i) { + // Prevent enabling lock release on chains which have already been migrated + if (s_migratedChains.contains(adds[i])) { + revert TokenLockingNotAllowedAfterMigration(adds[i]); + } + s_shouldUseLockRelease[adds[i]] = true; + emit LockReleaseEnabled(adds[i]); + } + } +} From 796230b5252d8a9e118d3db84790bbef45b073f4 Mon Sep 17 00:00:00 2001 From: Alkhara Date: Mon, 4 May 2026 13:57:20 -0400 Subject: [PATCH 3/7] add contract --- .../pools/USDC/USDCBridgeMigrator.sol | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 chains/evm/contracts/pools/USDC/USDCBridgeMigrator.sol diff --git a/chains/evm/contracts/pools/USDC/USDCBridgeMigrator.sol b/chains/evm/contracts/pools/USDC/USDCBridgeMigrator.sol new file mode 100644 index 0000000000..3b9a57f2a2 --- /dev/null +++ b/chains/evm/contracts/pools/USDC/USDCBridgeMigrator.sol @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.24; + +import {Ownable2StepMsgSender} from "@chainlink/contracts/src/v0.8/shared/access/Ownable2StepMsgSender.sol"; +import {IBurnMintERC20} from "@chainlink/contracts/src/v0.8/shared/token/ERC20/IBurnMintERC20.sol"; + +import {EnumerableSet} from + "@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/structs/EnumerableSet.sol"; + +/// @notice Allows migration of a lane in a token pool from Lock/Release to CCTP supported Burn/Mint. Contract +/// functionality is based on hard requirements defined by Circle to allow for future CCTP compatibility +/// https://github.com/circlefin/stablecoin-evm/blob/master/doc/bridged_USDC_standard.md +abstract contract USDCBridgeMigrator is Ownable2StepMsgSender { + using EnumerableSet for EnumerableSet.UintSet; + + event CCTPMigrationProposed(uint64 remoteChainSelector); + event CCTPMigrationExecuted(uint64 remoteChainSelector, uint256 USDCBurned); + event CCTPMigrationCancelled(uint64 existingProposalSelector); + event CircleMigratorAddressSet(address migratorAddress); + event TokensExcludedFromBurn( + uint64 indexed remoteChainSelector, uint256 amount, uint256 burnableAmountAfterExclusion + ); + + error onlyCircle(); + error ExistingMigrationProposal(); + error NoMigrationProposalPending(); + error InvalidChainSelector(); + + IBurnMintERC20 private immutable i_USDC; + + address internal s_circleUSDCMigrator; + uint64 internal s_proposedUSDCMigrationChain; + + mapping(uint64 chainSelector => uint256 lockedBalance) internal s_lockedTokensByChainSelector; + mapping(uint64 remoteChainSelector => uint256 excludedTokens) internal s_tokensExcludedFromBurn; + + mapping(uint64 chainSelector => bool shouldUseLockRelease) internal s_shouldUseLockRelease; + + EnumerableSet.UintSet internal s_migratedChains; + + constructor( + address token + ) { + i_USDC = IBurnMintERC20(token); + } + + /// @notice Burn USDC locked for a specific lane so that destination USDC can be converted from + /// non-canonical to canonical USDC. + /// @dev This function can only be called by an address specified by the owner to be controlled by circle + /// @dev proposeCCTPMigration must be called first on an approved lane to execute properly. + /// @dev This function signature should NEVER be overwritten, otherwise it will be unable to be called by + /// circle to properly migrate USDC over to CCTP. + function burnLockedUSDC() external { + if (msg.sender != s_circleUSDCMigrator) revert onlyCircle(); + if (s_proposedUSDCMigrationChain == 0) revert NoMigrationProposalPending(); + + uint64 burnChainSelector = s_proposedUSDCMigrationChain; + + // Burnable tokens is the total locked minus the amount excluded from burn + uint256 tokensToBurn = + s_lockedTokensByChainSelector[burnChainSelector] - s_tokensExcludedFromBurn[burnChainSelector]; + + // Even though USDC is a trusted call, ensure CEI by updating state first + delete s_lockedTokensByChainSelector[burnChainSelector]; + delete s_proposedUSDCMigrationChain; + + // This should only be called after this contract has been granted a "zero allowance minter role" on USDC by Circle, + // otherwise the call will revert. Executing this burn will functionally convert all USDC on the destination chain + // to canonical USDC by removing the canonical USDC backing it from circulation. + i_USDC.burn(tokensToBurn); + + // Disable L/R automatically on burned chain and enable CCTP + delete s_shouldUseLockRelease[burnChainSelector]; + + s_migratedChains.add(burnChainSelector); + + emit CCTPMigrationExecuted(burnChainSelector, tokensToBurn); + } + + /// @notice Propose a destination chain to migrate from lock/release mechanism to CCTP enabled burn/mint + /// through a Circle controlled burn. + /// @param remoteChainSelector the CCIP specific selector for the remote chain currently using a + /// non-canonical form of USDC which they wish to update to canonical. Function will revert if an existing migration + /// proposal is already in progress. + /// @dev This function can only be called by the owner + function proposeCCTPMigration( + uint64 remoteChainSelector + ) external onlyOwner { + // Prevent overwriting existing migration proposals until the current one is finished + if (s_proposedUSDCMigrationChain != 0) revert ExistingMigrationProposal(); + + // Ensure that the chain is currently using lock/release and not CCTP + if (!s_shouldUseLockRelease[remoteChainSelector]) revert InvalidChainSelector(); + + s_proposedUSDCMigrationChain = remoteChainSelector; + + emit CCTPMigrationProposed(remoteChainSelector); + } + + /// @notice Cancel an existing proposal to migrate a lane to CCTP. + /// @notice This function will revert if no proposal is currently in progress. + function cancelExistingCCTPMigrationProposal() external onlyOwner { + if (s_proposedUSDCMigrationChain == 0) revert NoMigrationProposalPending(); + + uint64 currentProposalChainSelector = s_proposedUSDCMigrationChain; + delete s_proposedUSDCMigrationChain; + + // If a migration is cancelled, the tokens excluded from burn should be reset, and must be manually + // re-excluded if the proposal is re-proposed in the future + delete s_tokensExcludedFromBurn[currentProposalChainSelector]; + + emit CCTPMigrationCancelled(currentProposalChainSelector); + } + + /// @notice retrieve the chain selector for an ongoing CCTP migration in progress. + /// @return uint64 the chain selector of the lane to be migrated. Will be zero if no proposal currently + /// exists + function getCurrentProposedCCTPChainMigration() public view returns (uint64) { + return s_proposedUSDCMigrationChain; + } + + /// @notice Set the address of the circle-controlled wallet which will execute a CCTP lane migration + /// @dev The function should only be invoked once the address has been confirmed by Circle prior to + /// chain expansion. + function setCircleMigratorAddress( + address migrator + ) external onlyOwner { + s_circleUSDCMigrator = migrator; + + emit CircleMigratorAddressSet(migrator); + } + + /// @notice Retrieve the amount of canonical USDC locked into this lane and minted on the destination + /// @param remoteChainSelector the CCIP specific destination chain implementing a mintable and + /// non-canonical form of USDC at present. + /// @return uint256 the amount of USDC locked into the specified lane. If non-zero, the number + /// should match the current circulating supply of USDC on the destination chain + function getLockedTokensForChain( + uint64 remoteChainSelector + ) public view returns (uint256) { + return s_lockedTokensByChainSelector[remoteChainSelector]; + } + + /// @notice Exclude tokens to be burned in a CCTP-migration because the amount are locked in an undelivered message. + /// @dev When a message is sitting in manual execution from the L/R chain, those tokens need to be excluded from + /// being burned in a CCTP-migration otherwise the message will never be able to be delivered due to it not having + /// an attestation on the source-chain to mint. In that instance it should use provided liquidity that was designated + /// @dev This function should ONLY be called on the home chain, where tokens are locked, NOT on the remote chain + /// and strict scrutiny should be applied to ensure that the amount of tokens excluded is accurate. + function excludeTokensFromBurn(uint64 remoteChainSelector, uint256 amount) external onlyOwner { + if (s_proposedUSDCMigrationChain != remoteChainSelector) revert NoMigrationProposalPending(); + + s_tokensExcludedFromBurn[remoteChainSelector] += amount; + + uint256 burnableAmountAfterExclusion = + s_lockedTokensByChainSelector[remoteChainSelector] - s_tokensExcludedFromBurn[remoteChainSelector]; + + emit TokensExcludedFromBurn(remoteChainSelector, amount, burnableAmountAfterExclusion); + } + + /// @notice Get the amount of tokens excluded from being burned in a CCTP-migration + /// @dev The sum of locked tokens and excluded tokens should equal the supply of the token on the remote chain + /// @param remoteChainSelector The chain for which the excluded tokens are being queried + /// @return uint256 amount of tokens excluded from being burned in a CCTP-migration + function getExcludedTokensByChain( + uint64 remoteChainSelector + ) external view returns (uint256) { + return s_tokensExcludedFromBurn[remoteChainSelector]; + } +} From c84c57caf215d6bfeee86293e2fcec4d3d8e1fcb Mon Sep 17 00:00:00 2001 From: Alkhara Date: Mon, 4 May 2026 13:57:51 -0400 Subject: [PATCH 4/7] add contract --- .../contracts/pools/USDC/USDCTokenPool.sol | 313 ++++++++++++++++++ 1 file changed, 313 insertions(+) create mode 100644 chains/evm/contracts/pools/USDC/USDCTokenPool.sol diff --git a/chains/evm/contracts/pools/USDC/USDCTokenPool.sol b/chains/evm/contracts/pools/USDC/USDCTokenPool.sol new file mode 100644 index 0000000000..25335f9044 --- /dev/null +++ b/chains/evm/contracts/pools/USDC/USDCTokenPool.sol @@ -0,0 +1,313 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.24; + +import {IPoolV1} from "../../interfaces/IPool.sol"; +import {IMessageTransmitter} from "./interfaces/IMessageTransmitter.sol"; +import {ITokenMessenger} from "./interfaces/ITokenMessenger.sol"; + +import {Pool} from "../../libraries/Pool.sol"; +import {TokenPool} from "../TokenPool.sol"; +import {CCTPMessageTransmitterProxy} from "./CCTPMessageTransmitterProxy.sol"; + +import {ITypeAndVersion} from "@chainlink/contracts/src/v0.8/shared/interfaces/ITypeAndVersion.sol"; +import {IERC20} from + "@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from + "@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC165} from + "@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/introspection/IERC165.sol"; + +/// @notice This pool mints and burns USDC tokens through the Cross Chain Transfer +/// Protocol (CCTP). +contract USDCTokenPool is TokenPool, ITypeAndVersion { + using SafeERC20 for IERC20; + + event DomainsSet(DomainUpdate[]); + event ConfigSet(address tokenMessenger); + + error UnknownDomain(uint64 domain); + error UnlockingUSDCFailed(); + error InvalidConfig(); + error InvalidDomain(DomainUpdate domain); + error InvalidMessageVersion(uint32 version); + error InvalidTokenMessengerVersion(uint32 version); + error InvalidNonce(uint64 expected, uint64 got); + error InvalidSourceDomain(uint32 expected, uint32 got); + error InvalidDestinationDomain(uint32 expected, uint32 got); + error InvalidReceiver(bytes receiver); + error InvalidTransmitterInProxy(); + error InvalidPreviousPool(); + error InvalidMessageLength(uint256 length); + + // This data is supplied from offchain and contains everything needed + // to receive the USDC tokens. + struct MessageAndAttestation { + bytes message; + bytes attestation; + } + + // A domain is a USDC representation of a chain. + struct DomainUpdate { + bytes32 allowedCaller; // Address allowed to mint on the domain + bytes32 mintRecipient; // Address to mint to on the destination chain + uint32 domainIdentifier; // ──╮ Unique domain ID + uint64 destChainSelector; // │ The destination chain for this domain + bool enabled; // ─────────────╯ Whether the domain is enabled + } + + struct SourceTokenDataPayload { + uint64 nonce; + uint32 sourceDomain; + } + + string public constant override typeAndVersion = "USDCTokenPool 1.6.2"; + + // We restrict to the first version. New pool may be required for subsequent versions. + uint32 public constant SUPPORTED_USDC_VERSION = 0; + + // The local USDC config + ITokenMessenger public immutable i_tokenMessenger; + CCTPMessageTransmitterProxy public immutable i_messageTransmitterProxy; + uint32 public immutable i_localDomainIdentifier; + + /// A domain is a USDC representation of a destination chain. + /// @dev Zero is a valid domain identifier. + /// @dev The address to mint on the destination chain is the corresponding USDC pool. + /// @dev The allowedCaller represents the contract authorized to call receiveMessage on the destination CCTP message transmitter. + /// For dest pool version 1.6.1, this is the MessageTransmitterProxy of the destination chain. + /// For dest pool version 1.5.1, this is the destination chain's token pool. + struct Domain { + bytes32 allowedCaller; // Address allowed to mint on the domain + bytes32 mintRecipient; // Address to mint to on the destination chain + uint32 domainIdentifier; // ─╮ Unique domain ID + bool enabled; // ────────────╯ Whether the domain is enabled + } + + // A mapping of CCIP chain identifiers to destination domains + mapping(uint64 chainSelector => Domain CCTPDomain) private s_chainToDomain; + + // In the event of an inflight message during a token pool migration, we need to route the message to the + // previous pool to satisfy the allowedCaller. The currently in-use token pool must be set as an offRamp + // in the router in order for the previous pool to accept the incoming call. + address public immutable i_previousPool; + + constructor( + ITokenMessenger tokenMessenger, + CCTPMessageTransmitterProxy cctpMessageTransmitterProxy, + IERC20 token, + address[] memory allowlist, + address rmnProxy, + address router, + address previousPool + ) TokenPool(token, 6, allowlist, rmnProxy, router) { + if (address(tokenMessenger) == address(0)) revert InvalidConfig(); + IMessageTransmitter transmitter = IMessageTransmitter(tokenMessenger.localMessageTransmitter()); + uint32 transmitterVersion = transmitter.version(); + if (transmitterVersion != SUPPORTED_USDC_VERSION) revert InvalidMessageVersion(transmitterVersion); + uint32 tokenMessengerVersion = tokenMessenger.messageBodyVersion(); + if (tokenMessengerVersion != SUPPORTED_USDC_VERSION) revert InvalidTokenMessengerVersion(tokenMessengerVersion); + if (cctpMessageTransmitterProxy.i_cctpTransmitter() != transmitter) revert InvalidTransmitterInProxy(); + + i_tokenMessenger = tokenMessenger; + i_messageTransmitterProxy = cctpMessageTransmitterProxy; + i_localDomainIdentifier = transmitter.localDomain(); + i_token.safeIncreaseAllowance(address(i_tokenMessenger), type(uint256).max); + + // PreviousPool should not be current pool. + if (previousPool == address(this)) { + revert InvalidPreviousPool(); + } + // If previousPool exists, it should be a valid token pool, we check it with supportsInterface. + if (previousPool != address(0) && !IERC165(previousPool).supportsInterface(type(IPoolV1).interfaceId)) { + revert InvalidPreviousPool(); + } + + i_previousPool = previousPool; + + emit ConfigSet(address(tokenMessenger)); + } + + /// @notice Burn tokens from the pool to initiate cross-chain transfer. + /// @notice Outgoing messages (burn operations) are routed via `i_tokenMessenger.depositForBurnWithCaller`. + /// The allowedCaller is preconfigured per destination domain and token pool version refer Domain struct. + /// @dev Emits ITokenMessenger.DepositForBurn event. + /// @dev Assumes caller has validated the destinationReceiver. + function lockOrBurn( + Pool.LockOrBurnInV1 calldata lockOrBurnIn + ) public virtual override returns (Pool.LockOrBurnOutV1 memory) { + _validateLockOrBurn(lockOrBurnIn); + + Domain memory domain = s_chainToDomain[lockOrBurnIn.remoteChainSelector]; + if (!domain.enabled) revert UnknownDomain(lockOrBurnIn.remoteChainSelector); + + if (lockOrBurnIn.receiver.length != 32) { + revert InvalidReceiver(lockOrBurnIn.receiver); + } + + bytes32 decodedReceiver; + // For EVM chains, the mintRecipient is not used, but is needed for Solana, where the mintRecipient will + // be a PDA owned by the pool, and will forward the tokens to its final destination after minting. + if (domain.mintRecipient != bytes32(0)) { + decodedReceiver = domain.mintRecipient; + } else { + decodedReceiver = abi.decode(lockOrBurnIn.receiver, (bytes32)); + } + + // Since this pool is the msg sender of the CCTP transaction, only this contract + // is able to call replaceDepositForBurn. Since this contract does not implement + // replaceDepositForBurn, the tokens cannot be maliciously re-routed to another address. + uint64 nonce = i_tokenMessenger.depositForBurnWithCaller( + lockOrBurnIn.amount, domain.domainIdentifier, decodedReceiver, address(i_token), domain.allowedCaller + ); + + emit LockedOrBurned({ + remoteChainSelector: lockOrBurnIn.remoteChainSelector, + token: address(i_token), + sender: msg.sender, + amount: lockOrBurnIn.amount + }); + + return Pool.LockOrBurnOutV1({ + destTokenAddress: getRemoteToken(lockOrBurnIn.remoteChainSelector), + destPoolData: abi.encode(SourceTokenDataPayload({nonce: nonce, sourceDomain: i_localDomainIdentifier})) + }); + } + + /// @notice Mint tokens from the pool to the recipient + /// * sourceTokenData is part of the verified message and passed directly from + /// the offRamp so it is guaranteed to be what the lockOrBurn pool released on the + /// source chain. It contains (nonce, sourceDomain) which is guaranteed by CCTP + /// to be unique. + /// * offchainTokenData is untrusted (can be supplied by manual execution), but we assert + /// that (nonce, sourceDomain) is equal to the message's (nonce, sourceDomain) and + /// receiveMessage will assert that Attestation contains a valid attestation signature + /// for that message, including its (nonce, sourceDomain). This way, the only + /// non-reverting offchainTokenData that can be supplied is a valid attestation for the + /// specific message that was sent on source. + function releaseOrMint( + Pool.ReleaseOrMintInV1 calldata releaseOrMintIn + ) public virtual override returns (Pool.ReleaseOrMintOutV1 memory) { + _validateReleaseOrMint(releaseOrMintIn, releaseOrMintIn.sourceDenominatedAmount); + SourceTokenDataPayload memory sourceTokenDataPayload = + abi.decode(releaseOrMintIn.sourcePoolData, (SourceTokenDataPayload)); + + MessageAndAttestation memory msgAndAttestation = + abi.decode(releaseOrMintIn.offchainTokenData, (MessageAndAttestation)); + + _validateMessage(msgAndAttestation.message, sourceTokenDataPayload); + + // If the destinationCaller is the previous pool, indicating an inflight message during the migration, we need to + // route the message to the previous pool to satisfy the allowedCaller. + bytes32 destinationCallerBytes32; + bytes memory messageBytes = msgAndAttestation.message; + assembly { + // destinationCaller is a 32-byte word starting at position 84 in messageBytes body, so add 32 to skip the 1st word + // representing bytes length + destinationCallerBytes32 := mload(add(messageBytes, 116)) // 84 + 32 = 116 + } + address destinationCaller = address(uint160(uint256(destinationCallerBytes32))); + + if (i_previousPool != address(0) && destinationCaller == i_previousPool) { + return USDCTokenPool(i_previousPool).releaseOrMint(releaseOrMintIn); + } + + if (!i_messageTransmitterProxy.receiveMessage(msgAndAttestation.message, msgAndAttestation.attestation)) { + revert UnlockingUSDCFailed(); + } + + emit ReleasedOrMinted({ + remoteChainSelector: releaseOrMintIn.remoteChainSelector, + token: address(i_token), + sender: msg.sender, + recipient: releaseOrMintIn.receiver, + amount: releaseOrMintIn.sourceDenominatedAmount + }); + + return Pool.ReleaseOrMintOutV1({destinationAmount: releaseOrMintIn.sourceDenominatedAmount}); + } + + /// @notice Validates the USDC encoded message against the given parameters. + /// @param usdcMessage The USDC encoded message + /// @param sourceTokenData The expected source chain token data to check against + /// @dev Only supports version SUPPORTED_USDC_VERSION of the CCTP message format + /// @dev Message format for USDC: + /// * Field Bytes Type Index + /// * version 4 uint32 0 + /// * sourceDomain 4 uint32 4 + /// * destinationDomain 4 uint32 8 + /// * nonce 8 uint64 12 + /// * sender 32 bytes32 20 + /// * recipient 32 bytes32 52 + /// * destinationCaller 32 bytes32 84 + /// * messageBody dynamic bytes 116 + function _validateMessage(bytes memory usdcMessage, SourceTokenDataPayload memory sourceTokenData) internal view { + // 116 is the minimum length of a valid USDC message. Since destinationCaller needs to be checked for the previous + // pool, this ensures that it can be parsed correctly and that the message is not too short. Since messageBody is + // dynamic and not always used, it is not checked. + if (usdcMessage.length < 116) revert InvalidMessageLength(usdcMessage.length); + + uint32 version; + // solhint-disable-next-line no-inline-assembly + assembly { + // We truncate using the datatype of the version variable, meaning + // we will only be left with the first 4 bytes of the message when we cast it to uint32. We want the lower 4 bytes + // to be the version when casted to a uint32 , so we only add 4. If you added 32, attempting to skip the first word + // containing the length, then version would be in the upper-4 bytes of the corresponding slot, which + // would not be as easily parsed into a uint32. + version := mload(add(usdcMessage, 4)) // 0 + 4 = 4 + } + // This token pool only supports version 0 of the CCTP message format + // We check the version prior to loading the rest of the message + // to avoid unexpected reverts due to out-of-bounds reads. + if (version != SUPPORTED_USDC_VERSION) revert InvalidMessageVersion(version); + + uint32 sourceDomain; + uint32 destinationDomain; + uint64 nonce; + + // solhint-disable-next-line no-inline-assembly + assembly { + sourceDomain := mload(add(usdcMessage, 8)) // 4 + 4 = 8 + destinationDomain := mload(add(usdcMessage, 12)) // 8 + 4 = 12 + nonce := mload(add(usdcMessage, 20)) // 12 + 8 = 20 + } + + if (sourceDomain != sourceTokenData.sourceDomain) { + revert InvalidSourceDomain(sourceTokenData.sourceDomain, sourceDomain); + } + if (destinationDomain != i_localDomainIdentifier) { + revert InvalidDestinationDomain(i_localDomainIdentifier, destinationDomain); + } + if (nonce != sourceTokenData.nonce) revert InvalidNonce(sourceTokenData.nonce, nonce); + } + + // ================================================================ + // │ Config │ + // ================================================================ + + /// @notice Gets the CCTP domain for a given CCIP chain selector. + function getDomain( + uint64 chainSelector + ) external view returns (Domain memory) { + return s_chainToDomain[chainSelector]; + } + + /// @notice Sets the CCTP domain for a CCIP chain selector. + /// @dev Must verify mapping of selectors -> (domain, caller) offchain. + function setDomains( + DomainUpdate[] calldata domains + ) external onlyOwner { + for (uint256 i = 0; i < domains.length; ++i) { + DomainUpdate memory domain = domains[i]; + if (domain.allowedCaller == bytes32(0) || domain.destChainSelector == 0) revert InvalidDomain(domain); + + s_chainToDomain[domain.destChainSelector] = Domain({ + domainIdentifier: domain.domainIdentifier, + mintRecipient: domain.mintRecipient, + allowedCaller: domain.allowedCaller, + enabled: domain.enabled + }); + } + emit DomainsSet(domains); + } +} From 69a53968ccf997355f01d52c74a2e6fe0ff016ca Mon Sep 17 00:00:00 2001 From: Alkhara Date: Mon, 4 May 2026 14:26:17 -0400 Subject: [PATCH 5/7] patch contracts for deps --- .../USDC/HybridLockReleaseUSDCTokenPool.sol | 16 ++++++---------- .../pools/USDC/USDCBridgeMigrator.sol | 3 +-- .../evm/contracts/pools/USDC/USDCTokenPool.sol | 18 +++++++----------- 3 files changed, 14 insertions(+), 23 deletions(-) diff --git a/chains/evm/contracts/pools/USDC/HybridLockReleaseUSDCTokenPool.sol b/chains/evm/contracts/pools/USDC/HybridLockReleaseUSDCTokenPool.sol index 2a91706f3a..6f50773682 100644 --- a/chains/evm/contracts/pools/USDC/HybridLockReleaseUSDCTokenPool.sol +++ b/chains/evm/contracts/pools/USDC/HybridLockReleaseUSDCTokenPool.sol @@ -9,12 +9,9 @@ import {CCTPMessageTransmitterProxy} from "../USDC/CCTPMessageTransmitterProxy.s import {USDCTokenPool} from "../USDC/USDCTokenPool.sol"; import {USDCBridgeMigrator} from "./USDCBridgeMigrator.sol"; -import {IERC20} from - "@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; -import {SafeERC20} from - "@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; -import {EnumerableSet} from - "@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/structs/EnumerableSet.sol"; +import {IERC20} from "@openzeppelin/contracts@5.3.0/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts@5.3.0/token/ERC20/utils/SafeERC20.sol"; +import {EnumerableSet} from "@openzeppelin/contracts@5.3.0/utils/structs/EnumerableSet.sol"; // bytes4(keccak256("NO_CCTP_USE_LOCK_RELEASE")) bytes4 constant LOCK_RELEASE_FLAG = 0xfa7c07de; @@ -51,12 +48,11 @@ contract HybridLockReleaseUSDCTokenPool is USDCTokenPool, USDCBridgeMigrator { ITokenMessenger tokenMessenger, CCTPMessageTransmitterProxy cctpMessageTransmitterProxy, IERC20 token, - address[] memory allowlist, address rmnProxy, address router, address previousPool ) - USDCTokenPool(tokenMessenger, cctpMessageTransmitterProxy, token, allowlist, rmnProxy, router, previousPool) + USDCTokenPool(tokenMessenger, cctpMessageTransmitterProxy, token, rmnProxy, router, previousPool) USDCBridgeMigrator(address(token)) {} @@ -87,7 +83,7 @@ contract HybridLockReleaseUSDCTokenPool is USDCTokenPool, USDCBridgeMigrator { function _lockReleaseOutgoingMessage( Pool.LockOrBurnInV1 calldata lockOrBurnIn ) internal virtual returns (Pool.LockOrBurnOutV1 memory) { - _validateLockOrBurn(lockOrBurnIn); + _validateLockOrBurn(lockOrBurnIn, WAIT_FOR_FINALITY, "", 0); // Increase internal accounting of locked tokens for burnLockedUSDC() migration s_lockedTokensByChainSelector[lockOrBurnIn.remoteChainSelector] += lockOrBurnIn.amount; @@ -127,7 +123,7 @@ contract HybridLockReleaseUSDCTokenPool is USDCTokenPool, USDCBridgeMigrator { function _lockReleaseIncomingMessage( Pool.ReleaseOrMintInV1 calldata releaseOrMintIn ) internal virtual returns (Pool.ReleaseOrMintOutV1 memory) { - _validateReleaseOrMint(releaseOrMintIn, releaseOrMintIn.sourceDenominatedAmount); + _validateReleaseOrMint(releaseOrMintIn, releaseOrMintIn.sourceDenominatedAmount, WAIT_FOR_FINALITY); // Circle requires a supply-lock to prevent incoming messages once the migration process begins. // This prevents new incoming messages once the migration has begun to ensure any the procedure runs as expected diff --git a/chains/evm/contracts/pools/USDC/USDCBridgeMigrator.sol b/chains/evm/contracts/pools/USDC/USDCBridgeMigrator.sol index 3b9a57f2a2..4929257605 100644 --- a/chains/evm/contracts/pools/USDC/USDCBridgeMigrator.sol +++ b/chains/evm/contracts/pools/USDC/USDCBridgeMigrator.sol @@ -4,8 +4,7 @@ pragma solidity ^0.8.24; import {Ownable2StepMsgSender} from "@chainlink/contracts/src/v0.8/shared/access/Ownable2StepMsgSender.sol"; import {IBurnMintERC20} from "@chainlink/contracts/src/v0.8/shared/token/ERC20/IBurnMintERC20.sol"; -import {EnumerableSet} from - "@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/structs/EnumerableSet.sol"; +import {EnumerableSet} from "@openzeppelin/contracts@5.3.0/utils/structs/EnumerableSet.sol"; /// @notice Allows migration of a lane in a token pool from Lock/Release to CCTP supported Burn/Mint. Contract /// functionality is based on hard requirements defined by Circle to allow for future CCTP compatibility diff --git a/chains/evm/contracts/pools/USDC/USDCTokenPool.sol b/chains/evm/contracts/pools/USDC/USDCTokenPool.sol index 25335f9044..a7df827bd8 100644 --- a/chains/evm/contracts/pools/USDC/USDCTokenPool.sol +++ b/chains/evm/contracts/pools/USDC/USDCTokenPool.sol @@ -10,12 +10,9 @@ import {TokenPool} from "../TokenPool.sol"; import {CCTPMessageTransmitterProxy} from "./CCTPMessageTransmitterProxy.sol"; import {ITypeAndVersion} from "@chainlink/contracts/src/v0.8/shared/interfaces/ITypeAndVersion.sol"; -import {IERC20} from - "@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; -import {SafeERC20} from - "@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; -import {IERC165} from - "@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/introspection/IERC165.sol"; +import {IERC20} from "@openzeppelin/contracts@5.3.0/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts@5.3.0/token/ERC20/utils/SafeERC20.sol"; +import {IERC165} from "@openzeppelin/contracts@5.3.0/utils/introspection/IERC165.sol"; /// @notice This pool mints and burns USDC tokens through the Cross Chain Transfer /// Protocol (CCTP). @@ -95,11 +92,10 @@ contract USDCTokenPool is TokenPool, ITypeAndVersion { ITokenMessenger tokenMessenger, CCTPMessageTransmitterProxy cctpMessageTransmitterProxy, IERC20 token, - address[] memory allowlist, address rmnProxy, address router, address previousPool - ) TokenPool(token, 6, allowlist, rmnProxy, router) { + ) TokenPool(token, 6, address(0), rmnProxy, router) { if (address(tokenMessenger) == address(0)) revert InvalidConfig(); IMessageTransmitter transmitter = IMessageTransmitter(tokenMessenger.localMessageTransmitter()); uint32 transmitterVersion = transmitter.version(); @@ -111,7 +107,7 @@ contract USDCTokenPool is TokenPool, ITypeAndVersion { i_tokenMessenger = tokenMessenger; i_messageTransmitterProxy = cctpMessageTransmitterProxy; i_localDomainIdentifier = transmitter.localDomain(); - i_token.safeIncreaseAllowance(address(i_tokenMessenger), type(uint256).max); + i_token.forceApprove(address(i_tokenMessenger), type(uint256).max); // PreviousPool should not be current pool. if (previousPool == address(this)) { @@ -135,7 +131,7 @@ contract USDCTokenPool is TokenPool, ITypeAndVersion { function lockOrBurn( Pool.LockOrBurnInV1 calldata lockOrBurnIn ) public virtual override returns (Pool.LockOrBurnOutV1 memory) { - _validateLockOrBurn(lockOrBurnIn); + _validateLockOrBurn(lockOrBurnIn, WAIT_FOR_FINALITY, "", 0); Domain memory domain = s_chainToDomain[lockOrBurnIn.remoteChainSelector]; if (!domain.enabled) revert UnknownDomain(lockOrBurnIn.remoteChainSelector); @@ -187,7 +183,7 @@ contract USDCTokenPool is TokenPool, ITypeAndVersion { function releaseOrMint( Pool.ReleaseOrMintInV1 calldata releaseOrMintIn ) public virtual override returns (Pool.ReleaseOrMintOutV1 memory) { - _validateReleaseOrMint(releaseOrMintIn, releaseOrMintIn.sourceDenominatedAmount); + _validateReleaseOrMint(releaseOrMintIn, releaseOrMintIn.sourceDenominatedAmount, WAIT_FOR_FINALITY); SourceTokenDataPayload memory sourceTokenDataPayload = abi.decode(releaseOrMintIn.sourcePoolData, (SourceTokenDataPayload)); From 4230ea8a71e407aac647005f5d07dab3b8d2c660 Mon Sep 17 00:00:00 2001 From: Alkhara Date: Wed, 6 May 2026 12:20:28 -0400 Subject: [PATCH 6/7] remove ink --- .../evm/contracts/test/fork/USDCForkTest.t.sol | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/chains/evm/contracts/test/fork/USDCForkTest.t.sol b/chains/evm/contracts/test/fork/USDCForkTest.t.sol index ec772b54c4..30a7d3eb4e 100644 --- a/chains/evm/contracts/test/fork/USDCForkTest.t.sol +++ b/chains/evm/contracts/test/fork/USDCForkTest.t.sol @@ -19,11 +19,8 @@ contract USDCForkTest is MCMSForkTest { address private s_lockbox_address_jovay_testnet; - address private s_lockbox_address_ink_testnet; - uint256 private s_adi_withdraw_amount; uint256 private s_jovay_withdraw_amount; - uint256 private s_ink_withdraw_amount; uint256 private s_forkId; bytes[] private s_payloads; @@ -47,10 +44,8 @@ contract USDCForkTest is MCMSForkTest { s_timelock_address_sepolia = vm.envAddress("TIMELOCK_ADDRESS_SEPOLIA"); s_lockbox_address_adi_testnet = vm.envAddress("LOCKBOX_ADDRESS_ADI_TESTNET"); s_lockbox_address_jovay_testnet = vm.envAddress("LOCKBOX_ADDRESS_JOVAY_TESTNET"); - s_lockbox_address_ink_testnet = vm.envAddress("LOCKBOX_ADDRESS_INK_TESTNET"); s_adi_withdraw_amount = vm.envUint("ADI_WITHDRAW_AMOUNT"); s_jovay_withdraw_amount = vm.envUint("JOVAY_WITHDRAW_AMOUNT"); - s_ink_withdraw_amount = vm.envUint("INK_WITHDRAW_AMOUNT"); } function testFork_Migration() public { @@ -67,33 +62,28 @@ contract USDCForkTest is MCMSForkTest { uint64 remoteChainSelectorADI = 9418205736192840573; // ADI Testnet uint64 remoteChainSelectorJovay = 945045181441419236; // Jovay Testnet - uint64 remoteChainSelectorInk = 9763904284804119144; // Ink Testnet uint256 adiTestnetLockedTokens = HybridLockReleaseUSDCTokenPool(s_usdc_token_pool_address_sepolia).getLockedTokensForChain(remoteChainSelectorADI); uint256 jovayTestnetLockedTokens = HybridLockReleaseUSDCTokenPool(s_usdc_token_pool_address_sepolia).getLockedTokensForChain(remoteChainSelectorJovay); - uint256 inkTestnetLockedTokens = HybridLockReleaseUSDCTokenPool(s_usdc_token_pool_address_sepolia).getLockedTokensForChain(remoteChainSelectorInk); uint256 timelockBalance = IERC20(s_usdc_token_address_sepolia).balanceOf(s_timelock_address_sepolia); uint256 adiTestnetLockBoxBalance = IERC20(s_usdc_token_address_sepolia).balanceOf(s_lockbox_address_adi_testnet); uint256 jovayTestnetLockBoxBalance = IERC20(s_usdc_token_address_sepolia).balanceOf(s_lockbox_address_jovay_testnet); - uint256 inkTestnetLockBoxBalance = IERC20(s_usdc_token_address_sepolia).balanceOf(s_lockbox_address_ink_testnet); // Locked Tokens should be >= withdraw amounts assertEq(adiTestnetLockedTokens, s_adi_withdraw_amount, "ADI Testnet locked tokens should be >= withdraw amount"); assertEq(jovayTestnetLockedTokens, s_jovay_withdraw_amount, "Jovay Testnet locked tokens should be >= withdraw amount"); - assertEq(inkTestnetLockedTokens, s_ink_withdraw_amount, "Ink Testnet locked tokens should be >= withdraw amount"); // Apply first payload (withdraw liquidity) _applyPayload(s_timelock_address_sepolia, s_payloads[0]); // Timelock should have the withdraw amounts - assertEq(timelockBalance, s_adi_withdraw_amount + s_jovay_withdraw_amount + s_ink_withdraw_amount, "Timelock balance should be >= withdraw amounts"); + assertEq(timelockBalance, s_adi_withdraw_amount + s_jovay_withdraw_amount, "Timelock balance should be >= withdraw amounts"); // LockBoxes should have 0 balance still (no deposit yet) assertEq(adiTestnetLockBoxBalance, 0, "ADI Testnet lock box balance should be 0"); assertEq(jovayTestnetLockBoxBalance, 0, "Jovay Testnet lock box balance should be 0"); - assertEq(inkTestnetLockBoxBalance, 0, "Ink Testnet lock box balance should be 0"); //Apply the second payload (approve lockbox to spend timelock's USDC) _applyPayload(s_timelock_address_sepolia, s_payloads[1]); @@ -104,14 +94,11 @@ contract USDCForkTest is MCMSForkTest { // Re-check Locked Tokens uint256 newadiTestnetLockedTokens = HybridLockReleaseUSDCTokenPool(s_usdc_token_pool_address_sepolia).getLockedTokensForChain(remoteChainSelectorADI); uint256 newjovayTestnetLockedTokens = HybridLockReleaseUSDCTokenPool(s_usdc_token_pool_address_sepolia).getLockedTokensForChain(remoteChainSelectorJovay); - uint256 newinkTestnetLockedTokens = HybridLockReleaseUSDCTokenPool(s_usdc_token_pool_address_sepolia).getLockedTokensForChain(remoteChainSelectorInk); // Locked Tokens should be adiTestnetLockedTokens - s_adi_withdraw_amount // Jovay Testnet locked tokens should be jovayTestnetLockedTokens - s_jovay_withdraw_amount - // Ink Testnet locked tokens should be inkTestnetLockedTokens - s_ink_withdraw_amount assertEq(newadiTestnetLockedTokens, adiTestnetLockedTokens - s_adi_withdraw_amount, "ADI Testnet locked tokens should be adiTestnetLockedTokens - s_adi_withdraw_amount"); assertEq(newjovayTestnetLockedTokens, jovayTestnetLockedTokens - s_jovay_withdraw_amount, "Jovay Testnet locked tokens should be jovayTestnetLockedTokens - s_jovay_withdraw_amount"); - assertEq(newinkTestnetLockedTokens, inkTestnetLockedTokens - s_ink_withdraw_amount, "Ink Testnet locked tokens should be inkTestnetLockedTokens - s_ink_withdraw_amount"); // Re-check Timelock balance uint256 newtimelockBalance = IERC20(s_usdc_token_address_sepolia).balanceOf(s_timelock_address_sepolia); @@ -122,11 +109,9 @@ contract USDCForkTest is MCMSForkTest { // Re-check LockBox balances uint256 newadiTestnetLockBoxBalance = IERC20(s_usdc_token_address_sepolia).balanceOf(s_lockbox_address_adi_testnet); uint256 newjovayTestnetLockBoxBalance = IERC20(s_usdc_token_address_sepolia).balanceOf(s_lockbox_address_jovay_testnet); - uint256 newinkTestnetLockBoxBalance = IERC20(s_usdc_token_address_sepolia).balanceOf(s_lockbox_address_ink_testnet); // LockBox balances should be s_adi_withdraw_amount assertEq(newadiTestnetLockBoxBalance, s_adi_withdraw_amount, "ADI Testnet lock box balance should be s_adi_withdraw_amount"); assertEq(newjovayTestnetLockBoxBalance, s_jovay_withdraw_amount, "Jovay Testnet lock box balance should be s_jovay_withdraw_amount"); - assertEq(newinkTestnetLockBoxBalance, s_ink_withdraw_amount, "Ink Testnet lock box balance should be s_ink_withdraw_amount"); } } \ No newline at end of file From 05da53e2b599072d5dba85716ff498c8a62523d6 Mon Sep 17 00:00:00 2001 From: Alkhara Date: Wed, 6 May 2026 15:53:59 -0400 Subject: [PATCH 7/7] fixes --- .../contracts/test/fork/MCMSForkTest.t.sol | 65 ++++++++++++++++++- .../contracts/test/fork/USDCForkTest.t.sol | 22 ++++--- 2 files changed, 76 insertions(+), 11 deletions(-) diff --git a/chains/evm/contracts/test/fork/MCMSForkTest.t.sol b/chains/evm/contracts/test/fork/MCMSForkTest.t.sol index 1d6f4b70c0..126781c823 100644 --- a/chains/evm/contracts/test/fork/MCMSForkTest.t.sol +++ b/chains/evm/contracts/test/fork/MCMSForkTest.t.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.24; import {Test} from "forge-std/Test.sol"; +import {console} from "forge-std/console.sol"; contract MCMSForkTest is Test { struct Call { @@ -12,14 +13,72 @@ contract MCMSForkTest is Test { error TransactionReverted(); + /// @dev ABI tuple `(Call[], bytes32, bytes32, uint256)`: 4-word head (128) + at least one word for `Call[]` tail (empty array). + uint256 private constant MIN_MCMS_PAYLOAD_BYTES = 160; + + error PayloadTooShortForMCMSEnvelope(uint256 length, uint256 minLength); + function _applyPayload(address sender, bytes memory payload) internal { - (MCMSForkTest.Call[] memory calls,,,) = abi.decode(payload, (MCMSForkTest.Call[], bytes32, bytes32, uint256)); + console.log("MCMSForkTest._applyPayload: prank sender"); + console.logAddress(sender); + console.log("MCMSForkTest._applyPayload: payload length (bytes)"); + console.logUint(payload.length); + + if (payload.length < MIN_MCMS_PAYLOAD_BYTES) { + console.log("MCMSForkTest._applyPayload: payload too short; expected full abi.encode(Call[],bytes32,bytes32,uint256) bytes"); + console.log("MCMSForkTest._applyPayload: min length (bytes)"); + console.logUint(MIN_MCMS_PAYLOAD_BYTES); + revert PayloadTooShortForMCMSEnvelope(payload.length, MIN_MCMS_PAYLOAD_BYTES); + } + + MCMSForkTest.Call[] memory calls; + { + bytes32 preHash; + bytes32 postHash; + uint256 chainId; + (calls, preHash, postHash, chainId) = abi.decode(payload, (MCMSForkTest.Call[], bytes32, bytes32, uint256)); + console.log("MCMSForkTest._applyPayload: decoded call count"); + console.logUint(calls.length); + console.log("MCMSForkTest._applyPayload: pre/post hash, chainId"); + console.logBytes32(preHash); + console.logBytes32(postHash); + console.logUint(chainId); + } + for (uint256 i = 0; i < calls.length; ++i) { MCMSForkTest.Call memory call = calls[i]; + console.log("MCMSForkTest._applyPayload: --- call index ---"); + console.logUint(i); + console.log("MCMSForkTest._applyPayload: target"); + console.logAddress(call.target); + console.log("MCMSForkTest._applyPayload: value (wei)"); + console.logUint(call.value); + console.log("MCMSForkTest._applyPayload: calldata length"); + console.logUint(call.data.length); + if (call.data.length >= 4) { + console.log("MCMSForkTest._applyPayload: selector"); + console.logBytes4(bytes4(call.data)); + } + vm.startPrank(sender); - (bool success,) = call.target.call{value: call.value}(call.data); - if (!success) revert TransactionReverted(); + (bool success, bytes memory returndata) = call.target.call{value: call.value}(call.data); vm.stopPrank(); + + if (!success) { + console.log("MCMSForkTest._applyPayload: CALL FAILED at index"); + console.logUint(i); + console.log("MCMSForkTest._applyPayload: failing calldata"); + console.logBytes(call.data); + console.log("MCMSForkTest._applyPayload: revert returndata (empty => bare revert)"); + console.logBytes(returndata); + if (returndata.length > 0) { + assembly ("memory-safe") { + revert(add(returndata, 0x20), mload(returndata)) + } + } + revert TransactionReverted(); + } } + console.log("MCMSForkTest._applyPayload: all calls succeeded"); } } \ No newline at end of file diff --git a/chains/evm/contracts/test/fork/USDCForkTest.t.sol b/chains/evm/contracts/test/fork/USDCForkTest.t.sol index 30a7d3eb4e..5f10c2929a 100644 --- a/chains/evm/contracts/test/fork/USDCForkTest.t.sol +++ b/chains/evm/contracts/test/fork/USDCForkTest.t.sol @@ -65,18 +65,20 @@ contract USDCForkTest is MCMSForkTest { uint256 adiTestnetLockedTokens = HybridLockReleaseUSDCTokenPool(s_usdc_token_pool_address_sepolia).getLockedTokensForChain(remoteChainSelectorADI); uint256 jovayTestnetLockedTokens = HybridLockReleaseUSDCTokenPool(s_usdc_token_pool_address_sepolia).getLockedTokensForChain(remoteChainSelectorJovay); - - uint256 timelockBalance = IERC20(s_usdc_token_address_sepolia).balanceOf(s_timelock_address_sepolia); - - uint256 adiTestnetLockBoxBalance = IERC20(s_usdc_token_address_sepolia).balanceOf(s_lockbox_address_adi_testnet); - uint256 jovayTestnetLockBoxBalance = IERC20(s_usdc_token_address_sepolia).balanceOf(s_lockbox_address_jovay_testnet); // Locked Tokens should be >= withdraw amounts assertEq(adiTestnetLockedTokens, s_adi_withdraw_amount, "ADI Testnet locked tokens should be >= withdraw amount"); assertEq(jovayTestnetLockedTokens, s_jovay_withdraw_amount, "Jovay Testnet locked tokens should be >= withdraw amount"); // Apply first payload (withdraw liquidity) - _applyPayload(s_timelock_address_sepolia, s_payloads[0]); + _applyPayload(s_timelock_address_sepolia, s_payloads[0]); // Chain 1 + // Apply the fourth payload (withdraw liquidity) + _applyPayload(s_timelock_address_sepolia, s_payloads[3]); // Chain 2 + + // Check balances before deposit + uint256 timelockBalance = IERC20(s_usdc_token_address_sepolia).balanceOf(s_timelock_address_sepolia); + uint256 adiTestnetLockBoxBalance = IERC20(s_usdc_token_address_sepolia).balanceOf(s_lockbox_address_adi_testnet); + uint256 jovayTestnetLockBoxBalance = IERC20(s_usdc_token_address_sepolia).balanceOf(s_lockbox_address_jovay_testnet); // Timelock should have the withdraw amounts assertEq(timelockBalance, s_adi_withdraw_amount + s_jovay_withdraw_amount, "Timelock balance should be >= withdraw amounts"); @@ -85,11 +87,15 @@ contract USDCForkTest is MCMSForkTest { assertEq(adiTestnetLockBoxBalance, 0, "ADI Testnet lock box balance should be 0"); assertEq(jovayTestnetLockBoxBalance, 0, "Jovay Testnet lock box balance should be 0"); - //Apply the second payload (approve lockbox to spend timelock's USDC) + // Apply the second payload (approve lockbox to spend timelock's USDC) Chain 1 _applyPayload(s_timelock_address_sepolia, s_payloads[1]); + // Apply the fifth payload (approve lockbox to spend timelock's USDC) Chain 2 + _applyPayload(s_timelock_address_sepolia, s_payloads[4]); - // Apply the third payload (deposit USDC into the lockbox) + // Apply the third payload (deposit USDC into the lockbox) Chain 1 _applyPayload(s_timelock_address_sepolia, s_payloads[2]); + // Apply the sixth payload (deposit USDC into the lockbox) Chain 2 + _applyPayload(s_timelock_address_sepolia, s_payloads[5]); // Re-check Locked Tokens uint256 newadiTestnetLockedTokens = HybridLockReleaseUSDCTokenPool(s_usdc_token_pool_address_sepolia).getLockedTokensForChain(remoteChainSelectorADI);