- CCIP/trustless bridge contracts, GRU tokens, DEX/PMM tests, reserve vault. - Token-aggregation service routes, planner, chain config, relay env templates. - Config snapshots and multi-chain deployment markdown updates. - gitignore services/btc-intake/dist/ (tsc output); do not track dist. Run forge build && forge test before deploy (large solc graph). Made-with: Cursor
219 lines
6.9 KiB
JavaScript
219 lines
6.9 KiB
JavaScript
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],
|
|
});
|
|
});
|
|
});
|
|
});
|