Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Fixed

- `CCIPLocalSimulatorFork` now ABI-encodes `sender` as a 32-byte word in v1.6 messages, matching production encoding and preventing receiver-side `abi.decode` reverts. ([#62](https://github.com/smartcontractkit/chainlink-local/issues/62), [#63](https://github.com/smartcontractkit/chainlink-local/pull/63))


## [0.2.9] - 19 May 2026

### Dependencies
Expand Down
2 changes: 1 addition & 1 deletion src/ccip/CCIPLocalSimulatorFork.sol
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,7 @@ contract CCIPLocalSimulatorFork is Test {
}
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,
Expand Down
90 changes: 88 additions & 2 deletions test/unit/ccip/CCIPLocalSimulatorForkRouting.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import {
IOffRampSourceConfigFork,
IEVM2EVMOffRampStaticConfigFork
} from "../../../src/ccip/CCIPLocalSimulatorFork.sol";
import {CCIPReceiver} from "@chainlink/contracts-ccip/contracts/applications/CCIPReceiver.sol";
import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol";
import {Internal} from "@chainlink/contracts-ccip/contracts/libraries/Internal.sol";

/// @dev Exposes internal OffRamp resolution for unit testing.
contract CCIPLocalSimulatorForkHarness is CCIPLocalSimulatorFork {
Expand All @@ -26,6 +29,13 @@ contract CCIPLocalSimulatorForkHarness is CCIPLocalSimulatorFork {
}
return _findOffRampForOnRamp(ramps, sourceChainSelector, sourceOnRamp);
}

function exposedExecutePostV1dot6(
address offRamp,
Internal.EVM2AnyRampMessage memory message
) external returns (bool) {
return _executePostV1dot6(offRamp, message);
}
}

/// @dev v1.6-style OffRamp mock: `getSourceChainConfig` returns a fixed lane binding.
Expand Down Expand Up @@ -106,6 +116,42 @@ contract MockOffRampV16WrongThenPre16 is MockOffRampPre16 {
}
}

contract SenderEncodingReceiver is CCIPReceiver {
address public lastSender;
bytes public lastSenderRaw;
string public lastMessage;

constructor(address router) CCIPReceiver(router) {}

function _ccipReceive(Client.Any2EVMMessage memory message) internal override {
lastSenderRaw = message.sender;
lastSender = abi.decode(message.sender, (address));
lastMessage = abi.decode(message.data, (string));
}
}

contract MockOffRampCapture {
bytes internal s_lastSenderBytes;
bool internal s_called;

function executeSingleMessage(
Internal.Any2EVMRampMessage memory message,
bytes[] memory,
uint32[] memory
) external {
s_lastSenderBytes = message.sender;
s_called = true;
}

function wasCalled() external view returns (bool) {
return s_called;
}

function lastSenderBytes() external view returns (bytes memory) {
return s_lastSenderBytes;
}
}

/// @dev No introspection getters — `_findOffRampForOnRamp` skips these via try/catch.
contract MockOffRampForeign {}

Expand All @@ -119,12 +165,12 @@ contract CCIPLocalSimulatorForkRoutingTest is Test {
harness = new CCIPLocalSimulatorForkHarness();
}

function test_decodeReceiver_abiEncodedAddress() public {
function test_decodeReceiver_abiEncodedAddress() public view {
address a = address(0x1234567890123456789012345678901234567890);
assertEq(harness.exposedDecodeReceiver(abi.encode(a)), a);
}

function test_decodeReceiver_twentyByteRaw() public {
function test_decodeReceiver_twentyByteRaw() public view {
address a = address(0x1234567890123456789012345678901234567890);
assertEq(harness.exposedDecodeReceiver(abi.encodePacked(a)), a);
}
Expand Down Expand Up @@ -188,4 +234,44 @@ contract CCIPLocalSimulatorForkRoutingTest is Test {

assertEq(harness.exposedFindOffRamp(ramps, SOURCE_SELECTOR, sourceOnRamp), enabled);
}

// =============================================================
// SENDER ENCODING REGRESSION TEST
// =============================================================

/// @dev Membuktikan bahwa `sender` di pesan v1.6 di-ABI-encode sebagai 32-byte word.
/// Kalau `_executePostV1dot6` memakai `abi.encodePacked`, `lastSenderBytes()` akan
/// panjangnya 20 byte, dan test ini gagal.
function test_executePostV1dot6_senderIs32ByteABIWord() public {
MockOffRampCapture mockOffRamp = new MockOffRampCapture();
address expectedSender = 0x2e234DAe75C793f67A35089C9d99245E1C58470b;

Internal.EVM2AnyRampMessage memory message = Internal.EVM2AnyRampMessage({
header: Internal.RampMessageHeader({
messageId: bytes32(uint256(1)),
sourceChainSelector: SOURCE_SELECTOR,
destChainSelector: DEST_CHAIN_SELECTOR,
sequenceNumber: 1,
nonce: 0
}),
sender: expectedSender, // EVM2AnyRampMessage.sender bertipe address
data: abi.encode("hello v1.6"),
receiver: abi.encode(address(0xCAFE)), // receiver bertipe bytes
extraArgs: "",
feeToken: address(0),
feeTokenAmount: 0,
feeValueJuels: 0,
tokenAmounts: new Internal.EVM2AnyTokenTransfer[](0)
});

harness.exposedExecutePostV1dot6(address(mockOffRamp), message);

assertTrue(mockOffRamp.wasCalled(), "offRamp.executeSingleMessage must be called");
assertEq(
mockOffRamp.lastSenderBytes().length,
32,
"sender must be 32-byte ABI word (abi.encode), not 20-byte (abi.encodePacked)"
);
}
}