Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CCIP-4723 Feat/burn to zero address pool #15804

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from
10 changes: 10 additions & 0 deletions contracts/.changeset/chilly-news-wink.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@chainlink/contracts': minor
---

#feature Add two new pool types: Siloed-LockRelease and BurnToAddress and fix bug in HybridUSDCTokenPool for transferLiqudity #bugfix


PR issue: CCIP-4723

Solidity Review issue: CCIP-3966
15 changes: 14 additions & 1 deletion contracts/gas-snapshots/ccip.gas-snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ BurnMintTokenPool_lockOrBurn:test_PoolBurn() (gas: 236872)
BurnMintTokenPool_lockOrBurn:test_Setup() (gas: 17819)
BurnMintTokenPool_releaseOrMint:test_PoolMint() (gas: 102527)
BurnMintWithLockReleaseFlagTokenPool_lockOrBurn:test_LockOrBurn_CorrectReturnData() (gas: 237292)
BurnToAddressMintTokenPool_lockOrBurn:test_LockOrBurn() (gas: 244533)
BurnToAddressMintTokenPool_releaseOrMint:test_releaseOrMint() (gas: 109873)
BurnWithFromMintTokenPool_lockOrBurn:test_PoolBurn() (gas: 239012)
BurnWithFromMintTokenPool_lockOrBurn:test_Setup() (gas: 24169)
CCIPClientExample_sanity:test_ImmutableExamples() (gas: 2072849)
Expand Down Expand Up @@ -130,7 +132,8 @@ FeeQuoter_updateTokenPriceFeeds:test_SingleFeedUpdate() (gas: 53215)
FeeQuoter_updateTokenPriceFeeds:test_ZeroFeeds() (gas: 12471)
FeeQuoter_validateDestFamilyAddress:test_ValidEVMAddress() (gas: 6789)
FeeQuoter_validateDestFamilyAddress:test_ValidNonEVMAddress() (gas: 6514)
HybridLockReleaseUSDCTokenPool_TransferLiquidity:test_transferLiquidity() (gas: 167013)
HybridLockReleaseUSDCTokenPool_TransferLiquidity:test_transferLiquidity() (gas: 167715)
HybridLockReleaseUSDCTokenPool_TransferLiquidity:test_transferLiquidity_MultipleChainsSequentially() (gas: 233095)
HybridLockReleaseUSDCTokenPool_lockOrBurn:test_PrimaryMechanism() (gas: 130356)
HybridLockReleaseUSDCTokenPool_lockOrBurn:test_onLockReleaseMechanism() (gas: 140104)
HybridLockReleaseUSDCTokenPool_lockOrBurn:test_onLockReleaseMechanism_thenSwitchToPrimary() (gas: 202967)
Expand Down Expand Up @@ -345,6 +348,16 @@ Router_recoverTokens:test_RecoverTokens() (gas: 52668)
Router_routeMessage:test_routeMessage_AutoExec() (gas: 38071)
Router_routeMessage:test_routeMessage_ExecutionEvent() (gas: 153593)
Router_routeMessage:test_routeMessage_ManualExec() (gas: 31120)
SiloedLockReleaseTokenPool_lockOrBurn:testLockOrBurn_NonSiloedFunds_Success(uint256) (runs: 256, μ: 54330, ~: 54330)
SiloedLockReleaseTokenPool_lockOrBurn:test_LockOrBurn_SiloedFunds_Success(uint256) (runs: 256, μ: 75057, ~: 75057)
SiloedLockReleaseTokenPool_provideLiqudity:test_ProvideLiquidity_ChainNotSiloed_Success() (gas: 60201)
SiloedLockReleaseTokenPool_provideLiqudity:test_ProvideLiquidity_ChainSiloed_Success() (gas: 80899)
SiloedLockReleaseTokenPool_releaseOrMint:test_ReleaseOrMint_SiloedFunds_Success() (gas: 261601)
SiloedLockReleaseTokenPool_setRebalancer:test_setRebalancer_Success() (gas: 29682)
SiloedLockReleaseTokenPool_transferLiquidity:test_transferLiquidity_MultipleTransfers_Success() (gas: 3650435)
SiloedLockReleaseTokenPool_transferLiquidity:test_transferLiquidity_Success() (gas: 3627117)
SiloedLockReleaseTokenPool_updateChainSelectorMechanism:test_updateChainSelectorMechanism_Success() (gas: 76302)
SiloedLockReleaseTokenPool_withdrawLiqudity:test_withdrawLiquidity_Success() (gas: 69484)
TokenAdminRegistry_acceptAdminRole:test_acceptAdminRole() (gas: 44236)
TokenAdminRegistry_addRegistryModule:test_addRegistryModule() (gas: 67093)
TokenAdminRegistry_getAllConfiguredTokens:test_getAllConfiguredTokens_outOfBounds() (gas: 11363)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ abstract contract BurnMintTokenPoolAbstract is TokenPool {
/// @dev The _validateReleaseOrMint check is an essential security check
function releaseOrMint(
Pool.ReleaseOrMintInV1 calldata releaseOrMintIn
) external virtual override returns (Pool.ReleaseOrMintOutV1 memory) {
) public virtual override returns (Pool.ReleaseOrMintOutV1 memory) {
_validateReleaseOrMint(releaseOrMintIn);

// Calculate the local amount
Expand Down
78 changes: 78 additions & 0 deletions contracts/src/v0.8/ccip/pools/BurnToAddressMintTokenPool.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.24;

import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol";
import {IBurnMintERC20} from "../../shared/token/ERC20/IBurnMintERC20.sol";

import {Pool} from "../libraries/Pool.sol";
import {BurnMintTokenPoolAbstract} from "./BurnMintTokenPoolAbstract.sol";
import {TokenPool} from "./TokenPool.sol";

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

/// @notice This pool mints and burns a 3rd-party token by sending tokens to an address which is unrecoverable
/// @dev Pool whitelisting mode is set in the constructor and cannot be modified later.
/// It either accepts any address as originalSender, or only accepts whitelisted originalSender.
/// The only way to change whitelisting mode is to deploy a new pool.
/// If that is expected, please make sure the token's burner/minter roles are adjustable.
/// @dev This contract is a variant of BurnMintTokenPool that uses `burn(amount)`.
contract BurnToAddressMintTokenPool is BurnMintTokenPoolAbstract, ITypeAndVersion {
using SafeERC20 for IERC20;

string public constant override typeAndVersion = "BurnToAddressTokenPool 1.5.1";

address public immutable i_burnAddress;

/// @notice Locked Tokens is a safety mechanism to ensure that more tokens cannot be sent out of the bridge
/// than were originally sent in via CCIP.
uint256 internal s_lockedTokens;

/// @dev burnAddress is expected to be an address of which has no corresponding private key. Therefore the zero
/// address is a valid input, and no check for non-zero address is performed.
constructor(
IBurnMintERC20 token,
uint8 localTokenDecimals,
address[] memory allowlist,
address rmnProxy,
address router,
address burnAddress,
uint256 initialLockedTokens
) TokenPool(token, localTokenDecimals, allowlist, rmnProxy, router) {
i_burnAddress = burnAddress;
s_lockedTokens = initialLockedTokens;
}

/// @notice Mint 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) {
s_lockedTokens += releaseOrMintIn.amount;

return super.releaseOrMint(releaseOrMintIn);
}

/// @inheritdoc BurnMintTokenPoolAbstract
/// @notice Tokens are burned by sending to an address which is expected to have no corresponding private key,
/// which makes the tokens unrecoverable without reducing the total supply.
function _burn(
uint256 amount
) internal virtual override {
s_lockedTokens -= amount;

getToken().safeTransfer(i_burnAddress, amount);
}

/// @notice returns the address where tokens are sent during a call to lockOrBurn
/// @return burnAddress the address which receives the tokens.
function getBurnAddress() public view returns (address burnAddress) {
return i_burnAddress;
}

/// @notice Return the amount of tokens which were minted by this contract and not yet burned.
/// @return lockedTokens The amount of tokens which were minted by this token pool and not yet burned.
function getLockedTokens() public view returns (uint256 lockedTokens) {
return s_lockedTokens;
}
}
210 changes: 210 additions & 0 deletions contracts/src/v0.8/ccip/pools/SiloedLockReleaseTokenPool.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.24;

import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol";

import {Ownable2StepMsgSender} from "../../shared/access/Ownable2StepMsgSender.sol";
import {Pool} from "../libraries/Pool.sol";
import {TokenPool} from "./TokenPool.sol";

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

/// @notice A variation on Lock Release token pools where liquidity is shared among some chains, and stored independently
/// for others. Chains which do not share liquidity are known as siloed chains.
/// @dev One token per LockReleaseTokenPool.
contract SiloedLockReleaseTokenPool is TokenPool, ITypeAndVersion {
using SafeERC20 for IERC20;
using EnumerableSet for EnumerableSet.UintSet;

error InsufficientLiquidity();
error LiquidityNotAccepted();

event LiquidityTransferred(uint64 remoteChainSelector, address indexed from, uint256 amount);
event LiquidityAdded(uint64 remoteChainSelector, address indexed provider, uint256 indexed amount);
event LiquidityRemoved(uint64 remoteChainSelector, address indexed provider, uint256 indexed amount);
event ChainSiloeDesignationUpdated(uint64 remoteChainSelector, bool isSiloed);

event RebalancerSet(uint64 indexed remoteChainSelector, address oldRebalancer, address newRebalancer);

string public constant override typeAndVersion = "SiloedLockReleaseTokenPool 1.5.10-dev";

mapping(uint64 chainSelector => uint256 lockedBalance) internal s_lockedTokensByChainSelector;
mapping(uint64 remoteChainSelector => address rebalancer) internal s_rebalancerByChain;
mapping(uint64 remoteChainSelector => bool shouldSiloFunds) internal s_siloedChainSelectors;

EnumerableSet.UintSet internal s_siloedChains;

constructor(
IERC20 token,
uint8 localTokenDecimals,
address[] memory allowlist,
address rmnProxy,
address router
) TokenPool(token, localTokenDecimals, allowlist, rmnProxy, router) {}

/// @notice Locks the token in the pool
/// @dev The _validateLockOrBurn check is an essential security check
function lockOrBurn(
Pool.LockOrBurnInV1 calldata lockOrBurnIn
) external virtual override returns (Pool.LockOrBurnOutV1 memory) {
_validateLockOrBurn(lockOrBurnIn);

// If funds need to be siloed, update internal accounting;
if (s_siloedChainSelectors[lockOrBurnIn.remoteChainSelector]) {
s_lockedTokensByChainSelector[lockOrBurnIn.remoteChainSelector] += lockOrBurnIn.amount;
}

emit Locked(msg.sender, lockOrBurnIn.amount);

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

/// @notice Release tokens from the pool to the recipient
/// @dev The _validateReleaseOrMint check is an essential security check
function releaseOrMint(
Pool.ReleaseOrMintInV1 calldata releaseOrMintIn
) external virtual override returns (Pool.ReleaseOrMintOutV1 memory) {
_validateReleaseOrMint(releaseOrMintIn);

// Calculate the local amount
uint256 localAmount =
_calculateLocalAmount(releaseOrMintIn.amount, _parseRemoteDecimals(releaseOrMintIn.sourcePoolData));

// If the message comes from a chain with siloed funds, update internal accounting before releasing tokens.
if (s_siloedChainSelectors[releaseOrMintIn.remoteChainSelector]) {
s_lockedTokensByChainSelector[releaseOrMintIn.remoteChainSelector] -= localAmount;
}

// Release to the recipient
getToken().safeTransfer(releaseOrMintIn.receiver, localAmount);

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

return Pool.ReleaseOrMintOutV1({destinationAmount: localAmount});
}

/// @notice Returns whether the tokens locked for a given remote chain should be siloed independently
/// from all other remote chains.
/// @param remoteChainSelector the CCIP specific selector for the remote chain being interacted with.
/// @return isSiloed Whether the funds should be isolated from all the others.
function chainFundsAreSiloed(
uint64 remoteChainSelector
) external view returns (bool isSiloed) {
return s_siloedChainSelectors[remoteChainSelector];
}

/// @notice Returns the amount of tokens in the token pool that were locked for a specific chain selector.
/// @param remoteChainSelector the CCIP specific selector for the remote chain being interacted with.
/// @return lockedTokens The tokens locked into this token pool for the given selector, zero if the remote chain
/// funds are not siloed.
function getLockedTokensByChain(
uint64 remoteChainSelector
) external view returns (uint256 lockedTokens) {
return s_lockedTokensByChainSelector[remoteChainSelector];
}

/// @notice Updates designations for chains on whether to mark funds as Siloed or not
/// @param removes A list of chain selectors to disable Siloeing
/// @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 {
// If removing, change designation and update tokens as being available for all chains to use.
for (uint256 i = 0; i < removes.length; ++i) {
delete s_siloedChainSelectors[removes[i]];
delete s_lockedTokensByChainSelector[removes[i]];

emit ChainSiloeDesignationUpdated(removes[i], false);
}

for (uint256 i = 0; i < adds.length; ++i) {
s_siloedChainSelectors[adds[i]] = true;
emit ChainSiloeDesignationUpdated(adds[i], true);
}
}

/// @notice Gets the rebalancer able to provide liquidity for a remote chain selector
/// @param remoteChainSelector The CCIP specific selector for the remote chain being interacted with.
/// @return The current liquidity manager, owner if the chain's funds are not siloed.
function getRebalancerByChain(
uint64 remoteChainSelector
) external view returns (address) {
if (s_siloedChainSelectors[remoteChainSelector]) return s_rebalancerByChain[remoteChainSelector];
else return owner();
}

/// @notice Sets the LiquidityManager address.
/// @dev Only callable by the owner.
/// @param remoteChainSelector the remote chain to set.
/// @param rebalancer the address allowed to add liquidity for the given siloed chain.
function setRebalancer(uint64 remoteChainSelector, address rebalancer) external onlyOwner {
address oldRebalancer = s_rebalancerByChain[remoteChainSelector];

s_rebalancerByChain[remoteChainSelector] = rebalancer;

emit RebalancerSet(remoteChainSelector, rebalancer, oldRebalancer);
}

/// @notice Adds liquidity to the pool. The tokens should be approved first.
/// @param remoteChainSelector the remote chain to set.
/// @param amount The amount of liquidity to provide.
function provideLiquidity(uint64 remoteChainSelector, uint256 amount) external {
// Save gas by performing the enumerable set query once for both authorization and internal accounting
if (s_siloedChainSelectors[remoteChainSelector]) {
if (msg.sender != s_rebalancerByChain[remoteChainSelector]) revert Unauthorized(msg.sender);

s_lockedTokensByChainSelector[remoteChainSelector] += amount;
} else if (msg.sender != owner()) {
revert Unauthorized(msg.sender);
}

i_token.safeTransferFrom(msg.sender, address(this), amount);
emit LiquidityAdded(remoteChainSelector, msg.sender, amount);
}

/// @notice Removed liquidity to the pool. The tokens will be sent to msg.sender.
/// @param remoteChainSelector the remote chain to set. If the chain is not siloed, then no accounting will be updated,
/// which can be considered the liquidity for all non-siloed chains sharing liquidity.
/// @param amount The amount of liquidity to remove.
function withdrawLiquidity(uint64 remoteChainSelector, uint256 amount) external onlyOwner {
// If funds are siloed by chain, prevent more than has been locked from being removed from the token pool.
if (s_siloedChainSelectors[remoteChainSelector]) {
s_lockedTokensByChainSelector[remoteChainSelector] -= amount;
}

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

/// @notice This function can be used to transfer liquidity from an older version of the pool to this pool. To do so
/// this pool will have to be set as the rebalancer in the older version of the pool. This allows it to transfer the
/// funds in the old pool to the new pool.
/// @dev When upgrading a LockRelease pool, this function can be called at the same time as the pool is changed in the
/// TokenAdminRegistry. This allows for a smooth transition of both liquidity and transactions to the new pool.
/// Alternatively, when no multicall is available, a portion of the funds can be transferred to the new pool before
/// changing which pool CCIP uses, to ensure both pools can operate. Then the pool should be changed in the
/// TokenAdminRegistry, which will activate the new pool. All new transactions will use the new pool and its
/// liquidity. Finally, the remaining liquidity can be transferred to the new pool using this function one more time.
/// @param remoteChainSelector the remote chain to set. If the chain is not siloed, then no accounting will be updated,
/// which can be considered the liquidity for all non-siloed chains sharing liquidity.
/// @param from The address of the old pool.
/// @param amount The amount of liquidity to transfer.
function transferLiquidity(uint64 remoteChainSelector, address from, uint256 amount) external onlyOwner {
// If The ownership has already been accepted, do not attempt to accept again as it would fail.
if (Ownable2StepMsgSender(from).owner() != address(this)) Ownable2StepMsgSender(from).acceptOwnership();

SiloedLockReleaseTokenPool(from).withdrawLiquidity(remoteChainSelector, amount);

// Since both siloed and non-siloed token liquidity can be transferred, allow transfers from both, but only
// update internal accounting for siloed chains.
if (s_siloedChainSelectors[remoteChainSelector]) {
s_lockedTokensByChainSelector[remoteChainSelector] += amount;
}

emit LiquidityTransferred(remoteChainSelector, from, amount);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,10 @@ contract HybridLockReleaseUSDCTokenPool is USDCTokenPool, USDCBridgeMigrator {
/// @param from The address of the old pool.
/// @param remoteChainSelector The chain for which liquidity is being transferred.
function transferLiquidity(address from, uint64 remoteChainSelector) external onlyOwner {
Ownable2StepMsgSender(from).acceptOwnership();
// Only accept ownership the first time otherwise the call will revert due to no pending owner.
if (Ownable2StepMsgSender(from).owner() != address(this)) {
Ownable2StepMsgSender(from).acceptOwnership();
}

// Withdraw all available liquidity from the old pool. No check is needed for pending migrations, as the old pool
// will revert if the migration has begun.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ contract MaybeRevertingBurnMintTokenPool is BurnMintTokenPool {
/// @notice Reverts depending on the value of `s_revertReason`
function releaseOrMint(
Pool.ReleaseOrMintInV1 calldata releaseOrMintIn
) external virtual override returns (Pool.ReleaseOrMintOutV1 memory) {
) public virtual override returns (Pool.ReleaseOrMintOutV1 memory) {
_validateReleaseOrMint(releaseOrMintIn);

bytes memory revertReason = s_revertReason;
Expand Down
Loading
Loading