Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
85fb7f7
feat: Add Mock Data Feeds contracts
andrejrakic Jun 8, 2024
d79388c
feat: Add MockOffchainAggregator contract that expose minAnswer and m…
andrejrakic Jun 8, 2024
0f5e71f
chore: Forge install @chainlink/contracts@v1.1.1
andrejrakic Jun 8, 2024
2bb3a03
feat: Add mock data feeds smoke test
andrejrakic Jun 8, 2024
f1e3c14
chore: List upcoming release in Changelog
andrejrakic Jun 8, 2024
8235aaf
feat: Add unit tests for mock data feeds contracts
andrejrakic Jun 23, 2024
c5780d3
feat: Add smoke test for mock data feeds using Hardhat
andrejrakic Jun 23, 2024
3318bab
feat: Add forking test example and prepare for beta release
andrejrakic Jun 24, 2024
eae01b6
Merge branch 'main' into develop
andrejrakic Aug 13, 2024
54ff7b0
Support for CCIP v1.5 and preparing for 0.2.2-beta release (#19)
andrejrakic Sep 12, 2024
32543f6
chore: Add CCIP v1.5 config details to Register for all available tes…
andrejrakic Oct 4, 2024
e601b3b
fix: Use the latest version of EVM2EVMOffRamp contract in the switchC…
andrejrakic Oct 10, 2024
be7007e
chore: Prepare repo for the 0.2.2 release
andrejrakic Oct 15, 2024
ffcb38e
Merge branch 'main' into develop
andrejrakic Nov 14, 2024
2d294dc
Merge branch 'main' into develop
andrejrakic Dec 10, 2024
a9d3fff
Support for Chainlink Data Streams (#25)
andrejrakic Dec 10, 2024
6620d59
chore: Include JavaScript Data Strems scripts into package.json
andrejrakic Dec 10, 2024
d43f3e4
Update README to include installation instructions for Foundry (solde…
EmanHerawy Jan 28, 2025
1243ad3
Data Streams fixes - v0.2.4-beta.0 release candidate (#34)
andrejrakic Feb 23, 2025
649b3b9
Fix year in CHANGELOG (#35)
andrejrakic Feb 23, 2025
eb2fca7
fix: Fix incorrect import path (#36)
andrejrakic Feb 23, 2025
08f53fc
chore: Prepare for 0.2.4 release
andrejrakic Mar 25, 2025
bf41d58
Merge branch 'main' into develop
andrejrakic May 9, 2025
b429481
Add support for CCIP v1.6 (#38)
andrejrakic May 14, 2025
46faf3a
fix: Update npm authentication token in publish workflows
andrejrakic May 16, 2025
9fbeafb
chore: Prepare for v0.2.5-beta.0 release, bump @chainlink/contracts t…
andrejrakic May 20, 2025
92fd9ac
chore: Prepare for 0.2.5 release
andrejrakic Jun 10, 2025
0769abf
V0.2.6-beta release (#41)
andrejrakic Jun 11, 2025
6caea04
Merge branch 'main' into develop
andrejrakic Sep 10, 2025
c71aa37
CCIP Fork: Route multiple messages to multiple chains at once (#45)
andrejrakic Sep 10, 2025
e318825
chore: Prepare repo for 0.2.6 release
andrejrakic Sep 18, 2025
cf78654
Merge branch 'main' into develop
andrejrakic Oct 2, 2025
585ae93
chore: Bump @chainlink/contracts to v1.5.0 and @chainlink/contracts-c…
andrejrakic Oct 3, 2025
1263371
DEVREL-144 : Data Streams billing mechanism (#47)
andrejrakic Oct 9, 2025
34d88d8
fix: Update Node.js version to 22 in publish workflow and refine vers…
andrejrakic Oct 10, 2025
b50710a
chore: Prepare for v0.2.7 release
andrejrakic Nov 9, 2025
382c10a
DEVREL-155: Auto populate Register.sol (#51)
Nalon Jan 14, 2026
2600e9d
Improved update register helper
Nalon Mar 5, 2026
51503ca
feat: Add Data Stream Report Schemas V1, V5-V13 (#55)
andrejrakic May 5, 2026
691ce48
chore: Prepare for v0.2.8 release
andrejrakic May 5, 2026
8453f1a
Merge branch 'main' into develop
andrejrakic May 5, 2026
78fe544
Merge branch 'main' into develop
andrejrakic May 6, 2026
643284b
fix: Improve the algorithm for handling multiple offramps in ccipLoca…
andrejrakic May 6, 2026
f7ede06
Correct v1.6 destTokenAddress and sourcePoolAddress encoding (#60)
andrejrakic May 7, 2026
f33cc51
chore: Prepare repo for v0.2.9 release
andrejrakic May 19, 2026
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
45 changes: 45 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,48 @@ 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).

## [0.2.9] - 19 May 2026

### Dependencies

| Package | Version |
| ------------------------- | ------- |
| @chainlink/contracts-ccip | 1.6.2 |
| @chainlink/contracts | 1.5.0 |

### Fixed

- `CCIPLocalSimulatorFork.switchChainAndRouteMessage` now pairs the destination OffRamp with the source OnRamp by reading `getSourceChainConfig` (v1.6+) and `getStaticConfig` (pre-v1.6), and falls back to trying other OffRamps with the same `sourceChainSelector` when execution fails or no deterministic match is found.
- `CCIPLocalSimulatorFork.switchChainAndRouteMessage` now correctly decodes `destTokenAddress` for v1.6 token transfers. The 32-byte `abi.encode(address)` value was previously truncated via a `bytes20` cast, yielding a garbage address and causing the destination OffRamp's `TokenAdminRegistry.getPool` lookup to revert. Decoding now handles both 32-byte ABI-encoded and 20-byte packed forms, matching production OffRamp behavior, and is shared with `receiver` decoding via a single internal `_decodeEVMAddress` helper.
- For v1.6 token transfers, `sourcePoolAddress` is now passed to `Internal.Any2EVMTokenTransfer` as `abi.encode(address)` (32-byte word), matching production OnRamp output and destination pool validation. The simulator previously used `abi.encodePacked(address)` (20 bytes), which caused compatible pools to revert with `InvalidSourcePoolAddress` during fork testing.

## [0.2.9-beta.0] - 7 May 2026

### Dependencies

| Package | Version |
| ------------------------- | ------- |
| @chainlink/contracts-ccip | 1.6.2 |
| @chainlink/contracts | 1.5.0 |

### Fixed

- `CCIPLocalSimulatorFork.switchChainAndRouteMessage` now correctly decodes `destTokenAddress` for v1.6 token transfers. The 32-byte `abi.encode(address)` value was previously truncated via a `bytes20` cast, yielding a garbage address and causing the destination OffRamp's `TokenAdminRegistry.getPool` lookup to revert. Decoding now handles both 32-byte ABI-encoded and 20-byte packed forms, matching production OffRamp behavior, and is shared with `receiver` decoding via a single internal `_decodeEVMAddress` helper.
- For v1.6 token transfers, `sourcePoolAddress` is now passed to `Internal.Any2EVMTokenTransfer` as `abi.encode(address)` (32-byte word), matching production OnRamp output and destination pool validation. The simulator previously used `abi.encodePacked(address)` (20 bytes), which caused compatible pools to revert with `InvalidSourcePoolAddress` during fork testing.

## [0.2.9-beta] - 6 May 2026

### Dependencies

| Package | Version |
| ------------------------- | ------- |
| @chainlink/contracts-ccip | 1.6.2 |
| @chainlink/contracts | 1.5.0 |

### Fixed

- `CCIPLocalSimulatorFork.switchChainAndRouteMessage` now pairs the destination OffRamp with the source OnRamp by reading `getSourceChainConfig` (v1.6+) and `getStaticConfig` (pre-v1.6), and falls back to trying other OffRamps with the same `sourceChainSelector` when execution fails or no deterministic match is found.

## [0.2.8] - 5 May 2026

### Dependencies
Expand Down Expand Up @@ -637,3 +679,6 @@ and this project adheres to
[0.2.7]: https://github.com/smartcontractkit/chainlink-local/releases/tag/v0.2.7
[0.2.8-beta]: https://github.com/smartcontractkit/chainlink-local/releases/tag/v0.2.8-beta
[0.2.8]: https://github.com/smartcontractkit/chainlink-local/releases/tag/v0.2.8
[0.2.9-beta]: https://github.com/smartcontractkit/chainlink-local/releases/tag/v0.2.9-beta
[0.2.9-beta.0]: https://github.com/smartcontractkit/chainlink-local/releases/tag/v0.2.9-beta.0
[0.2.9]: https://github.com/smartcontractkit/chainlink-local/releases/tag/v0.2.9
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@chainlink/local",
"description": "Chainlink Local Simulator",
"license": "MIT",
"version": "0.2.8",
"version": "0.2.9",
"files": [
"src/**/*.sol",
"!src/test/**/*",
Expand Down
243 changes: 185 additions & 58 deletions src/ccip/CCIPLocalSimulatorFork.sol
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,37 @@ interface IEVM2EVMOffRampPreV1dot6Fork {
) external;
}

/// @title IEVM2EVMOffRampStaticConfigFork
/// @notice Minimal view surface for pre-v1.6 OffRamp static config (EVM2EVMOffRamp 1.5.x).
interface IEVM2EVMOffRampStaticConfigFork {
struct StaticConfig {
address commitStore;
uint64 chainSelector;
uint64 sourceChainSelector;
address onRamp;
address prevOffRamp;
address rmnProxy;
address tokenAdminRegistry;
}

function getStaticConfig() external view returns (StaticConfig memory);
}

/// @title IOffRampSourceConfigFork
/// @notice Minimal view surface for v1.6+ OffRamp per-source-chain config (OffRamp 1.6.x).
/// @dev `router` is ABI-compatible with `IRouter` in CCIP OffRamp `SourceChainConfig`.
interface IOffRampSourceConfigFork {
struct SourceChainConfig {
address router;
bool isEnabled;
uint64 minSeqNr;
bool isRMNVerificationDisabled;
bytes onRamp;
}

function getSourceChainConfig(uint64 sourceChainSelector) external view returns (SourceChainConfig memory);
}

/// @title CCIPLocalSimulatorFork
/// @notice Works with Foundry only
contract CCIPLocalSimulatorFork is Test {
Expand All @@ -89,6 +120,8 @@ contract CCIPLocalSimulatorFork is Test {

error InvalidExtraArgsTag();

error InvalidEVMAddressEncoding(bytes encodedAddress);

uint32 public constant DEFAULT_GAS_LIMIT = 200_000;

/// @notice The immutable register instance
Expand Down Expand Up @@ -228,28 +261,29 @@ contract CCIPLocalSimulatorFork is Test {
IRouterFork(i_register.getNetworkDetails(block.chainid).routerAddress).getOffRamps();
uint256 offRampsLength = offRamps.length;

for (uint256 k = offRampsLength; k > 0; --k) {
if (offRamps[k - 1].sourceChainSelector == message.sourceChainSelector) {
vm.startPrank(offRamps[k - 1].offRamp);
uint256 numberOfTokens = message.tokenAmounts.length;
bytes[] memory offchainTokenData = new bytes[](numberOfTokens);
uint32[] memory tokenGasOverrides = new uint32[](numberOfTokens);
for (uint256 l; l < numberOfTokens; ++l) {
tokenGasOverrides[l] = uint32(message.gasLimit);
address matchedOffRamp =
_findOffRampForOnRamp(offRamps, message.sourceChainSelector, onRampContract);
bool routed = false;
if (matchedOffRamp != address(0)) {
routed = _executePreV1dot6(matchedOffRamp, message);
}
if (!routed) {
for (uint256 k = offRampsLength; k > 0; --k) {
if (offRamps[k - 1].sourceChainSelector != message.sourceChainSelector) {
continue;
}
if (matchedOffRamp != address(0) && offRamps[k - 1].offRamp == matchedOffRamp) {
continue;
}
try IEVM2EVMOffRampPreV1dot6Fork(offRamps[k - 1].offRamp).executeSingleMessage(
message, offchainTokenData, tokenGasOverrides
) {
vm.stopPrank();
s_processedMessages[message.messageId] = true;
} catch (bytes memory err) {
vm.stopPrank();
// Solidity does not support yet catching custom errors, so this is the best we can do for now
console2.logBytes(err);
if (_executePreV1dot6(offRamps[k - 1].offRamp, message)) {
routed = true;
break;
}
break;
}
}
if (routed) {
s_processedMessages[message.messageId] = true;
}
}
}
}
Expand All @@ -260,8 +294,6 @@ contract CCIPLocalSimulatorFork is Test {
Internal.EVM2AnyRampMessage memory message = abi.decode(entries[i].data, (Internal.EVM2AnyRampMessage));

if (!s_processedMessages[message.header.messageId]) {
s_processedMessages[message.header.messageId] = true;

for (uint256 j; j < forkIds.length; ++j) {
vm.selectFork(forkIds[j]);
uint64 destinationChainSelector = i_register.getNetworkDetails(block.chainid).chainSelector;
Expand All @@ -277,56 +309,151 @@ contract CCIPLocalSimulatorFork is Test {
IRouterFork(i_register.getNetworkDetails(block.chainid).routerAddress).getOffRamps();
uint256 offRampsLength = offRamps.length;

for (uint256 k = offRampsLength; k > 0; --k) {
if (offRamps[k - 1].sourceChainSelector == message.header.sourceChainSelector) {
vm.startPrank(offRamps[k - 1].offRamp);
uint256 gasLimit = _fromBytes(message.extraArgs).gasLimit;
uint256 numberOfTokens = message.tokenAmounts.length;
Internal.Any2EVMTokenTransfer[] memory tokenAmounts =
new Internal.Any2EVMTokenTransfer[](numberOfTokens);
for (uint256 l; l < numberOfTokens; ++l) {
tokenAmounts[l] = Internal.Any2EVMTokenTransfer({
sourcePoolAddress: abi.encodePacked(message.tokenAmounts[l].sourcePoolAddress),
destTokenAddress: address(
uint160(bytes20(message.tokenAmounts[l].destTokenAddress))
),
destGasAmount: abi.decode(message.tokenAmounts[l].destExecData, (uint32)),
extraData: message.tokenAmounts[l].extraData,
amount: message.tokenAmounts[l].amount
});
address matchedOffRamp =
_findOffRampForOnRamp(offRamps, message.header.sourceChainSelector, onRampContract);
bool routed = false;
if (matchedOffRamp != address(0)) {
routed = _executePostV1dot6(matchedOffRamp, message);
}
if (!routed) {
for (uint256 k = offRampsLength; k > 0; --k) {
if (offRamps[k - 1].sourceChainSelector != message.header.sourceChainSelector) {
continue;
}
Internal.Any2EVMRampMessage memory any2EVMRampMessage = Internal.Any2EVMRampMessage({
header: message.header,
sender: abi.encodePacked(message.sender),
data: message.data,
receiver: address(uint160(bytes20(message.receiver))),
gasLimit: gasLimit,
tokenAmounts: tokenAmounts
});
bytes[] memory offchainTokenData = new bytes[](numberOfTokens);
uint32[] memory tokenGasOverrides = new uint32[](numberOfTokens);
for (uint256 l; l < numberOfTokens; ++l) {
tokenGasOverrides[l] = uint32(gasLimit);
if (matchedOffRamp != address(0) && offRamps[k - 1].offRamp == matchedOffRamp) {
continue;
}
try IEVM2EVMOffRampFork(offRamps[k - 1].offRamp).executeSingleMessage(
any2EVMRampMessage, offchainTokenData, tokenGasOverrides
) {
vm.stopPrank();
} catch (bytes memory err) {
vm.stopPrank();
// Solidity does not support yet catching custom errors, so this is the best we can do for now
console2.logBytes(err);
if (_executePostV1dot6(offRamps[k - 1].offRamp, message)) {
routed = true;
break;
}
break;
}
}
if (routed) {
s_processedMessages[message.header.messageId] = true;
}
}
}
}
}
}
}

/// @notice Returns the destination OffRamp whose lane is configured for `sourceOnRamp`, if discoverable.
function _findOffRampForOnRamp(
IRouterFork.OffRamp[] memory offRamps,
uint64 sourceChainSelector,
address sourceOnRamp
) internal view returns (address offRampAddress) {
uint256 length = offRamps.length;
for (uint256 i = length; i > 0; --i) {
address candidateAddr = offRamps[i - 1].offRamp;
if (offRamps[i - 1].sourceChainSelector != sourceChainSelector) {
continue;
}

try IOffRampSourceConfigFork(candidateAddr).getSourceChainConfig(sourceChainSelector) returns (
IOffRampSourceConfigFork.SourceChainConfig memory cfg
) {
if (cfg.isEnabled && cfg.onRamp.length == 32) {
address configuredOnRamp = abi.decode(cfg.onRamp, (address));
if (configuredOnRamp == sourceOnRamp) {
return candidateAddr;
}
}
} catch {}

try IEVM2EVMOffRampStaticConfigFork(candidateAddr).getStaticConfig() returns (
IEVM2EVMOffRampStaticConfigFork.StaticConfig memory staticCfg
) {
if (staticCfg.onRamp == sourceOnRamp && staticCfg.sourceChainSelector == sourceChainSelector) {
return candidateAddr;
}
} catch {}
}

return address(0);
}

function _executePreV1dot6(address offRamp, InternalPreV1dot6.EVM2EVMMessage memory message)
internal
returns (bool success)
{
uint256 numberOfTokens = message.tokenAmounts.length;
bytes[] memory offchainTokenData = new bytes[](numberOfTokens);
uint32[] memory tokenGasOverrides = new uint32[](numberOfTokens);
for (uint256 l; l < numberOfTokens; ++l) {
tokenGasOverrides[l] = uint32(message.gasLimit);
}

vm.startPrank(offRamp);
try IEVM2EVMOffRampPreV1dot6Fork(offRamp).executeSingleMessage(message, offchainTokenData, tokenGasOverrides) {
vm.stopPrank();
return true;
} catch (bytes memory err) {
vm.stopPrank();
console2.logBytes(err);
return false;
}
}

function _executePostV1dot6(address offRamp, Internal.EVM2AnyRampMessage memory message)
internal
returns (bool success)
{
uint256 gasLimit = _fromBytes(message.extraArgs).gasLimit;
uint256 numberOfTokens = message.tokenAmounts.length;
Internal.Any2EVMTokenTransfer[] memory tokenAmounts = new Internal.Any2EVMTokenTransfer[](numberOfTokens);
for (uint256 l; l < numberOfTokens; ++l) {
tokenAmounts[l] = Internal.Any2EVMTokenTransfer({
sourcePoolAddress: abi.encode(message.tokenAmounts[l].sourcePoolAddress),
destTokenAddress: _decodeEVMAddress(message.tokenAmounts[l].destTokenAddress),
destGasAmount: abi.decode(message.tokenAmounts[l].destExecData, (uint32)),
extraData: message.tokenAmounts[l].extraData,
amount: message.tokenAmounts[l].amount
});
}
Internal.Any2EVMRampMessage memory any2EVMRampMessage = Internal.Any2EVMRampMessage({
header: message.header,
sender: abi.encodePacked(message.sender),
data: message.data,
receiver: _decodeEVMAddress(message.receiver),
gasLimit: gasLimit,
tokenAmounts: tokenAmounts
});
bytes[] memory offchainTokenData = new bytes[](numberOfTokens);
uint32[] memory tokenGasOverrides = new uint32[](numberOfTokens);
for (uint256 l; l < numberOfTokens; ++l) {
tokenGasOverrides[l] = uint32(gasLimit);
}

vm.startPrank(offRamp);
try IEVM2EVMOffRampFork(offRamp).executeSingleMessage(any2EVMRampMessage, offchainTokenData, tokenGasOverrides)
{
vm.stopPrank();
return true;
} catch (bytes memory err) {
vm.stopPrank();
console2.logBytes(err);
return false;
}
}

/**
* @notice Decodes ABI-encoded EVM address bytes to an `address`.
* @dev Used for `Client.EVM2AnyMessage.receiver` and `Internal.EVM2AnyTokenTransfer.destTokenAddress`.
* CCIP uses `abi.encode(address)` (32 bytes). A legacy 20-byte packed encoding is also supported.
*/
function _decodeEVMAddress(bytes memory encodedAddress) internal pure returns (address) {
if (encodedAddress.length == 32) {
return abi.decode(encodedAddress, (address));
}
if (encodedAddress.length == 20) {
return address(uint160(bytes20(encodedAddress)));
}
revert InvalidEVMAddressEncoding(encodedAddress);
}

/**
* @notice Internal helper function to decode extraArgs bytes to GenericExtraArgsV2 struct.
* Supports decoding of both GenericExtraArgsV2 and EVMExtraArgsV1 structs.
Expand Down
Loading
Loading