Summary
For v1.6+ messages (CCIPMessageSent), _executePostV1dot6 reconstructs the inbound message with a packed, 20-byte sender:
|
sender: abi.encodePacked(message.sender), |
sender: abi.encodePacked(message.sender),
On a production v1.6 lane with an EVM source chain that field is a 32-byte ABI word.
v0.2.9 fixed the same 20-vs-32-byte mismatch for receiver, destTokenAddress and sourcePoolAddress in this struct construction; sender was left unchanged, and is unchanged on main. #52 covered receiver and destTokenAddress only.
The pre-v1.6 path is unaffected: InternalPreV1dot6.EVM2EVMMessage.sender is an address and the v1.5 OffRamp does the encoding itself.
Impact
- A receiver calling
abi.decode(message.sender, (address)) reverts. The OffRamp wraps it as ReceiverError(bytes) (0x0a8d6e8c) with empty inner error data, and _executePostV1dot6 catches the revert, logs the bytes and returns false. The test observes an undelivered message rather than a failed assertion.
- A receiver comparing the raw bytes against
abi.encode(trustedRemote) sees a mismatch and takes its untrusted-sender branch.
Why
Client.Any2EVMMessage.sender is documented as bytes sender; // abi.decode(sender) if coming from an EVM chain. OffRamp.executeSingleMessage copies Internal.Any2EVMRampMessage.sender into it unchanged.
NonceManager._getInboundNonce calls abi.decode(sender, (address)) on those bytes in the previous-OffRamp fallback.
ccip/test/e2e/End2End.t.sol performs the same EVM2AnyRampMessage to Any2EVMRampMessage conversion as _executePostV1dot6, using sender: abi.encode(msgEvent.sender).
Reproduction
Attached SenderEncodingTest.md. It forks Ethereum Sepolia (block 11601000) to Base Sepolia; that lane is served by OnRamp 1.6.0 at 0x23a5084Fa78104F3DF11C63Ae59fcac4f6AD9DeE, so switchChainAndRouteMessage takes the _executePostV1dot6 path. The message carries no tokens, so only the sender encoding is under test.
On v0.2.9:
[FAIL] test_decodingReceiverIsReached()
Logs:
0x0a8d6e8c00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000
receivedCount: 0
message not delivered: 0 != 1
[FAIL: EvmError: Revert] test_senderIsAbiEncoded()
Logs:
receivedCount: 1
sender: 0xcd1722f3947def4cf144679da39c4c32bdc35681
sender length: 20 != 32
Proposed fix
--- a/src/ccip/CCIPLocalSimulatorFork.sol
+++ b/src/ccip/CCIPLocalSimulatorFork.sol
@@ -415,7 +415,7 @@
}
Internal.Any2EVMRampMessage memory any2EVMRampMessage = Internal.Any2EVMRampMessage({
header: message.header,
- sender: abi.encodePacked(message.sender),
+ sender: abi.encode(message.sender),
data: message.data,
receiver: _decodeEVMAddress(message.receiver),
gasLimit: gasLimit,
Both tests pass with that change:
[PASS] test_decodingReceiverIsReached()
Logs:
receivedCount: 1
[PASS] test_senderIsAbiEncoded()
Logs:
receivedCount: 1
sender: 0x000000000000000000000000cd1722f3947def4cf144679da39c4c32bdc35681
A message carrying a CCIP-BnM token transfer on the same lane also delivers correctly with the fix.
Environment
chainlink-local v0.2.9 (f8c0efe8685660dac07e08f4558f1b578ae991aa), @chainlink/contracts-ccip 1.6.0, forge 1.7.1, solc 0.8.36.
Summary
For v1.6+ messages (
CCIPMessageSent),_executePostV1dot6reconstructs the inbound message with a packed, 20-byte sender:chainlink-local/src/ccip/CCIPLocalSimulatorFork.sol
Line 418 in f8c0efe
sender: abi.encodePacked(message.sender),On a production v1.6 lane with an EVM source chain that field is a 32-byte ABI word.
v0.2.9 fixed the same 20-vs-32-byte mismatch for
receiver,destTokenAddressandsourcePoolAddressin this struct construction;senderwas left unchanged, and is unchanged onmain. #52 coveredreceiveranddestTokenAddressonly.The pre-v1.6 path is unaffected:
InternalPreV1dot6.EVM2EVMMessage.senderis anaddressand the v1.5 OffRamp does the encoding itself.Impact
abi.decode(message.sender, (address))reverts. The OffRamp wraps it asReceiverError(bytes)(0x0a8d6e8c) with empty inner error data, and_executePostV1dot6catches the revert, logs the bytes and returnsfalse. The test observes an undelivered message rather than a failed assertion.abi.encode(trustedRemote)sees a mismatch and takes its untrusted-sender branch.Why
Client.Any2EVMMessage.senderis documented asbytes sender; // abi.decode(sender) if coming from an EVM chain.OffRamp.executeSingleMessagecopiesInternal.Any2EVMRampMessage.senderinto it unchanged.NonceManager._getInboundNoncecallsabi.decode(sender, (address))on those bytes in the previous-OffRamp fallback.ccip/test/e2e/End2End.t.solperforms the sameEVM2AnyRampMessagetoAny2EVMRampMessageconversion as_executePostV1dot6, usingsender: abi.encode(msgEvent.sender).Reproduction
Attached
SenderEncodingTest.md. It forks Ethereum Sepolia (block 11601000) to Base Sepolia; that lane is served byOnRamp 1.6.0at0x23a5084Fa78104F3DF11C63Ae59fcac4f6AD9DeE, soswitchChainAndRouteMessagetakes the_executePostV1dot6path. The message carries no tokens, so only the sender encoding is under test.On v0.2.9:
Proposed fix
Both tests pass with that change:
A message carrying a CCIP-BnM token transfer on the same lane also delivers correctly with the fix.
Environment
chainlink-local v0.2.9 (
f8c0efe8685660dac07e08f4558f1b578ae991aa),@chainlink/contracts-ccip1.6.0, forge 1.7.1, solc 0.8.36.