const { expect } = require("chai"); const { ethers } = require("hardhat"); describe("AereFaucetV2 — anonymous claim", () => { let owner, signer, recipient, outsider; let f; beforeEach(async () => { [owner, signer, recipient, outsider] = await ethers.getSigners(); const F = await ethers.getContractFactory("AereFaucetV2"); f = await F.deploy(signer.address); await f.waitForDeployment(); // Seed 10 AERE. await owner.sendTransaction({ to: await f.getAddress(), value: ethers.parseEther("10") }); }); async function ticket(commitment, recipientAddr, validUntil) { const inner = ethers.AbiCoder.defaultAbiCoder().encode( ["string", "uint256", "address", "bytes32", "address", "uint64"], ["AereFaucetV2", (await ethers.provider.getNetwork()).chainId, await f.getAddress(), commitment, recipientAddr, validUntil] ); const innerHash = ethers.keccak256(inner); return signer.signMessage(ethers.getBytes(innerHash)); } it("regular claim works with cooldown", async () => { const before = await ethers.provider.getBalance(outsider.address); await f.connect(outsider).claim(); const after = await ethers.provider.getBalance(outsider.address); expect(after).to.be.gt(before); // roughly + dripAmount - gas await expect(f.connect(outsider).claim()).to.be.revertedWithCustomError(f, "InCooldown"); }); it("anonymous claim with valid ticket sends drip to recipient", async () => { const commit = ethers.keccak256(ethers.toUtf8Bytes("secret-1")); const validUntil = BigInt((await ethers.provider.getBlock("latest")).timestamp + 3600); const sig = await ticket(commit, recipient.address, validUntil); const before = await ethers.provider.getBalance(recipient.address); await f.connect(outsider).claimAnonymous(commit, recipient.address, validUntil, sig); const after = await ethers.provider.getBalance(recipient.address); expect(after - before).to.equal(ethers.parseEther("0.05")); expect(await f.spentCommitments(commit)).to.equal(true); }); it("re-using a commitment reverts", async () => { const commit = ethers.keccak256(ethers.toUtf8Bytes("secret-2")); const validUntil = BigInt((await ethers.provider.getBlock("latest")).timestamp + 3600); const sig = await ticket(commit, recipient.address, validUntil); await f.connect(outsider).claimAnonymous(commit, recipient.address, validUntil, sig); await expect( f.connect(outsider).claimAnonymous(commit, recipient.address, validUntil, sig) ).to.be.revertedWithCustomError(f, "CommitmentSpent"); }); it("expired ticket rejected", async () => { const commit = ethers.keccak256(ethers.toUtf8Bytes("expired")); const validUntil = BigInt((await ethers.provider.getBlock("latest")).timestamp + 5); const sig = await ticket(commit, recipient.address, validUntil); await ethers.provider.send("evm_increaseTime", [10]); await ethers.provider.send("evm_mine", []); await expect( f.connect(outsider).claimAnonymous(commit, recipient.address, validUntil, sig) ).to.be.revertedWithCustomError(f, "TicketExpired"); }); it("ticket signed by wrong key rejected", async () => { const commit = ethers.keccak256(ethers.toUtf8Bytes("wrong-key")); const validUntil = BigInt((await ethers.provider.getBlock("latest")).timestamp + 3600); // signed by `outsider` (not the SIGNER) const inner = ethers.AbiCoder.defaultAbiCoder().encode( ["string", "uint256", "address", "bytes32", "address", "uint64"], ["AereFaucetV2", (await ethers.provider.getNetwork()).chainId, await f.getAddress(), commit, recipient.address, validUntil] ); const innerHash = ethers.keccak256(inner); const badSig = await outsider.signMessage(ethers.getBytes(innerHash)); await expect( f.connect(outsider).claimAnonymous(commit, recipient.address, validUntil, badSig) ).to.be.revertedWithCustomError(f, "TicketSignerMismatch"); }); it("recipient bound in signed message — cannot redirect", async () => { const commit = ethers.keccak256(ethers.toUtf8Bytes("redirect")); const validUntil = BigInt((await ethers.provider.getBlock("latest")).timestamp + 3600); const sig = await ticket(commit, recipient.address, validUntil); // Try claiming for a different recipient with the same ticket. await expect( f.connect(outsider).claimAnonymous(commit, outsider.address, validUntil, sig) ).to.be.revertedWithCustomError(f, "TicketSignerMismatch"); }); it("zero recipient + zero commitment rejected", async () => { const validUntil = BigInt((await ethers.provider.getBlock("latest")).timestamp + 3600); const sig = await ticket(ethers.ZeroHash, recipient.address, validUntil); await expect( f.connect(outsider).claimAnonymous(ethers.ZeroHash, recipient.address, validUntil, sig) ).to.be.revertedWithCustomError(f, "BadParams"); }); });