// 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 OracleCCIPTest is Test { CCIPSender public sender; CCIPReceiver public receiver; Aggregator public aggregator; address public mockRouter; address public linkToken; uint64 constant TARGET_CHAIN_SELECTOR = 5009297550715157269; function setUp() public { mockRouter = address(new MockRouter()); linkToken = address(new MockLinkToken()); aggregator = new Aggregator("ETH/USD", address(this), 60, 50); address oracleAggregator = address(aggregator); sender = new CCIPSender(mockRouter, oracleAggregator, linkToken); receiver = new CCIPReceiver(mockRouter, address(aggregator)); // Add receiver as transmitter aggregator.addTransmitter(address(receiver)); // Mint LINK tokens to aggregator (since aggregator will pay fees) MockLinkToken(linkToken).mint(address(aggregator), 1000e18); // Also mint to sender for fee calculations MockLinkToken(linkToken).mint(address(sender), 1000e18); } function testCrossChainOracleUpdate() public { uint256 price = 25000000000; // $250.00 uint256 roundId = 1; uint256 timestamp = block.timestamp; // Add destination first sender.addDestination(TARGET_CHAIN_SELECTOR, address(receiver)); // Approve sender to spend aggregator's LINK tokens vm.prank(address(aggregator)); MockLinkToken(linkToken).approve(address(sender), 1000e18); // Send oracle update (must be called by aggregator) vm.prank(address(aggregator)); sender.sendOracleUpdate(TARGET_CHAIN_SELECTOR, price, roundId, timestamp); // Encode message data bytes memory messageData = abi.encode(price, roundId, timestamp); // Simulate message delivery IRouterClient.Any2EVMMessage memory message = IRouterClient.Any2EVMMessage({ messageId: keccak256("test"), sourceChainSelector: 138, sender: abi.encode(address(sender)), data: messageData, tokenAmounts: new IRouterClient.TokenAmount[](0) }); vm.prank(mockRouter); receiver.ccipReceive(message); // Verify oracle was updated (uint80 roundIdResult, int256 answerResult, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) = aggregator.latestRoundData(); assertEq(uint256(answerResult), price, "Oracle price should match"); assertEq(uint256(roundIdResult), roundId, "Round ID should match"); } function testMultipleOracleUpdates() public { // Add destination first sender.addDestination(TARGET_CHAIN_SELECTOR, address(receiver)); uint256[] memory prices = new uint256[](3); prices[0] = 25000000000; prices[1] = 25100000000; prices[2] = 25200000000; for (uint256 i = 0; i < prices.length; i++) { uint256 roundId = i + 1; uint256 timestamp = block.timestamp + i; // Approve sender to spend aggregator's LINK tokens (reset to zero first to avoid non-zero to non-zero error) vm.startPrank(address(aggregator)); MockLinkToken(linkToken).approve(address(sender), 0); MockLinkToken(linkToken).approve(address(sender), 1000e18); vm.stopPrank(); // Send oracle update (must be called by aggregator) vm.prank(address(aggregator)); sender.sendOracleUpdate(TARGET_CHAIN_SELECTOR, prices[i], roundId, timestamp); // Encode message data bytes memory messageData = abi.encode(prices[i], roundId, timestamp); // Simulate delivery IRouterClient.Any2EVMMessage memory message = IRouterClient.Any2EVMMessage({ messageId: keccak256(abi.encode(i)), sourceChainSelector: 138, sender: abi.encode(address(sender)), data: messageData, tokenAmounts: new IRouterClient.TokenAmount[](0) }); vm.prank(mockRouter); receiver.ccipReceive(message); } (uint80 roundIdResult, int256 answerResult,,,) = aggregator.latestRoundData(); assertEq(uint256(answerResult), prices[2], "Latest price should be last update"); } } 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; } }