Skip to content

Commit 812a127

Browse files
CCTP V2 Token Pool Bug Fixes (#1331)
* bug fixes initial * better formatting, comments, and wrappers * comment fix * modify ITokenMessenger getMinFeeAmount() function * nit fixes * change abi.encode() to abi.encodePacked() * remove comment * formatting fix * fix errant test --------- Co-authored-by: Suryansh <[email protected]>
1 parent 98d6277 commit 812a127

File tree

17 files changed

+174
-38
lines changed

17 files changed

+174
-38
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@chainlink/contracts-ccip": patch
3+
---
4+
5+
Bugfixes for CCTP V2 Contracts

chains/evm/.gas-snapshot

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ BurnMintTokenPool_lockOrBurn:test_lockOrBurn_() (gas: 243286)
1414
BurnMintTokenPool_releaseOrMint:test_PoolMint() (gas: 105278)
1515
BurnMintWithLockReleaseFlagTokenPool_lockOrBurn:test_LockOrBurn_CorrectReturnData() (gas: 243807)
1616
BurnMintWithLockReleaseFlagTokenPool_releaseOrMint:test_releaseOrMint_EmptySourcePoolData() (gas: 104725)
17-
BurnMintWithLockReleaseFlagTokenPool_releaseOrMint:test_releaseOrMint_LockReleaseFlagInSourcePoolData() (gas: 104702)
17+
BurnMintWithLockReleaseFlagTokenPool_releaseOrMint:test_releaseOrMint_LockReleaseFlagInSourcePoolData() (gas: 104699)
1818
BurnToAddressMintTokenPool_lockOrBurn:test_LockOrBurn() (gas: 241000)
1919
BurnWithFromMintTokenPool_lockOrBurn:test_constructor() (gas: 23404)
2020
BurnWithFromMintTokenPool_lockOrBurn:test_lockOrBurn() (gas: 245593)
@@ -488,16 +488,16 @@ SiloedLockReleaseTokenPool_setRebalancer:test_setSiloRebalancer() (gas: 28778)
488488
SiloedLockReleaseTokenPool_updateSiloDesignations:test_updateSiloDesignations() (gas: 183879)
489489
SiloedLockReleaseTokenPool_withdrawLiqudity:test_withdrawLiquidity_SiloedFunds() (gas: 110516)
490490
SiloedLockReleaseTokenPool_withdrawLiqudity:test_withdrawLiquidity_UnsiloedFunds_LegacyFunctionSelector() (gas: 112193)
491-
SiloedUSDCTokenPool_burnLockedUSDC:test_burnLockedUSDC() (gas: 414564)
491+
SiloedUSDCTokenPool_burnLockedUSDC:test_burnLockedUSDC() (gas: 414741)
492492
SiloedUSDCTokenPool_cancelExistingCCTPMigrationProposal:test_cancelExistingCCTPMigrationProposal() (gas: 30532)
493493
SiloedUSDCTokenPool_cancelExistingCCTPMigrationProposal:test_cancelExistingCCTPMigrationProposal_EmitsEvent() (gas: 29852)
494494
SiloedUSDCTokenPool_cancelExistingCCTPMigrationProposal:test_cancelExistingCCTPMigrationProposal_ResetsExcludedTokens() (gas: 185948)
495495
SiloedUSDCTokenPool_excludeTokensFromBurn:test_excludeTokensFromBurn() (gas: 192396)
496496
SiloedUSDCTokenPool_excludeTokensFromBurn:test_excludeTokensFromBurn_EmitsEvent() (gas: 192244)
497-
SiloedUSDCTokenPool_lockOrBurn:test_lockOrBurn_MultipleLocks() (gas: 161173)
498-
SiloedUSDCTokenPool_lockOrBurn:test_lockOrBurn_Success() (gas: 135564)
499-
SiloedUSDCTokenPool_lockOrBurn:test_lockOrBurn_UpdatesLockedTokensAccounting() (gas: 132654)
500-
SiloedUSDCTokenPool_lockOrBurn:test_lockOrBurn_UpdatesSiloedTokensAccounting() (gas: 134596)
497+
SiloedUSDCTokenPool_lockOrBurn:test_lockOrBurn_MultipleLocks() (gas: 161615)
498+
SiloedUSDCTokenPool_lockOrBurn:test_lockOrBurn_Success() (gas: 135785)
499+
SiloedUSDCTokenPool_lockOrBurn:test_lockOrBurn_UpdatesLockedTokensAccounting() (gas: 132875)
500+
SiloedUSDCTokenPool_lockOrBurn:test_lockOrBurn_UpdatesSiloedTokensAccounting() (gas: 134817)
501501
SiloedUSDCTokenPool_proposeCCTPMigration:test_proposeCCTPMigration() (gas: 37970)
502502
SiloedUSDCTokenPool_proposeCCTPMigration:test_proposeCCTPMigration_AfterCancellation() (gas: 50568)
503503
SiloedUSDCTokenPool_releaseOrMint:test_releaseOrMint_SubtractsFromExcludedTokens() (gas: 352087)
@@ -550,11 +550,12 @@ TokenPool_validateReleaseOrMint:test_validateReleaseOrMint_Success() (gas: 39621
550550
USDCSourcePoolDataCodec__calculateDepositHash:test__calculateDepositHash() (gas: 9917)
551551
USDCSourcePoolDataCodec__decodeSourceTokenDataPayloadV2:test__decodeSourceTokenDataPayloadV2_CCTPV2() (gas: 5161)
552552
USDCSourcePoolDataCodec__decodeSourceTokenDataPayloadV2:test__decodeSourceTokenDataPayloadV2_CCTPV2CCV() (gas: 4916)
553-
USDCTokenPoolCCTPV2_constructor:test_constructor() (gas: 3429212)
554-
USDCTokenPoolCCTPV2_lockOrBurn:test_lockOrBurn() (gas: 129326)
555-
USDCTokenPoolCCTPV2_lockOrBurn:test_lockOrBurn_MintRecipientOverride() (gas: 156828)
553+
USDCTokenPoolCCTPV2_constructor:test_constructor() (gas: 3470365)
554+
USDCTokenPoolCCTPV2_lockOrBurn:test_lockOrBurn() (gas: 129947)
555+
USDCTokenPoolCCTPV2_lockOrBurn:test_lockOrBurn_MinFeeNotSupported() (gas: 114123)
556+
USDCTokenPoolCCTPV2_lockOrBurn:test_lockOrBurn_MintRecipientOverride() (gas: 157604)
556557
USDCTokenPoolCCTPV2_releaseOrMint:test_releaseOrMint_RealTx() (gas: 265533)
557-
USDCTokenPoolProxy_constructor:test_constructor() (gas: 1816306)
558+
USDCTokenPoolProxy_constructor:test_constructor() (gas: 1823082)
558559
USDCTokenPoolProxy_generateNewReleaseOrMintIn:test_generateNewReleaseOrMintIn() (gas: 35218)
559560
USDCTokenPoolProxy_lockOrBurn:test_lockOrBurn_CCTPV1() (gas: 104790)
560561
USDCTokenPoolProxy_lockOrBurn:test_lockOrBurn_CCTPV2() (gas: 94306)
@@ -571,9 +572,9 @@ USDCTokenPoolProxy_updateLockReleasePoolAddresses:test_updateLockReleasePoolAddr
571572
USDCTokenPoolProxy_updateLockReleasePoolAddresses:test_updateLockReleasePoolAddresses_Single() (gas: 52529)
572573
USDCTokenPoolProxy_updateLockReleasePoolAddresses:test_updateLockReleasePoolAddresses_ZeroAddress() (gas: 25431)
573574
USDCTokenPoolProxy_updatePoolAddresses:test_updatePoolAddresses() (gas: 55499)
574-
USDCTokenPool_constructor:test_constructor() (gas: 3377431)
575-
USDCTokenPool_lockOrBurn:test_LockOrBurn() (gas: 130827)
576-
USDCTokenPool_lockOrBurn:test_LockOrBurn_LegacySourcePoolDataFormat() (gas: 143734)
577-
USDCTokenPool_lockOrBurn:test_LockOrBurn_MintRecipientOverride() (gas: 159276)
575+
USDCTokenPool_constructor:test_constructor() (gas: 3377453)
576+
USDCTokenPool_lockOrBurn:test_LockOrBurn() (gas: 130897)
577+
USDCTokenPool_lockOrBurn:test_LockOrBurn_LegacySourcePoolDataFormat() (gas: 143822)
578+
USDCTokenPool_lockOrBurn:test_LockOrBurn_MintRecipientOverride() (gas: 159364)
578579
USDCTokenPool_releaseOrMint:test_ReleaseOrMintRealTx() (gas: 265775)
579580
USDCTokenPool_supportsInterface:test_SupportsInterface() (gas: 9990)

chains/evm/contracts/pools/USDC/SiloedUSDCTokenPool.sol

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
pragma solidity ^0.8.24;
33

44
import {Pool} from "../../libraries/Pool.sol";
5+
import {USDCSourcePoolDataCodec} from "../../libraries/USDCSourcePoolDataCodec.sol";
56
import {SiloedLockReleaseTokenPool} from "../SiloedLockReleaseTokenPool.sol";
67

78
import {AuthorizedCallers} from "@chainlink/contracts/src/v0.8/shared/access/AuthorizedCallers.sol";
@@ -90,7 +91,9 @@ contract SiloedUSDCTokenPool is SiloedLockReleaseTokenPool, AuthorizedCallers {
9091
// No excluded tokens is the common path, as it means no migration has occured yet, and any released
9192
// tokens should come from the stored token balance of previously deposited tokens.
9293
if (excludedTokens == 0) {
93-
if (localAmount > remoteConfig.tokenBalance) revert InsufficientLiquidity(remoteConfig.tokenBalance, localAmount);
94+
if (localAmount > remoteConfig.tokenBalance) {
95+
revert InsufficientLiquidity(remoteConfig.tokenBalance, localAmount);
96+
}
9497

9598
remoteConfig.tokenBalance -= localAmount;
9699

@@ -115,6 +118,22 @@ contract SiloedUSDCTokenPool is SiloedLockReleaseTokenPool, AuthorizedCallers {
115118
return Pool.ReleaseOrMintOutV1({destinationAmount: localAmount});
116119
}
117120

121+
/// @inheritdoc SiloedLockReleaseTokenPool
122+
/// @dev This function is overridden to encode the LOCK_RELEASE_FLAG into the destPoolData, as the destination pool.
123+
/// will be a BurnMintWithLockReleaseFlagTokenPool and may need to be processed by a proxy first.
124+
function lockOrBurn(
125+
Pool.LockOrBurnInV1 calldata lockOrBurnIn
126+
) public virtual override returns (Pool.LockOrBurnOutV1 memory) {
127+
// Call the parent contract's lockOrBurn function to get the base output. All functionality of this child
128+
// is inherited from the parent contract except for the overwritten destPoolData.
129+
Pool.LockOrBurnOutV1 memory baseOutput = super.lockOrBurn(lockOrBurnIn);
130+
131+
// Encode the LOCK_RELEASE_FLAG into the destPoolData using encodePacked to save space.
132+
baseOutput.destPoolData = abi.encodePacked(USDCSourcePoolDataCodec.LOCK_RELEASE_FLAG);
133+
134+
return baseOutput;
135+
}
136+
118137
/// @dev This function is overridden to prevent providing liquidity to a chain that has already been migrated, and thus should use CCTP-proper instead of a Lock/Release mechanism.
119138
function _provideLiquidity(uint64 remoteChainSelector, uint256 amount) internal override {
120139
if (s_migratedChains.contains(remoteChainSelector)) {

chains/evm/contracts/pools/USDC/USDCTokenPoolCCTPV2.sol

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ contract USDCTokenPoolCCTPV2 is USDCTokenPool {
2222
error InvalidExecutionFinalityThreshold(uint32 expected, uint32 got);
2323
error InvalidDepositHash(bytes32 expected, bytes32 got);
2424
error InvalidBurnToken(address expected, address got);
25+
error InvalidMinFee(uint256 maxAcceptableFee, uint256 actualFee);
2526

2627
/// @dev CCTP's max fee is based on the use of fast-burn. Since this pool does not utilize that feature, max fee should be 0.
2728
uint32 public constant MAX_FEE = 0;
@@ -64,6 +65,18 @@ contract USDCTokenPoolCCTPV2 is USDCTokenPool {
6465
revert InvalidReceiver(lockOrBurnIn.receiver);
6566
}
6667

68+
// Some CCTP-V2 chains support a configurable fee switch, but not all. It is therefore
69+
// necessary to check via a try-catch block. If the call reverts, then the fee switch is not supported and the
70+
// standard transfer fee will be zero, and no further action is required.
71+
try i_tokenMessenger.getMinFeeAmount(lockOrBurnIn.amount) returns (uint256 minFee) {
72+
// This token pool only supports zero-fee standard transfers. If the minFee is non-zero
73+
// then the function should revert as the message may not be able to be successfully
74+
// delivered on destination due to unexpected minting fees.
75+
if (minFee > MAX_FEE) {
76+
revert InvalidMinFee(MAX_FEE, minFee);
77+
}
78+
} catch {}
79+
6780
bytes32 decodedReceiver;
6881
// For EVM chains, the mintRecipient is not used, but is needed for Solana, where the mintRecipient will
6982
// be a PDA owned by the pool, and will forward the tokens to its final destination after minting.
@@ -166,8 +179,8 @@ contract USDCTokenPoolCCTPV2 is USDCTokenPool {
166179
/// * sender 32 bytes32 44
167180
/// * recipient 32 bytes32 76
168181
/// * destinationCaller 32 bytes32 108
169-
/// * minFinalityThreshold 32 uint32 140
170-
/// * finalityThresholdExecuted 32 uint32 144
182+
/// * minFinalityThreshold 4 uint32 140
183+
/// * finalityThresholdExecuted 4 uint32 144
171184
/// * messageBody dynamic bytes 148
172185

173186
/// @dev Message Body for USDC.

chains/evm/contracts/pools/USDC/USDCTokenPoolProxy.sol

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,8 @@ contract USDCTokenPoolProxy is Ownable2StepMsgSender, IPoolV1, ITypeAndVersion {
146146
function supportsInterface(
147147
bytes4 interfaceId
148148
) public pure override returns (bool) {
149-
return interfaceId == type(IPoolV1).interfaceId || interfaceId == type(IERC165).interfaceId;
149+
return interfaceId == type(IPoolV1).interfaceId || interfaceId == Pool.CCIP_POOL_V1
150+
|| interfaceId == type(IERC165).interfaceId;
150151
}
151152

152153
/// @inheritdoc IPoolV1

chains/evm/contracts/pools/USDC/interfaces/ITokenMessenger.sol

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,11 @@ interface ITokenMessenger {
108108
/// to/from remote domainsmessage transmitter for this token messenger.
109109
/// @dev immutable
110110
function localMessageTransmitter() external view returns (address);
111+
112+
/// Returns the minimum fee required for a deposit for burn message.
113+
/// @dev This function is only available for CCTP V2, and not every CCTP-V2 compatible
114+
/// chain supports this configurable fee switch.
115+
function getMinFeeAmount(
116+
uint256 amount
117+
) external view returns (uint256);
111118
}

chains/evm/contracts/test/mocks/MockE2EUSDCTokenMessenger.sol

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,13 @@ contract MockE2EUSDCTokenMessenger is ITokenMessenger {
123123
return i_transmitter;
124124
}
125125

126+
/// @dev This function is only available for CCTP V2
127+
function getMinFeeAmount(
128+
uint256
129+
) external pure returns (uint256) {
130+
return 0;
131+
}
132+
126133
/**
127134
* @notice Sends a BurnMessage through the local message transmitter
128135
* @dev calls local message transmitter's sendMessage() function if `_destinationCaller` == bytes32(0),

chains/evm/contracts/test/mocks/MockUSDCTokenMessenger.sol

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,11 @@ contract MockUSDCTokenMessenger is ITokenMessenger {
7575
function localMessageTransmitter() external view returns (address) {
7676
return i_transmitter;
7777
}
78+
79+
/// @dev This function is only available for CCTP V2
80+
function getMinFeeAmount(
81+
uint256
82+
) external pure returns (uint256) {
83+
return 0;
84+
}
7885
}

chains/evm/contracts/test/pools/BurnMintWithLockReleaseFlagTokenPool/BurnMintWithLockReleaseFlagTokenPool.releaseOrMint.t.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ contract BurnMintWithLockReleaseFlagTokenPool_releaseOrMint is BurnMintWithLockR
2525
localToken: address(s_token),
2626
remoteChainSelector: DEST_CHAIN_SELECTOR,
2727
sourcePoolAddress: abi.encode(s_initialRemotePool),
28-
sourcePoolData: abi.encode(LOCK_RELEASE_FLAG),
28+
sourcePoolData: abi.encodePacked(LOCK_RELEASE_FLAG),
2929
offchainTokenData: ""
3030
})
3131
);

chains/evm/contracts/test/pools/USDC/SiloedUSDCTokenPool/SiloedUSDCTokenPool.lockOrBurn.t.sol

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,7 @@ contract SiloedUSDCTokenPool_lockOrBurn is SiloedUSDCTokenPoolSetup {
4747
// Assert: Verify the result
4848
assertEq(s_usdcTokenPool.getAvailableTokens(DEST_CHAIN_SELECTOR), s_amount);
4949

50-
// destPoolData is the local token decimals abi-encoded to 32 bytes
51-
assertEq(result.destPoolData.length, 32);
50+
assertEq(result.destPoolData.length, 4);
5251
vm.stopPrank();
5352
}
5453

@@ -71,8 +70,7 @@ contract SiloedUSDCTokenPool_lockOrBurn is SiloedUSDCTokenPoolSetup {
7170
// Assert: Verify the locked tokens accounting is updated
7271
assertEq(s_usdcTokenPool.getAvailableTokens(DEST_CHAIN_SELECTOR), s_amount);
7372

74-
// destPoolData is the local token decimals abi-encoded to 32 bytes
75-
assertEq(result.destPoolData.length, 32);
73+
assertEq(result.destPoolData.length, 4);
7674
vm.stopPrank();
7775
}
7876

@@ -106,9 +104,8 @@ contract SiloedUSDCTokenPool_lockOrBurn is SiloedUSDCTokenPoolSetup {
106104
// Assert: Verify the locked tokens accounting is updated correctly
107105
assertEq(s_usdcTokenPool.getAvailableTokens(DEST_CHAIN_SELECTOR), s_amount + amount2);
108106

109-
// destPoolData is the local token decimals abi-encoded to 32 bytes
110-
assertEq(result1.destPoolData.length, 32);
111-
assertEq(result2.destPoolData.length, 32);
107+
assertEq(result1.destPoolData.length, 4);
108+
assertEq(result2.destPoolData.length, 4);
112109
vm.stopPrank();
113110
}
114111

@@ -128,7 +125,7 @@ contract SiloedUSDCTokenPool_lockOrBurn is SiloedUSDCTokenPoolSetup {
128125
assertEq(s_usdcTokenPool.getAvailableTokens(DEST_CHAIN_SELECTOR), s_amount);
129126
assertTrue(s_usdcTokenPool.isSiloed(DEST_CHAIN_SELECTOR));
130127

131-
assertEq(result.destPoolData.length, 32);
128+
assertEq(result.destPoolData.length, 4);
132129
vm.stopPrank();
133130
}
134131

0 commit comments

Comments
 (0)