Skip to content

Commit 6391121

Browse files
authored
Check invalid receiver in FTF Pool (#1087)
* add empty receiver check * add wrappers * add check for all zero receiver in ccipSendToken * update wrappers * update wrappers * refactor: remove loop and evaluate for 2 words + rename: hasNonZero to isNonZero * remove ifiszero(isNonZero) + add fuzz test * update test * change fuzz test to achieve higher input space coverage
1 parent 98d3d0c commit 6391121

File tree

6 files changed

+131
-24
lines changed

6 files changed

+131
-24
lines changed

chains/evm/.gas-snapshot

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
BurnFromMintTokenPool_lockOrBurn:test_constructor() (gas: 23404)
22
BurnFromMintTokenPool_lockOrBurn:test_lockOrBurn() (gas: 245510)
33
BurnMintFastTransferTokenPool_ccipReceive:test_ccipReceive_SlowFilled() (gas: 131739)
4-
BurnMintFastTransferTokenPool_ccipSendToken:test_ccipSendToken_Success() (gas: 130360)
5-
BurnMintFastTransferTokenPool_ccipSendToken:test_ccipSendToken_WithERC20FeeToken() (gas: 175290)
6-
BurnMintFastTransferTokenPool_validateSendRequest:test_validateSendRequest_Success() (gas: 31246)
7-
BurnMintFastTransferTokenPool_validateSendRequest:test_validateSendRequest_WithAllowlistedSender() (gas: 5666863)
4+
BurnMintFastTransferTokenPool_ccipSendToken:test_ccipSendToken_Success() (gas: 130544)
5+
BurnMintFastTransferTokenPool_ccipSendToken:test_ccipSendToken_WithERC20FeeToken() (gas: 175658)
6+
BurnMintFastTransferTokenPool_validateSendRequest:test_validateSendRequest_Success() (gas: 31430)
7+
BurnMintFastTransferTokenPool_validateSendRequest:test_validateSendRequest_WithAllowlistedSender() (gas: 5693920)
88
BurnMintFastTransferTokenPool_validateSettlement:test_validateSettlement_Success() (gas: 116622)
99
BurnMintFastTransferTokenPool_withdrawPoolFees_Test:test_withdrawPoolFees() (gas: 221755)
1010
BurnMintFastTransferTokenPool_withdrawPoolFees_Test:test_withdrawPoolFees_BothGasAnd() (gas: 285225)
@@ -90,16 +90,17 @@ FastTransferTokenPool_ccipReceive_Test:test_ccipReceive_SlowFill() (gas: 95467)
9090
FastTransferTokenPool_ccipReceive_Test:test_ccipReceive_SlowFill_WithPoolFee() (gas: 155881)
9191
FastTransferTokenPool_ccipReceive_Test:test_ccipReceive_WithDifferentDecimals() (gas: 94122)
9292
FastTransferTokenPool_ccipReceive_Test:test_ccipReceive_ZeroFastTransferFeeBps() (gas: 95480)
93-
FastTransferTokenPool_ccipSendToken_Test:test_ccipSendToken_APTOS_WithSettlementGas() (gas: 500582)
94-
FastTransferTokenPool_ccipSendToken_Test:test_ccipSendToken_APTOS_WithZeroSettlementGas() (gas: 587945)
95-
FastTransferTokenPool_ccipSendToken_Test:test_ccipSendToken_EqualFeeSplit() (gas: 73264)
96-
FastTransferTokenPool_ccipSendToken_Test:test_ccipSendToken_FeeQuote_WithPoolFee() (gas: 72784)
97-
FastTransferTokenPool_ccipSendToken_Test:test_ccipSendToken_FeeValidation_Success_WhenFeeEqualsLimit() (gas: 108966)
98-
FastTransferTokenPool_ccipSendToken_Test:test_ccipSendToken_FeeValidation_Success_WhenFeeWithinLimit() (gas: 108804)
99-
FastTransferTokenPool_ccipSendToken_Test:test_ccipSendToken_NativeFee() (gas: 129238)
100-
FastTransferTokenPool_ccipSendToken_Test:test_ccipSendToken_NativeFee_ToSVM() (gas: 170062)
101-
FastTransferTokenPool_ccipSendToken_Test:test_ccipSendToken_WithERC20FeeToken() (gas: 149985)
102-
FastTransferTokenPool_ccipSendToken_Test:test_ccipSendToken_WithPoolFee() (gas: 156575)
93+
FastTransferTokenPool_ccipSendToken_Test:test_ccipSendToken_APTOS_WithSettlementGas() (gas: 500814)
94+
FastTransferTokenPool_ccipSendToken_Test:test_ccipSendToken_APTOS_WithZeroSettlementGas() (gas: 588199)
95+
FastTransferTokenPool_ccipSendToken_Test:test_ccipSendToken_EqualFeeSplit() (gas: 73470)
96+
FastTransferTokenPool_ccipSendToken_Test:test_ccipSendToken_FeeQuote_WithPoolFee() (gas: 72990)
97+
FastTransferTokenPool_ccipSendToken_Test:test_ccipSendToken_FeeValidation_Success_WhenFeeEqualsLimit() (gas: 109194)
98+
FastTransferTokenPool_ccipSendToken_Test:test_ccipSendToken_FeeValidation_Success_WhenFeeWithinLimit() (gas: 109032)
99+
FastTransferTokenPool_ccipSendToken_Test:test_ccipSendToken_NativeFee() (gas: 129426)
100+
FastTransferTokenPool_ccipSendToken_Test:test_ccipSendToken_NativeFee_ToSVM() (gas: 170294)
101+
FastTransferTokenPool_ccipSendToken_Test:test_ccipSendToken_ValidReceiver(uint8,bytes32,bytes32) (runs: 256, μ: 135011, ~: 134445)
102+
FastTransferTokenPool_ccipSendToken_Test:test_ccipSendToken_WithERC20FeeToken() (gas: 150441)
103+
FastTransferTokenPool_ccipSendToken_Test:test_ccipSendToken_WithPoolFee() (gas: 156803)
103104
FastTransferTokenPool_constructor:test_Constructor() (gas: 23117)
104105
FastTransferTokenPool_constructor:test_GetAllowListedFillers() (gas: 16885)
105106
FastTransferTokenPool_constructor:test_GetDestChainConfig() (gas: 35757)
@@ -109,8 +110,8 @@ FastTransferTokenPool_fastFill_Test:test_FastFill() (gas: 96770)
109110
FastTransferTokenPool_fastFill_Test:test_FastFill_AllowlistDisabled() (gas: 287166)
110111
FastTransferTokenPool_fastFill_Test:test_FastFill_MultipleFillers() (gas: 359042)
111112
FastTransferTokenPool_fastFill_Test:test_FastFill_WithDifferentDecimals() (gas: 100800)
112-
FastTransferTokenPool_getCcipSendTokenFee_Test:test_GetCcipSendTokenFee() (gas: 37997)
113-
FastTransferTokenPool_getCcipSendTokenFee_Test:test_GetCcipSendTokenFee_WithNativeFeeToken() (gas: 35877)
113+
FastTransferTokenPool_getCcipSendTokenFee_Test:test_GetCcipSendTokenFee() (gas: 38181)
114+
FastTransferTokenPool_getCcipSendTokenFee_Test:test_GetCcipSendTokenFee_WithNativeFeeToken() (gas: 36061)
114115
FastTransferTokenPool_supportsInterface:test_supportsInterface() (gas: 11851)
115116
FastTransferTokenPool_updateDestChainConfig:test_updateDestChainConfig() (gas: 131236)
116117
FastTransferTokenPool_updateDestChainConfig:test_updateDestChainConfig_MaxFastFee() (gas: 124283)

chains/evm/contracts/pools/FastTransferTokenPoolAbstract.sol

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ abstract contract FastTransferTokenPoolAbstract is TokenPool, CCIPReceiver, ITyp
4141
error TransferAmountExceedsMaxFillAmount(uint64 remoteChainSelector, uint256 amount);
4242
error InsufficientPoolFees(uint256 requested, uint256 available);
4343
error QuoteFeeExceedsUserMaxLimit(uint256 quoteFee, uint256 maxFastTransferFee);
44+
error InvalidReceiver(bytes receiver);
4445

4546
event DestChainConfigUpdated(
4647
uint64 indexed destinationChainSelector,
@@ -230,7 +231,7 @@ abstract contract FastTransferTokenPoolAbstract is TokenPool, CCIPReceiver, ITyp
230231
address settlementFeeToken,
231232
bytes calldata
232233
) internal view virtual returns (InternalQuote memory internalQuote, Client.EVM2AnyMessage memory message) {
233-
_validateSendRequest(destinationChainSelector);
234+
_validateSendRequest(destinationChainSelector, receiver);
234235

235236
// Using storage here appears to be cheaper.
236237
DestChainConfig storage destChainConfig = s_fastTransferDestChainConfig[destinationChainSelector];
@@ -380,14 +381,46 @@ abstract contract FastTransferTokenPoolAbstract is TokenPool, CCIPReceiver, ITyp
380381
/// @notice Validates the send request parameters. Can be overridden by derived contracts to add additional checks.
381382
/// @param destinationChainSelector The destination chain selector.
382383
/// @dev Checks if the destination chain is allowed, if the sender is allowed, and if the RMN curse applies.
383-
function _validateSendRequest(
384-
uint64 destinationChainSelector
385-
) internal view virtual {
384+
function _validateSendRequest(uint64 destinationChainSelector, bytes calldata receiver) internal view virtual {
385+
_validateReceiver(receiver);
386+
386387
if (IRMN(i_rmnProxy).isCursed(bytes16(uint128(destinationChainSelector)))) revert CursedByRMN();
387388
_checkAllowList(msg.sender);
388389
if (!isSupportedChain(destinationChainSelector)) revert ChainNotAllowed(destinationChainSelector);
389390
}
390391

392+
/// @notice Validates receiver address parameters.
393+
/// @dev Checks length bounds (0 < length ≤ 64) and ensures receiver is not all zeros.
394+
/// @param receiver The receiver address to validate.
395+
function _validateReceiver(
396+
bytes calldata receiver
397+
) internal pure {
398+
uint256 receiverLength = receiver.length;
399+
if (receiverLength == 0 || receiverLength > 64) {
400+
revert InvalidReceiver(receiver);
401+
}
402+
403+
// Check if receiver is all zeros by scanning at most 2 32-byte words
404+
bool isNonZero = false;
405+
assembly {
406+
let dataPtr := receiver.offset
407+
// Load and check first 32 bytes
408+
if calldataload(dataPtr) { isNonZero := 1 }
409+
410+
if gt(receiverLength, 32) {
411+
// Load and check second 32 bytes only if receiver length > 32
412+
// Note: dataPtr + 32 may exceed the actual receiver data bounds (e.g., for 40-byte receiver,
413+
// this reads bytes [32, 64) where [40, 64) is out-of-bounds). However, this is safe because
414+
// calldata is ABI-encoded with zero-padding to 32-byte boundaries, so out-of-bounds bytes are zeros.
415+
if calldataload(add(dataPtr, 32)) { isNonZero := 1 }
416+
}
417+
}
418+
419+
if (!isNonZero) {
420+
revert InvalidReceiver(receiver);
421+
}
422+
}
423+
391424
/// @notice Validates settlement prerequisites. Can be overridden by derived contracts to add additional checks.
392425
/// @param sourceChainSelector The source chain selector.
393426
/// @param sourcePoolAddress The source pool address.

chains/evm/contracts/test/pools/FastTransferTokenPool/FastTransferTokenPool.ccipSendToken.t.sol

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,53 @@ contract FastTransferTokenPool_ccipSendToken_Test is FastTransferTokenPoolSetup
337337
assertEq(s_token.balanceOf(OWNER), balanceBefore - SOURCE_AMOUNT);
338338
}
339339

340+
function test_ccipSendToken_RevertWhen_ReceiverIsEmptyBytes() public {
341+
// Setup: Empty receiver address
342+
bytes memory emptyReceiver = "";
343+
344+
vm.expectRevert(abi.encodeWithSelector(FastTransferTokenPoolAbstract.InvalidReceiver.selector, emptyReceiver));
345+
s_pool.ccipSendToken{value: 1 ether}(DEST_CHAIN_SELECTOR, SOURCE_AMOUNT, 1 ether, emptyReceiver, address(0), "");
346+
}
347+
348+
function test_ccipSendToken_RevertWhen_ReceiverExceedsMaxLength() public {
349+
// Setup: Receiver longer than 64 bytes (65 bytes)
350+
bytes memory oversizedReceiver = new bytes(65);
351+
// Fill with non-zero data to ensure it's not rejected for being all zeros
352+
for (uint256 i = 0; i < 65; i++) {
353+
oversizedReceiver[i] = bytes1(uint8(i + 1));
354+
}
355+
356+
vm.expectRevert(abi.encodeWithSelector(FastTransferTokenPoolAbstract.InvalidReceiver.selector, oversizedReceiver));
357+
s_pool.ccipSendToken{value: 1 ether}(DEST_CHAIN_SELECTOR, SOURCE_AMOUNT, 1 ether, oversizedReceiver, address(0), "");
358+
}
359+
360+
function test_ccipSendToken_RevertWhen_ReceiverIsAllZeros() public {
361+
// Setup: 20-byte receiver address filled with zeros (typical Ethereum address length)
362+
bytes memory zeroReceiver20 = new bytes(20);
363+
// bytes constructor already initializes to zeros
364+
365+
vm.expectRevert(abi.encodeWithSelector(FastTransferTokenPoolAbstract.InvalidReceiver.selector, zeroReceiver20));
366+
s_pool.ccipSendToken{value: 1 ether}(DEST_CHAIN_SELECTOR, SOURCE_AMOUNT, 1 ether, zeroReceiver20, address(0), "");
367+
368+
// Setup: 32-byte receiver address filled with zeros (one full word)
369+
bytes memory zeroReceiver32 = new bytes(32);
370+
371+
vm.expectRevert(abi.encodeWithSelector(FastTransferTokenPoolAbstract.InvalidReceiver.selector, zeroReceiver32));
372+
s_pool.ccipSendToken{value: 1 ether}(DEST_CHAIN_SELECTOR, SOURCE_AMOUNT, 1 ether, zeroReceiver32, address(0), "");
373+
374+
// Setup: 40-byte receiver address filled with zeros
375+
bytes memory zeroReceiver40 = new bytes(40);
376+
377+
vm.expectRevert(abi.encodeWithSelector(FastTransferTokenPoolAbstract.InvalidReceiver.selector, zeroReceiver40));
378+
s_pool.ccipSendToken{value: 1 ether}(DEST_CHAIN_SELECTOR, SOURCE_AMOUNT, 1 ether, zeroReceiver40, address(0), "");
379+
380+
// Setup: 64-byte receiver address filled with zeros (maximum allowed length)
381+
bytes memory zeroReceiver64 = new bytes(64);
382+
383+
vm.expectRevert(abi.encodeWithSelector(FastTransferTokenPoolAbstract.InvalidReceiver.selector, zeroReceiver64));
384+
s_pool.ccipSendToken{value: 1 ether}(DEST_CHAIN_SELECTOR, SOURCE_AMOUNT, 1 ether, zeroReceiver64, address(0), "");
385+
}
386+
340387
function test_ccipSendToken_RevertWhen_FeeExceedsUserMaxLimit() public {
341388
// Setup: Calculate expected fee and set max limit lower
342389
uint256 expectedFee = (SOURCE_AMOUNT * FAST_FEE_FILLER_BPS) / 10_000; // 1% of 100 ether = 1 ether
@@ -511,4 +558,30 @@ contract FastTransferTokenPool_ccipSendToken_Test is FastTransferTokenPoolSetup
511558

512559
vm.startPrank(OWNER);
513560
}
561+
562+
// This test achieves higher input space coverage than setting 1 non-zero byte at random index
563+
function test_ccipSendToken_ValidReceiver(uint8 receiverLength, bytes32 receiverHead, bytes32 receiverTail) public {
564+
receiverLength = uint8(bound(receiverLength, 1, 64));
565+
// Combine the 2 halves into bytes of length 64
566+
bytes memory validReceiver = abi.encodePacked(receiverHead, receiverTail);
567+
// Set bytes array length to target receiver length
568+
assembly {
569+
mstore(validReceiver, receiverLength)
570+
}
571+
572+
// Throw out all-zero receiver
573+
vm.assume(keccak256(validReceiver) != keccak256(new bytes(receiverLength)));
574+
575+
TestParams memory params = TestParams({
576+
chainSelector: DEST_CHAIN_SELECTOR,
577+
fastFeeBpsExpected: FAST_FEE_FILLER_BPS,
578+
amount: SOURCE_AMOUNT,
579+
mockMessageId: keccak256("mockMessageId"),
580+
receiver: validReceiver
581+
});
582+
bytes memory extraArgs = Client._argsToBytes(
583+
Client.GenericExtraArgsV2({gasLimit: SETTLEMENT_GAS_OVERHEAD, allowOutOfOrderExecution: true})
584+
);
585+
_executeTest(params, extraArgs);
586+
}
514587
}

chains/evm/gobindings/generated/latest/fast_transfer_token_pool/fast_transfer_token_pool.go

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

chains/evm/gobindings/generation/generated-wrapper-dependency-versions-do-not-edit.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ cctp_message_transmitter_proxy: ../solc/ccip/CCTPMessageTransmitterProxy/CCTPMes
1111
don_id_claimer: ../solc/ccip/DonIDClaimer/DonIDClaimer.sol/DonIDClaimer.abi.json ../solc/ccip/DonIDClaimer/DonIDClaimer.sol/DonIDClaimer.bin 2ef6cba2f8e258c9d6f2dd55f8d2fc59ae5f686af609ed7d298e8ae9c3923448
1212
ether_sender_receiver: ../solc/ccip/EtherSenderReceiver/EtherSenderReceiver.sol/EtherSenderReceiver.abi.json ../solc/ccip/EtherSenderReceiver/EtherSenderReceiver.sol/EtherSenderReceiver.bin c862bb4e4aec2f889fae514872f1f287cc2bcdc2870e9134ffa3bdfd806ffb67
1313
factory_burn_mint_erc20: ../solc/ccip/FactoryBurnMintERC20/FactoryBurnMintERC20.sol/FactoryBurnMintERC20.abi.json ../solc/ccip/FactoryBurnMintERC20/FactoryBurnMintERC20.sol/FactoryBurnMintERC20.bin 5c4d7396a6fb8fd5087d28e15c64785426609670beb681547e7f924fb32c5f14
14-
fast_transfer_token_pool: ../solc/ccip/BurnMintFastTransferTokenPool/BurnMintFastTransferTokenPool.sol/BurnMintFastTransferTokenPool.abi.json ../solc/ccip/BurnMintFastTransferTokenPool/BurnMintFastTransferTokenPool.sol/BurnMintFastTransferTokenPool.bin 4dbe97fdc6e9977304c33239c448a6c2b3d62a85f106b232acdf5132ebd46c92
14+
fast_transfer_token_pool: ../solc/ccip/BurnMintFastTransferTokenPool/BurnMintFastTransferTokenPool.sol/BurnMintFastTransferTokenPool.abi.json ../solc/ccip/BurnMintFastTransferTokenPool/BurnMintFastTransferTokenPool.sol/BurnMintFastTransferTokenPool.bin 589df528573ce855f562236e5be48b71143ecb99214d6dc0b48c284ba300e157
1515
fee_quoter: ../solc/ccip/FeeQuoter/FeeQuoter.sol/FeeQuoter.abi.json ../solc/ccip/FeeQuoter/FeeQuoter.sol/FeeQuoter.bin 9c5997da2f7d38a98b2acba22146fa73f964139b29fae4996be708f34a4e9878
1616
hybrid_lock_release_usdc_token_pool: ../solc/ccip/HybridLockReleaseUSDCTokenPool/HybridLockReleaseUSDCTokenPool.sol/HybridLockReleaseUSDCTokenPool.abi.json ../solc/ccip/HybridLockReleaseUSDCTokenPool/HybridLockReleaseUSDCTokenPool.sol/HybridLockReleaseUSDCTokenPool.bin 914ea0b94e07a336e993f16422f5039f76db7c1949832f3fb29c4b69aabbd1ba
1717
lock_release_token_pool: ../solc/ccip/LockReleaseTokenPool/LockReleaseTokenPool.sol/LockReleaseTokenPool.abi.json ../solc/ccip/LockReleaseTokenPool/LockReleaseTokenPool.sol/LockReleaseTokenPool.bin a7db62ec9955f99c077f7746dd2a4fe327f474d8f72ece9833e15525e7b182ed

chains/evm/scripts/compile_all

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ echo " └───────────────────────
1010
# as specified in the foundry.toml.
1111
OPTIMIZE_RUNS_OFFRAMP=800
1212
OPTIMIZE_RUNS_FEE_QUOTER=8000
13-
OPTIMIZE_RUNS_BURN_MINT_FAST_TRASNFER_TOKEN_POOL=8000
13+
OPTIMIZE_RUNS_BURN_MINT_FAST_TRASNFER_TOKEN_POOL=5000
1414
PROJECT="ccip"
1515
FOUNDRY_PROJECT_SUFFIX="-compile"
1616
export FOUNDRY_PROFILE="$PROJECT"$FOUNDRY_PROJECT_SUFFIX

0 commit comments

Comments
 (0)