Skip to content

Bug: CCIPLocalSimulatorFork builds Any2EVMRampMessage.sender with abi.encodePacked (20 bytes) for v1.6 messages #62

Description

@Yurii3721

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions