const { expect } = require("chai"); const { ethers, network } = require("hardhat"); const path = require("path"); const mockRouterArtifact = require(path.join( __dirname, "../../out/CCIPWETH9Bridge.t.sol/MockCCIPRouter.json" )); async function expectEvent(txPromise, contract, eventName) { const tx = await txPromise; const receipt = await tx.wait(); const foundEvent = receipt.logs.some((log) => { try { return contract.interface.parseLog(log)?.name === eventName; } catch (error) { return false; } }); expect(foundEvent).to.equal(true); } describe("CCIP Integration", function () { let ccipLogger, ccipReporter; let owner, relayer; let mockRouter, mockSourceRouter; beforeEach(async function () { [owner, relayer] = await ethers.getSigners(); // Reuse the Foundry-built mock router so this suite doesn't depend on a // separate Hardhat-only mock artifact. const MockRouter = new ethers.ContractFactory( mockRouterArtifact.abi, mockRouterArtifact.bytecode.object, owner ); mockRouter = await MockRouter.deploy(); await mockRouter.waitForDeployment(); mockSourceRouter = await MockRouter.deploy(); await mockSourceRouter.waitForDeployment(); // Deploy CCIPLogger (Ethereum receiver) const CCIPLogger = await ethers.getContractFactory("CCIPLogger"); ccipLogger = await CCIPLogger.deploy( await mockRouter.getAddress(), ethers.ZeroAddress, // No authorized signer for basic test "0x000000000000008a" // Chain-138 selector ); await ccipLogger.waitForDeployment(); // Deploy CCIPTxReporter (Chain-138 sender) const CCIPTxReporter = await ethers.getContractFactory("CCIPTxReporter"); ccipReporter = await CCIPTxReporter.deploy( await mockSourceRouter.getAddress(), "0x500147", // Ethereum Mainnet selector await ccipLogger.getAddress() ); await ccipReporter.waitForDeployment(); }); describe("CCIPTxReporter", function () { it("Should report a single transaction", async function () { const txHash = ethers.keccak256(ethers.toUtf8Bytes("test-tx")); const fromAddr = owner.address; const toAddr = relayer.address; const value = ethers.parseEther("1.0"); await expectEvent( ccipReporter.reportTx(txHash, fromAddr, toAddr, value, "0x", { value: ethers.parseEther("0.01"), }), ccipReporter, "SingleTxReported" ); }); it("Should report a batch of transactions", async function () { const batchId = ethers.keccak256(ethers.toUtf8Bytes("test-batch")); const txHashes = [ ethers.keccak256(ethers.toUtf8Bytes("tx1")), ethers.keccak256(ethers.toUtf8Bytes("tx2")), ]; const froms = [owner.address, relayer.address]; const tos = [relayer.address, owner.address]; const values = [ethers.parseEther("1.0"), ethers.parseEther("2.0")]; await expectEvent( ccipReporter.reportBatch(batchId, txHashes, froms, tos, values, "0x", { value: ethers.parseEther("0.01"), }), ccipReporter, "BatchReported" ); }); it("Should estimate fee correctly", async function () { const txHashes = [ethers.keccak256(ethers.toUtf8Bytes("tx1"))]; const froms = [owner.address]; const tos = [relayer.address]; const values = [ethers.parseEther("1.0")]; const fee = await ccipReporter.estimateFee( txHashes, froms, tos, values ); expect(fee > 0n).to.equal(true); }); }); describe("CCIPLogger", function () { it("Should receive and log transactions", async function () { const batchId = ethers.keccak256(ethers.toUtf8Bytes("test-batch")); const txHashes = [ethers.keccak256(ethers.toUtf8Bytes("tx1"))]; const froms = [owner.address]; const tos = [relayer.address]; const values = [ethers.parseEther("1.0")]; const payload = ethers.AbiCoder.defaultAbiCoder().encode( ["bytes32", "bytes32[]", "address[]", "address[]", "uint256[]", "bytes"], [batchId, txHashes, froms, tos, values, "0x"] ); // Simulate CCIP message delivery const message = { messageId: ethers.keccak256(ethers.toUtf8Bytes("test-message")), sourceChainSelector: "0x000000000000008a", sender: ethers.zeroPadValue(await ccipReporter.getAddress(), 32), data: payload, tokenAmounts: [], }; const routerAddress = await mockRouter.getAddress(); await network.provider.request({ method: "hardhat_impersonateAccount", params: [routerAddress], }); await network.provider.send("hardhat_setBalance", [ routerAddress, "0x56BC75E2D63100000", ]); const routerSigner = await ethers.getSigner(routerAddress); await expectEvent( ccipLogger.connect(routerSigner).ccipReceive(message), ccipLogger, "RemoteBatchLogged" ); await network.provider.request({ method: "hardhat_stopImpersonatingAccount", params: [routerAddress], }); }); it("Should prevent replay attacks", async function () { const batchId = ethers.keccak256(ethers.toUtf8Bytes("replay-test")); const txHashes = [ethers.keccak256(ethers.toUtf8Bytes("tx1"))]; const froms = [owner.address]; const tos = [relayer.address]; const values = [ethers.parseEther("1.0")]; const payload = ethers.AbiCoder.defaultAbiCoder().encode( ["bytes32", "bytes32[]", "address[]", "address[]", "uint256[]", "bytes"], [batchId, txHashes, froms, tos, values, "0x"] ); const message = { messageId: ethers.keccak256(ethers.toUtf8Bytes("test-message-1")), sourceChainSelector: "0x000000000000008a", sender: ethers.zeroPadValue(await ccipReporter.getAddress(), 32), data: payload, tokenAmounts: [], }; const routerAddress = await mockRouter.getAddress(); await network.provider.request({ method: "hardhat_impersonateAccount", params: [routerAddress], }); await network.provider.send("hardhat_setBalance", [ routerAddress, "0x56BC75E2D63100000", ]); const routerSigner = await ethers.getSigner(routerAddress); // First delivery should succeed await ccipLogger.connect(routerSigner).ccipReceive(message); // Second delivery with same batchId should fail const message2 = { ...message, messageId: ethers.keccak256(ethers.toUtf8Bytes("test-message-2")), }; let reverted = false; try { await ccipLogger.connect(routerSigner).ccipReceive(message2); } catch (error) { reverted = error.message.includes("CCIPLogger: batch already processed"); } expect(reverted).to.equal(true); await network.provider.request({ method: "hardhat_stopImpersonatingAccount", params: [routerAddress], }); }); }); });