// Hardhat tests for AereDestinationSettler (ERC-7683 IDestinationSettler). // // The DESTINATION / filling side of AERE's cross-chain intents. A solver calls // fill(orderId, originData, fillerData) to deliver the order's promised output to // the recipient on AERE; the settler records the fill and emits Filled so the origin // chain can repay the solver behind a QBFT-finality proof. // // Cases: // - valid fill: delivers output to recipient, records the fill, emits Filled, // exposes the record + fillCommitment // - duplicate fill reverts (AlreadyFilled) // - mismatched-output fill reverts (OutputMismatch) // - zero-recipient reverts (ZeroRecipient) // // Run: npx hardhat test test/destination-settler.test.js const { expect } = require("chai"); const { ethers } = require("hardhat"); const ONE = 10n ** 18n; const AERE_CHAIN_ID = 2800n; function b32(addr) { return ethers.zeroPadValue(addr, 32); } // originData: the origin settler's declared output leg (matches AereSpokePool). function encodeOriginData(orderId, outputToken, outputAmount, recipient32) { return ethers.AbiCoder.defaultAbiCoder().encode( ["bytes32", "address", "uint256", "bytes32"], [orderId, outputToken, outputAmount, recipient32] ); } // fillerData: the solver's actual delivery + origin-chain repayment address. function encodeFillerData(deliveredToken, deliveredAmount, deliveredRecipient, repaymentAddress32) { return ethers.AbiCoder.defaultAbiCoder().encode( ["address", "uint256", "address", "bytes32"], [deliveredToken, deliveredAmount, deliveredRecipient, repaymentAddress32] ); } async function deployFixture() { const [deployer, solver, recipient, other] = await ethers.getSigners(); // Output token the solver delivers on AERE. const Token = await ethers.getContractFactory("MockERC20Lending"); const token = await Token.deploy("USD Coin (bridged)", "USDC.e", 6); await token.waitForDeployment(); const Settler = await ethers.getContractFactory( "contracts/intents/AereDestinationSettler.sol:AereDestinationSettler" ); const settler = await Settler.deploy(); await settler.waitForDeployment(); // Fund the solver with output tokens and approve the settler to pull them. const funded = 1_000_000n * ONE; await token.mint(solver.address, funded); await token.connect(solver).approve(await settler.getAddress(), funded); return { deployer, solver, recipient, other, token, settler }; } describe("AereDestinationSettler - ERC-7683 IDestinationSettler", function () { it("valid fill: delivers output to recipient, records the fill, emits Filled, exposes commitment", async function () { const { solver, recipient, token, settler } = await deployFixture(); const orderId = ethers.id("order-1"); const outputAmount = 250n * ONE; const tokenAddr = await token.getAddress(); const recipient32 = b32(recipient.address); const repayment32 = b32(solver.address); const originData = encodeOriginData(orderId, tokenAddr, outputAmount, recipient32); const fillerData = encodeFillerData(tokenAddr, outputAmount, recipient.address, repayment32); const recipBefore = await token.balanceOf(recipient.address); await expect(settler.connect(solver).fill(orderId, originData, fillerData)) .to.emit(settler, "Filled"); // Recipient received exactly the delivered output; settler custodies nothing. expect(await token.balanceOf(recipient.address) - recipBefore).to.equal(outputAmount); expect(await token.balanceOf(await settler.getAddress())).to.equal(0n); // Fill recorded and readable. expect(await settler.isFilled(orderId)).to.equal(true); const rec = await settler.getFill(orderId); expect(rec.filler).to.equal(solver.address); expect(rec.outputToken).to.equal(tokenAddr); expect(rec.outputAmount).to.equal(outputAmount); expect(rec.recipient).to.equal(recipient.address); expect(rec.repaymentAddress).to.equal(repayment32); expect(await settler.fillCount()).to.equal(1n); // fillCommitment binds the exact fields the origin-side verifier re-derives. const expectedCommitment = ethers.keccak256( ethers.AbiCoder.defaultAbiCoder().encode( ["uint256", "address", "bytes32", "address", "address", "uint256", "address", "bytes32"], [ AERE_CHAIN_ID, await settler.getAddress(), orderId, solver.address, tokenAddr, outputAmount, recipient.address, repayment32, ] ) ); expect(await settler.fillCommitment(orderId)).to.equal(expectedCommitment); }); it("duplicate fill reverts (AlreadyFilled)", async function () { const { solver, recipient, token, settler } = await deployFixture(); const orderId = ethers.id("order-dup"); const outputAmount = 100n * ONE; const tokenAddr = await token.getAddress(); const originData = encodeOriginData(orderId, tokenAddr, outputAmount, b32(recipient.address)); const fillerData = encodeFillerData(tokenAddr, outputAmount, recipient.address, b32(solver.address)); await settler.connect(solver).fill(orderId, originData, fillerData); await expect(settler.connect(solver).fill(orderId, originData, fillerData)) .to.be.revertedWithCustomError(settler, "AlreadyFilled") .withArgs(orderId); }); it("mismatched-output fill reverts (OutputMismatch)", async function () { const { solver, recipient, other, token, settler } = await deployFixture(); const orderId = ethers.id("order-mismatch"); const outputAmount = 100n * ONE; const tokenAddr = await token.getAddress(); const originData = encodeOriginData(orderId, tokenAddr, outputAmount, b32(recipient.address)); // Short delivery (less than the declared output) must revert. const shortFiller = encodeFillerData(tokenAddr, outputAmount - 1n, recipient.address, b32(solver.address)); await expect(settler.connect(solver).fill(orderId, originData, shortFiller)) .to.be.revertedWithCustomError(settler, "OutputMismatch"); // Wrong recipient (delivering to someone other than the declared recipient) must revert. const wrongRecipFiller = encodeFillerData(tokenAddr, outputAmount, other.address, b32(solver.address)); await expect(settler.connect(solver).fill(orderId, originData, wrongRecipFiller)) .to.be.revertedWithCustomError(settler, "OutputMismatch"); // Wrong token (delivering a different token than declared) must revert. const OtherToken = await ethers.getContractFactory("MockERC20Lending"); const otherToken = await OtherToken.deploy("Wrapped Ether", "WETH", 18); await otherToken.waitForDeployment(); await otherToken.mint(solver.address, outputAmount); await otherToken.connect(solver).approve(await settler.getAddress(), outputAmount); const wrongTokenFiller = encodeFillerData(await otherToken.getAddress(), outputAmount, recipient.address, b32(solver.address)); await expect(settler.connect(solver).fill(orderId, originData, wrongTokenFiller)) .to.be.revertedWithCustomError(settler, "OutputMismatch"); // Order stayed unfilled through all three rejected attempts. expect(await settler.isFilled(orderId)).to.equal(false); }); it("zero-recipient reverts (ZeroRecipient)", async function () { const { solver, token, settler } = await deployFixture(); const orderId = ethers.id("order-zero-recip"); const outputAmount = 100n * ONE; const tokenAddr = await token.getAddress(); // Declared recipient is zero. const originData = encodeOriginData(orderId, tokenAddr, outputAmount, ethers.ZeroHash); const fillerData = encodeFillerData(tokenAddr, outputAmount, ethers.ZeroAddress, b32(solver.address)); await expect(settler.connect(solver).fill(orderId, originData, fillerData)) .to.be.revertedWithCustomError(settler, "ZeroRecipient"); }); });