Skip to content

Commit

Permalink
Multi-Mechanism USDC Token Pools (#1280)
Browse files Browse the repository at this point in the history
## Motivation

Currently all USDC token transfers occur using Burn/Mint with calls to
Circle's CCTP system for cross-chain transfers. Certain chains in CCIP
have non-canonical USDC deployed and want to integrate the ability to
use CCIP as a canonical bridge whereby canonical-USDC is locked on the
source-chain and then minted on the destination chain. Then at a later
date, when circle decides to add that chain to CCTP, the non-canonical
USDC will be converted into canonical by burning the locked-tokens on
the source-chain. To accomplish this, the token pool needs to be capable
of identifying when tokens should be burned/minted with CCTP, or if they
should use regular lock-release, and act accordingly.

## Solution

`USDCTokenPool.sol` was been modified into a new file to allow for two
different mechanisms to be used simultaneously. The primary mechanism,
which is opt-out, and the secondary-mechanism which is opt in. These
mechanisms are configured on a per-chain-selector basis, and must be
manually enabled. In this implementation CCTP is the primary mechanism,
and Lock/Release is the alternative mechanism.

There are two new files
1. `HybridLockReleaseUSDCTokenPool.sol`, The actual implementation of
the hybrid pool structure.
3. `USDCBridgeMigrator.sol`, which contains the logic necessary to
conform to [Circle's migration policy
guide](https://github.com/circlefin/stablecoin-evm/blob/master/doc/bridged_USDC_standard.md#transferring-the-roles-to-circle)
  • Loading branch information
jhweintraub authored Aug 27, 2024
1 parent 00ace85 commit 8d54185
Show file tree
Hide file tree
Showing 4 changed files with 1,022 additions and 0 deletions.
22 changes: 22 additions & 0 deletions contracts/gas-snapshots/ccip.gas-snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,28 @@ FeeQuoter_validatePoolReturnData:test_InvalidEVMAddressDestToken_Revert() (gas:
FeeQuoter_validatePoolReturnData:test_ProcessPoolReturnData_Success() (gas: 73252)
FeeQuoter_validatePoolReturnData:test_SourceTokenDataTooLarge_Revert() (gas: 107744)
FeeQuoter_validatePoolReturnData:test_TokenAmountArraysMismatching_Revert() (gas: 40091)
HybridUSDCTokenPoolMigrationTests:test_LockOrBurn_LocKReleaseMechanism_then_switchToPrimary_Success() (gas: 208137)
HybridUSDCTokenPoolMigrationTests:test_LockOrBurn_PrimaryMechanism_Success() (gas: 135392)
HybridUSDCTokenPoolMigrationTests:test_LockOrBurn_WhileMigrationPause_Revert() (gas: 106624)
HybridUSDCTokenPoolMigrationTests:test_LockOrBurn_onLockReleaseMechanism_Success() (gas: 143884)
HybridUSDCTokenPoolMigrationTests:test_MintOrRelease_OnLockReleaseMechanism_Success() (gas: 230399)
HybridUSDCTokenPoolMigrationTests:test_MintOrRelease_OnLockReleaseMechanism_then_switchToPrimary_Success() (gas: 438259)
HybridUSDCTokenPoolMigrationTests:test_MintOrRelease_incomingMessageWithPrimaryMechanism() (gas: 269968)
HybridUSDCTokenPoolMigrationTests:test_burnLockedUSDC_invalidPermissions_Revert() (gas: 39124)
HybridUSDCTokenPoolMigrationTests:test_cancelExistingCCTPMigrationProposal() (gas: 31124)
HybridUSDCTokenPoolMigrationTests:test_cannotCancelANonExistentMigrationProposal() (gas: 12628)
HybridUSDCTokenPoolMigrationTests:test_cannotModifyLiquidityWithoutPermissions_Revert() (gas: 17133)
HybridUSDCTokenPoolMigrationTests:test_lockOrBurn_then_BurnInCCTPMigration_Success() (gas: 252432)
HybridUSDCTokenPoolMigrationTests:test_transferLiquidity_Success() (gas: 157049)
HybridUSDCTokenPoolMigrationTests:test_withdrawLiquidity_Success() (gas: 140780)
HybridUSDCTokenPoolTests:test_LockOrBurn_LocKReleaseMechanism_then_switchToPrimary_Success() (gas: 208102)
HybridUSDCTokenPoolTests:test_LockOrBurn_PrimaryMechanism_Success() (gas: 135365)
HybridUSDCTokenPoolTests:test_LockOrBurn_WhileMigrationPause_Revert() (gas: 106589)
HybridUSDCTokenPoolTests:test_LockOrBurn_onLockReleaseMechanism_Success() (gas: 143832)
HybridUSDCTokenPoolTests:test_MintOrRelease_OnLockReleaseMechanism_Success() (gas: 230365)
HybridUSDCTokenPoolTests:test_MintOrRelease_OnLockReleaseMechanism_then_switchToPrimary_Success() (gas: 438171)
HybridUSDCTokenPoolTests:test_MintOrRelease_incomingMessageWithPrimaryMechanism() (gas: 269912)
HybridUSDCTokenPoolTests:test_withdrawLiquidity_Success() (gas: 140774)
LockReleaseTokenPoolAndProxy_setRebalancer:test_SetRebalancer_Revert() (gas: 10970)
LockReleaseTokenPoolAndProxy_setRebalancer:test_SetRebalancer_Success() (gas: 17992)
LockReleaseTokenPoolPoolAndProxy_canAcceptLiquidity:test_CanAcceptLiquidity_Success() (gas: 3368110)
Expand Down
220 changes: 220 additions & 0 deletions contracts/src/v0.8/ccip/pools/USDC/HybridLockReleaseUSDCTokenPool.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.24;

import {ILiquidityContainer} from "../../../liquiditymanager/interfaces/ILiquidityContainer.sol";
import {ITokenMessenger} from "../USDC/ITokenMessenger.sol";

import {Pool} from "../../libraries/Pool.sol";
import {TokenPool} from "../TokenPool.sol";
import {USDCTokenPool} from "../USDC/USDCTokenPool.sol";
import {USDCBridgeMigrator} from "./USDCBridgeMigrator.sol";

import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol";
import {EnumerableSet} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/structs/EnumerableSet.sol";

/// @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 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,
IERC20 token,
address[] memory allowlist,
address rmnProxy,
address router
) USDCTokenPool(tokenMessenger, token, allowlist, rmnProxy, router) USDCBridgeMigrator(address(token), router) {}

// ================================================================
// │ 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 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)
{
if (!shouldUseLockRelease(releaseOrMintIn.remoteChainSelector)) {
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);

// Decrease internal tracking of locked tokens to ensure accurate accounting for burnLockedUSDC() migration
s_lockedTokensByChainSelector[releaseOrMintIn.remoteChainSelector] -= releaseOrMintIn.amount;

// Release to the offRamp, which forwards it to the recipient
getToken().safeTransfer(releaseOrMintIn.receiver, releaseOrMintIn.amount);

emit Released(msg.sender, releaseOrMintIn.receiver, releaseOrMintIn.amount);

return Pool.ReleaseOrMintOutV1({destinationAmount: releaseOrMintIn.amount});
}

/// @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 Locked(msg.sender, lockOrBurnIn.amount);

return Pool.LockOrBurnOutV1({destTokenAddress: getRemoteToken(lockOrBurnIn.remoteChainSelector), destPoolData: ""});
}

// ================================================================
// │ 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.
/// @param amount The amount of liquidity to provide.
/// @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);

s_lockedTokensByChainSelector[remoteChainSelector] += amount;

i_token.safeTransferFrom(msg.sender, address(this), amount);

emit ILiquidityContainer.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 {
if (s_liquidityProvider[remoteChainSelector] != msg.sender) revert TokenPool.Unauthorized(msg.sender);

s_lockedTokensByChainSelector[remoteChainSelector] -= amount;

i_token.safeTransfer(msg.sender, amount);
emit ILiquidityContainer.LiquidityRemoved(msg.sender, amount);
}

/// @notice This function can be used to transfer liquidity from an older version of the pool to this pool. To do so
/// this pool will have to be set as the liquidity provider in the older version of the pool. This allows it to transfer the
/// funds in the old pool to the new pool.
/// @dev When upgrading a LockRelease pool, this function can be called at the same time as the pool is changed in the
/// TokenAdminRegistry. This allows for a smooth transition of both liquidity and transactions to the new pool.
/// Alternatively, when no multicall is available, a portion of the funds can be transferred to the new pool before
/// changing which pool CCIP uses, to ensure both pools can operate. Then the pool should be changed in the
/// TokenAdminRegistry, which will activate the new pool. All new transactions will use the new pool and its
/// liquidity. Finally, the remaining liquidity can be transferred to the new pool using this function one more time.
/// @param from The address of the old pool.
/// @param amount The amount of liquidity to transfer.
function transferLiquidity(address from, uint64 remoteChainSelector, uint256 amount) external onlyOwner {
HybridLockReleaseUSDCTokenPool(from).withdrawLiquidity(remoteChainSelector, amount);

s_lockedTokensByChainSelector[remoteChainSelector] += amount;

emit LiquidityTransferred(from, remoteChainSelector, 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
function shouldUseLockRelease(uint64 remoteChainSelector) public view virtual returns (bool) {
return s_shouldUseLockRelease[remoteChainSelector];
}

/// @notice Updates 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
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) {
s_shouldUseLockRelease[adds[i]] = true;
emit LockReleaseEnabled(adds[i]);
}
}
}
119 changes: 119 additions & 0 deletions contracts/src/v0.8/ccip/pools/USDC/USDCBridgeMigrator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
pragma solidity ^0.8.24;

