-
Notifications
You must be signed in to change notification settings - Fork 51
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Multi-Mechanism USDC Token Pools (#1280)
## 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
1 parent
00ace85
commit 8d54185
Showing
4 changed files
with
1,022 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
220 changes: 220 additions & 0 deletions
220
contracts/src/v0.8/ccip/pools/USDC/HybridLockReleaseUSDCTokenPool.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
119
contracts/src/v0.8/ccip/pools/USDC/USDCBridgeMigrator.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]; | ||
} | ||
} |
Oops, something went wrong.