// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import {Test, console} from "forge-std/Test.sol"; import {CCIPSender} from "../../contracts/ccip/CCIPSender.sol"; import {CCIPReceiver} from "../../contracts/ccip/CCIPReceiver.sol"; import {Aggregator} from "../../contracts/oracle/Aggregator.sol"; import {IRouterClient} from "../../contracts/ccip/IRouterClient.sol"; contract CrossChainOracleTest is Test { CCIPSender public sender; CCIPReceiver public receiver; Aggregator public sourceAggregator; Aggregator public destAggregator; address public mockRouter; address public linkToken; uint64 constant SOURCE_CHAIN = 138; uint64 constant DEST_CHAIN = 5009297550715157269; function setUp() public { mockRouter = address(new MockRouter()); linkToken = address(new MockLinkToken()); sourceAggregator = new Aggregator("ETH/USD", address(this), 60, 50); destAggregator = new Aggregator("ETH/USD", address(this), 60, 50); sender = new CCIPSender(mockRouter, address(sourceAggregator), linkToken); receiver = new CCIPReceiver(mockRouter, address(destAggregator)); sourceAggregator.addTransmitter(address(this)); destAggregator.addTransmitter(address(receiver)); sender.addDestination(DEST_CHAIN, address(receiver)); // Mint LINK tokens to aggregator (since aggregator will pay fees) MockLinkToken(linkToken).mint(address(sourceAggregator), 1000e18); // Also mint to sender for fee calculations MockLinkToken(linkToken).mint(address(sender), 1000e18); } function testCrossChainOracleSync() public { uint256 price = 25000000000; // Update source oracle (this should trigger CCIP send if using OracleWithCCIP) sourceAggregator.updateAnswer(price); // Approve sender to spend aggregator's LINK tokens vm.prank(address(sourceAggregator)); MockLinkToken(linkToken).approve(address(sender), 1000e18); // Send cross-chain update (must be called by aggregator) vm.prank(address(sourceAggregator)); sender.sendOracleUpdate(DEST_CHAIN, price, 1, block.timestamp); // Simulate message delivery IRouterClient.Any2EVMMessage memory message = IRouterClient.Any2EVMMessage({ messageId: keccak256("test"), sourceChainSelector: SOURCE_CHAIN, sender: abi.encode(address(sender)), data: abi.encode(price, uint256(1), block.timestamp), tokenAmounts: new IRouterClient.TokenAmount[](0) }); vm.prank(mockRouter); receiver.ccipReceive(message); // Verify destination oracle updated (uint256 roundId, int256 answer, , , ) = destAggregator.latestRoundData(); assertEq(uint256(answer), price, "Destination price should match"); assertEq(roundId, 1, "Round ID should match"); } } contract MockRouter is IRouterClient { function ccipSend(uint64, EVM2AnyMessage memory) external payable returns (bytes32, uint256) { return (keccak256("mock"), 0.01e18); } function getFee(uint64, EVM2AnyMessage memory) external pure returns (uint256) { return 0.01e18; } function getSupportedTokens(uint64) external pure returns (address[] memory) { return new address[](0); } } contract MockLinkToken { mapping(address => uint256) public balanceOf; mapping(address => mapping(address => uint256)) public allowance; function mint(address to, uint256 amount) external { balanceOf[to] += amount; } function transfer(address to, uint256 amount) external returns (bool) { require(balanceOf[msg.sender] >= amount, "Insufficient balance"); balanceOf[msg.sender] -= amount; balanceOf[to] += amount; return true; } function transferFrom(address from, address to, uint256 amount) external returns (bool) { require(balanceOf[from] >= amount, "Insufficient balance"); require(allowance[from][msg.sender] >= amount, "Insufficient allowance"); balanceOf[from] -= amount; balanceOf[to] += amount; allowance[from][msg.sender] -= amount; return true; } function approve(address spender, uint256 amount) external returns (bool) { allowance[msg.sender][spender] = amount; return true; } }