import {OwnerIsCreator} from "../../../shared/access/OwnerIsCreator.sol";
import {IBurnMintERC20} from "../../../shared/token/ERC20/IBurnMintERC20.sol";

import {EnumerableSet} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/structs/EnumerableSet.sol";

import {Router} from "../../Router.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 future CCTP compatibility
/// @dev Once a migration for a lane has occured, it can never be reversed, and CCTP will be the mechanism forever. This makes the assumption that Circle will continue to support that lane indefinitely.
abstract contract USDCBridgeMigrator is OwnerIsCreator {
using EnumerableSet for EnumerableSet.UintSet;

event CCTPMigrationProposed(uint64 remoteChainSelector);
event CCTPMigrationExecuted(uint64 remoteChainSelector, uint256 USDCBurned);
event CCTPMigrationCancelled(uint64 existingProposalSelector);
event CircleMigratorAddressSet(address migratorAddress);

error onlyCircle();
error ExistingMigrationProposal();
error NoExistingMigrationProposal();
error NoMigrationProposalPending();
error InvalidChainSelector(uint64 remoteChainSelector);

IBurnMintERC20 internal immutable i_USDC;
Router internal immutable i_router;

address internal s_circleUSDCMigrator;
uint64 internal s_proposedUSDCMigrationChain;

mapping(uint64 chainSelector => uint256 lockedBalance) internal s_lockedTokensByChainSelector;

mapping(uint64 chainSelector => bool shouldUseLockRelease) internal s_shouldUseLockRelease;

constructor(address token, address router) {
i_USDC = IBurnMintERC20(token);
i_router = Router(router);
}

/// @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() public {
if (msg.sender != s_circleUSDCMigrator) revert onlyCircle();
if (s_proposedUSDCMigrationChain == 0) revert ExistingMigrationProposal();

uint64 burnChainSelector = s_proposedUSDCMigrationChain;
uint256 tokensToBurn = s_lockedTokensByChainSelector[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];

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 the chain
/// selector is zero, or if a migration has already occured for the specified selector.
/// @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();

s_proposedUSDCMigrationChain = remoteChainSelector;

emit CCTPMigrationProposed(remoteChainSelector);
}

/// @notice Cancel an existing proposal to migrate a lane to CCTP.
function cancelExistingCCTPMigrationProposal() external onlyOwner {
if (s_proposedUSDCMigrationChain == 0) revert NoExistingMigrationProposal();

uint64 currentProposalChainSelector = s_proposedUSDCMigrationChain;
delete s_proposedUSDCMigrationChain;

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];
}
}
Loading

0 comments on commit 8d54185

Please sign in to comment.