const { expect } = require("chai"); const { ethers, network } = require("hardhat"); // --------------------------------------------------------------------------- // AereComputeMarketV3: proof-carrying DePIN compute marketplace. // // The three verification modes, the Falcon-512 post-quantum settlement gate, and // the escrow-solvency invariant are exercised against faithful test doubles: // // * MockSp1Verifier - the deployed SP1 gateway stand-in. verifyProof // REVERTS on an invalid proof (exact production semantics), so a ZK_VERIFIED // job cannot pay without a valid proof. // * MockPQCPrecompile(1) - installed at 0x0AE1 via hardhat_setCode. Parses // the Falcon-512 SPEC offsets and accepts iff the embedded commitment equals // keccak256(pk || message). The market builds the precompile input itself, so a // positive here proves the market's real precompile-calling path is correct. // * MockERC20Lending - an allowlisted ERC-20 reward asset. // // Real Falcon crypto and real SP1 proving are external [MEASURE]; the wire // encoding of the Falcon input is separately KAT-proven in AerePQCMessageVerifier // and against the live mainnet precompile. // --------------------------------------------------------------------------- const AE1 = ethers.getAddress("0x0000000000000000000000000000000000000ae1"); // Falcon-512 precompile const ZERO = ethers.ZeroAddress; const Mode = { REPLAY: 0, ZK_VERIFIED: 1, OPTIMISTIC: 2 }; const Status = { None: 0, Open: 1, Claimed: 2, Submitted: 3, Disputed: 4, Paid: 5, Refunded: 6, Slashed: 7, }; const SETTLEMENT_DOMAIN = ethers.keccak256(ethers.toUtf8Bytes("AereComputeMarketV3.v1.settlement")); // Deterministic seeded RNG (keccak hash-chain), reproducible. function makeRng(seedText) { let seed = ethers.keccak256(ethers.toUtf8Bytes(seedText)); return { bytes(n) { const out = new Uint8Array(n); let i = 0; while (i < n) { seed = ethers.keccak256(seed); const chunk = ethers.getBytes(seed); for (let j = 0; j < chunk.length && i < n; j++, i++) out[i] = chunk[j]; } return out; }, }; } // A well-formed Falcon-512 public key (897 bytes, header 0x09). function falconPubKey(rng) { const raw = rng.bytes(897); raw[0] = 0x09; return ethers.hexlify(raw); } // Build a Falcon envelope the mock treats as a genuine signature by `pk` over the // 32-byte `message`: nonce(40) || esig ; esig = 0x29 || keccak256(pk || message). function falconMockEnvelope(pk, message32, rng) { const commitment = ethers.keccak256(ethers.concat([pk, message32])); const nonce = rng.bytes(40); const esig = ethers.concat([new Uint8Array([0x29]), commitment]); return ethers.hexlify(ethers.concat([nonce, esig])); } function abi() { return ethers.AbiCoder.defaultAbiCoder(); } // Reproduce settlementDigest(id, provider, token, amount, resultHash) off-chain. function settlementDigest(chainId, market, id, provider, token, amount, resultHash) { return ethers.keccak256( abi().encode( ["bytes32", "uint256", "address", "uint256", "address", "address", "uint256", "bytes32"], [SETTLEMENT_DOMAIN, chainId, market, id, provider, token, amount, resultHash] ) ); } describe("AereComputeMarketV3", function () { let market, sp1, token; let deployer, requester, provider, challenger, arbiter, governance, other; let chainId; let falconMockCode; before(async function () { // Compile + cache the Falcon-512 mock precompile code once. const M = await ethers.getContractFactory("MockPQCPrecompile"); const f = await M.deploy(1); // scheme 1 = Falcon-512 await f.waitForDeployment(); falconMockCode = await ethers.provider.getCode(await f.getAddress()); }); beforeEach(async function () { [deployer, requester, provider, challenger, arbiter, governance, other] = await ethers.getSigners(); chainId = (await ethers.provider.getNetwork()).chainId; // Install the faithful Falcon-512 mock at the live precompile address. await network.provider.send("hardhat_setCode", [AE1, falconMockCode]); sp1 = await (await ethers.getContractFactory("MockSp1Verifier")).deploy(); await sp1.waitForDeployment(); token = await (await ethers.getContractFactory("MockERC20Lending")).deploy("USD Coin e", "USDC.e", 6); await token.waitForDeployment(); market = await (await ethers.getContractFactory("AereComputeMarketV3")).deploy( await sp1.getAddress(), arbiter.address, governance.address ); await market.waitForDeployment(); }); // Assert the escrow-solvency invariant for both assets: contract balance >= liabilities. async function assertSolvent() { expect(await market.isSolvent(ZERO), "native solvency").to.equal(true); expect(await market.isSolvent(await token.getAddress()), "token solvency").to.equal(true); // Cross-check the actual balances against the tracked liabilities. const mAddr = await market.getAddress(); const nativeBal = await ethers.provider.getBalance(mAddr); const nativeLia = await market.totalLiabilities(ZERO); expect(nativeBal >= nativeLia, "native bal>=liab").to.equal(true); const tokBal = await token.balanceOf(mAddr); const tokLia = await market.totalLiabilities(await token.getAddress()); expect(tokBal >= tokLia, "token bal>=liab").to.equal(true); } function baseParams(overrides) { return Object.assign( { token: ZERO, reward: ethers.parseEther("10"), providerBond: ethers.parseEther("2"), specHash: ethers.keccak256(ethers.toUtf8Bytes("spec:matmul-128x128")), zkProgramVKey: ethers.ZeroHash, mode: Mode.OPTIMISTIC, deadline: 0, // filled below challengeWindow: 3600, pqcSettlement: false, falconPubKey: "0x", }, overrides || {} ); } async function post(p, valueOverride) { const now = (await ethers.provider.getBlock("latest")).timestamp; const params = baseParams(p); if (params.deadline === 0) params.deadline = now + 7 * 24 * 3600; const id = await market.jobCount(); const value = valueOverride !== undefined ? valueOverride : (params.token === ZERO ? params.reward : 0n); await market.connect(requester).postJob(params, { value }); return id; } // ========================================================================= // 0. Construction guards (L1 fail-open) // ========================================================================= describe("construction guards", function () { // L1 (LOW) fix: verifyProof returns void, so a code-less ZK verifier makes the call succeed // silently and a ZK_VERIFIED job would pay with NO valid proof (fund loss). The constructor must // reject a code-less verifier, exactly like the sibling AereFinalityCertificateVerifier. it("rejects a code-less ZK verifier at construction (L1 fail-open guard)", async function () { const F = await ethers.getContractFactory("AereComputeMarketV3"); // A non-zero EOA has no code. await expect( F.deploy(other.address, arbiter.address, governance.address) ).to.be.revertedWithCustomError(market, "VerifierHasNoCode"); // A zero verifier still reverts on the earlier ZeroAddress check. await expect( F.deploy(ZERO, arbiter.address, governance.address) ).to.be.revertedWithCustomError(market, "ZeroAddress"); // Control: the real (code-bearing) mock gateway deploys fine. const ok = await F.deploy(await sp1.getAddress(), arbiter.address, governance.address); await ok.waitForDeployment(); expect(await ok.ZK_VERIFIER()).to.equal(await sp1.getAddress()); }); }); // ========================================================================= // 1. ZK_VERIFIED: pay only on a valid proof, reject an invalid one // ========================================================================= describe("ZK_VERIFIED mode", function () { const vkey = ethers.keccak256(ethers.toUtf8Bytes("sp1-program:aere-verifiable-compute-v1")); async function postZK() { return post({ mode: Mode.ZK_VERIFIED, reward: ethers.parseEther("10"), providerBond: 0n, zkProgramVKey: vkey, challengeWindow: 0, }); } it("pays the provider on a valid proof, and only then", async function () { const id = await postZK(); await assertSolvent(); await market.connect(provider).claimJob(id, { value: 0 }); await assertSolvent(); const specHash = (await market.getJob(id)).specHash; const resultHash = ethers.keccak256(ethers.toUtf8Bytes("result:0xC0FFEE")); const publicValues = abi().encode(["bytes32", "bytes32"], [specHash, resultHash]); const proof = "0x1122334455667788"; // Register this exact (vkey, publicValues, proof) as valid in the gateway. const marker = ethers.keccak256(abi().encode(["bytes32", "bytes", "bytes"], [vkey, publicValues, proof])); await sp1.setValid(marker, true); const before = await ethers.provider.getBalance(provider.address); // A third party submits on the provider's behalf? No: only the job provider may submit. await expect(market.connect(provider).submitResultZK(id, publicValues, proof, "0x")) .to.emit(market, "ResultVerifiedZK") .and.to.emit(market, "JobPaid"); const after = await ethers.provider.getBalance(provider.address); const j = await market.getJob(id); expect(Number(j.status)).to.equal(Status.Paid); expect(j.resultHash).to.equal(resultHash); // Provider received ~10 AERE (minus its own gas for the submit tx). expect(after > before).to.equal(true); await assertSolvent(); expect(await market.totalLiabilities(ZERO)).to.equal(0n); }); it("rejects an invalid proof and pays nothing (fail-closed)", async function () { const id = await postZK(); await market.connect(provider).claimJob(id, { value: 0 }); const specHash = (await market.getJob(id)).specHash; const resultHash = ethers.keccak256(ethers.toUtf8Bytes("result:fabricated")); const publicValues = abi().encode(["bytes32", "bytes32"], [specHash, resultHash]); const proof = "0xdeadbeef"; // never registered as valid await expect( market.connect(provider).submitResultZK(id, publicValues, proof, "0x") ).to.be.revertedWithCustomError(sp1, "MockInvalidProof"); const j = await market.getJob(id); expect(Number(j.status)).to.equal(Status.Claimed); // unchanged, no payment await assertSolvent(); expect(await market.totalLiabilities(ZERO)).to.equal(ethers.parseEther("10")); }); it("rejects a proof whose public values bind a different spec", async function () { const id = await postZK(); await market.connect(provider).claimJob(id, { value: 0 }); const wrongSpec = ethers.keccak256(ethers.toUtf8Bytes("spec:some-other-job")); const resultHash = ethers.keccak256(ethers.toUtf8Bytes("result")); const publicValues = abi().encode(["bytes32", "bytes32"], [wrongSpec, resultHash]); const proof = "0x1234"; const marker = ethers.keccak256(abi().encode(["bytes32", "bytes", "bytes"], [vkey, publicValues, proof])); await sp1.setValid(marker, true); // even a "valid" proof for the wrong spec must be rejected await expect( market.connect(provider).submitResultZK(id, publicValues, proof, "0x") ).to.be.revertedWithCustomError(market, "BadPublicValues"); await assertSolvent(); }); }); // ========================================================================= // 2. OPTIMISTIC mode: finalize after the window; dispute slashes on a bad result // ========================================================================= describe("OPTIMISTIC mode", function () { it("post + escrow + claim + submit + finalize pays provider and returns its bond", async function () { const id = await post({ mode: Mode.OPTIMISTIC }); await assertSolvent(); await market.connect(provider).claimJob(id, { value: ethers.parseEther("2") }); await assertSolvent(); expect(await market.totalLiabilities(ZERO)).to.equal(ethers.parseEther("12")); // 10 reward + 2 bond await market.connect(provider).submitResult(id, ethers.toUtf8Bytes("optimistic-result")); expect(Number((await market.getJob(id)).status)).to.equal(Status.Submitted); // Cannot finalize before the window closes. await expect(market.connect(other).finalize(id, "0x")).to.be.revertedWithCustomError(market, "WindowOpen"); await network.provider.send("evm_increaseTime", [3601]); await network.provider.send("evm_mine", []); const before = await ethers.provider.getBalance(provider.address); // A third party finalizes so the provider pays no gas: delta is exactly reward + bond. await expect(market.connect(other).finalize(id, "0x")).to.emit(market, "JobPaid"); const after = await ethers.provider.getBalance(provider.address); expect(after - before).to.equal(ethers.parseEther("12")); expect(Number((await market.getJob(id)).status)).to.equal(Status.Paid); expect(await market.totalLiabilities(ZERO)).to.equal(0n); await assertSolvent(); }); it("a successful dispute slashes the provider bond to the challenger and refunds the requester", async function () { const id = await post({ mode: Mode.OPTIMISTIC }); await market.connect(provider).claimJob(id, { value: ethers.parseEther("2") }); await market.connect(provider).submitResult(id, ethers.toUtf8Bytes("bad-result")); await assertSolvent(); // Challenger disputes within the window, posting a matching bond. await expect( market.connect(challenger).dispute(id, ethers.ZeroHash, { value: ethers.parseEther("2") }) ).to.emit(market, "JobDisputed"); expect(Number((await market.getJob(id)).status)).to.equal(Status.Disputed); expect(await market.totalLiabilities(ZERO)).to.equal(ethers.parseEther("14")); // 10 + 2 + 2 await assertSolvent(); // Only the arbiter can resolve. await expect(market.connect(other).resolveDispute(id, false)).to.be.revertedWithCustomError(market, "NotArbiter"); const reqBefore = await ethers.provider.getBalance(requester.address); const chBefore = await ethers.provider.getBalance(challenger.address); // Arbiter upholds the dispute (provider lost). Resolve is sent by a neutral account so the // requester/challenger balance deltas are exact. await market.connect(arbiter).resolveDispute(id, false); const reqAfter = await ethers.provider.getBalance(requester.address); const chAfter = await ethers.provider.getBalance(challenger.address); // Requester refunded the full reward. expect(reqAfter - reqBefore).to.equal(ethers.parseEther("10")); // Challenger gets its own bond back (2) plus the slashed provider bond (2) = 4. expect(chAfter - chBefore).to.equal(ethers.parseEther("4")); const j = await market.getJob(id); expect(Number(j.status)).to.equal(Status.Slashed); expect(await market.totalLiabilities(ZERO)).to.equal(0n); await assertSolvent(); }); }); // ========================================================================= // 3. REPLAY mode: dispute path (deterministic recompute on record) // ========================================================================= describe("REPLAY mode", function () { it("records the challenger's recomputed hash; arbiter can uphold the provider", async function () { const id = await post({ mode: Mode.REPLAY, challengeWindow: 1800, providerBond: ethers.parseEther("1") }); await market.connect(provider).claimJob(id, { value: ethers.parseEther("1") }); const result = ethers.toUtf8Bytes("deterministic-output-42"); await market.connect(provider).submitResult(id, result); const submittedHash = ethers.keccak256(result); expect((await market.getJob(id)).resultHash).to.equal(submittedHash); await assertSolvent(); // A challenger claims a different recomputation and disputes. const challengerHash = ethers.keccak256(ethers.toUtf8Bytes("challenger-claims-this")); await market.connect(challenger).dispute(id, challengerHash, { value: ethers.parseEther("1") }); expect(await market.challengeResultHashOf(id)).to.equal(challengerHash); await assertSolvent(); // Off-chain re-run agrees with the provider: arbiter upholds the provider. const provBefore = await ethers.provider.getBalance(provider.address); await market.connect(arbiter).resolveDispute(id, true); const provAfter = await ethers.provider.getBalance(provider.address); // Provider gets reward (10) + own bond back (1) + challenger's forfeited bond (1) = 12. expect(provAfter - provBefore).to.equal(ethers.parseEther("12")); expect(Number((await market.getJob(id)).status)).to.equal(Status.Paid); expect(await market.totalLiabilities(ZERO)).to.equal(0n); await assertSolvent(); }); it("re-run verification off-chain matches the on-chain resultHash for a good result", async function () { const id = await post({ mode: Mode.REPLAY, challengeWindow: 60, providerBond: ethers.parseEther("1") }); await market.connect(provider).claimJob(id, { value: ethers.parseEther("1") }); const result = ethers.toUtf8Bytes("f(input)=output"); await market.connect(provider).submitResult(id, result); // Anyone re-runs f(input) locally and checks determinism against the on-chain anchor. const onChain = (await market.getJob(id)).resultHash; expect(onChain).to.equal(ethers.keccak256(result)); await network.provider.send("evm_increaseTime", [61]); await network.provider.send("evm_mine", []); await market.connect(other).finalize(id, "0x"); expect(Number((await market.getJob(id)).status)).to.equal(Status.Paid); await assertSolvent(); }); }); // ========================================================================= // 4. ERC-20 reward asset (allowlist) // ========================================================================= describe("allowlisted ERC-20 reward", function () { it("escrows and settles an allowlisted token; rejects a non-allowlisted one", async function () { const tokenAddr = await token.getAddress(); await token.mint(requester.address, ethers.parseUnits("1000", 6)); await token.connect(requester).approve(await market.getAddress(), ethers.parseUnits("1000", 6)); // Not allowlisted yet. await expect( post({ mode: Mode.OPTIMISTIC, token: tokenAddr, reward: ethers.parseUnits("100", 6) }, 0n) ).to.be.revertedWithCustomError(market, "TokenNotAllowed"); // Governance allowlists it (governance cannot move funds). await expect(market.connect(other).setTokenAllowed(tokenAddr, true)).to.be.revertedWithCustomError(market, "NotGovernance"); await market.connect(governance).setTokenAllowed(tokenAddr, true); const id = await post({ mode: Mode.OPTIMISTIC, token: tokenAddr, reward: ethers.parseUnits("100", 6) }, 0n); await assertSolvent(); expect(await market.totalLiabilities(tokenAddr)).to.equal(ethers.parseUnits("100", 6)); // Bond is native AERE even though the reward is an ERC-20. await market.connect(provider).claimJob(id, { value: ethers.parseEther("2") }); await market.connect(provider).submitResult(id, ethers.toUtf8Bytes("erc20-result")); await network.provider.send("evm_increaseTime", [3601]); await network.provider.send("evm_mine", []); const before = await token.balanceOf(provider.address); await market.connect(other).finalize(id, "0x"); const after = await token.balanceOf(provider.address); expect(after - before).to.equal(ethers.parseUnits("100", 6)); expect(await market.totalLiabilities(tokenAddr)).to.equal(0n); await assertSolvent(); }); }); // ========================================================================= // 5. Falcon-512 post-quantum settlement authorization // ========================================================================= describe("PQC settlement (Falcon-512 via 0x0AE1)", function () { it("finalizes only with a valid Falcon-512 authorization; a tampered one fails closed", async function () { const rng = makeRng("pqc-settle"); const pk = falconPubKey(rng); const id = await post({ mode: Mode.OPTIMISTIC, pqcSettlement: true, falconPubKey: pk }); await market.connect(provider).claimJob(id, { value: ethers.parseEther("2") }); await market.connect(provider).submitResult(id, ethers.toUtf8Bytes("pqc-guarded-result")); await assertSolvent(); await network.provider.send("evm_increaseTime", [3601]); await network.provider.send("evm_mine", []); const j = await market.getJob(id); const digest = settlementDigest( chainId, await market.getAddress(), id, provider.address, ZERO, j.reward, j.resultHash ); // Sanity: our off-chain digest matches the contract's. expect(await market.settlementDigest(id, provider.address, ZERO, j.reward, j.resultHash)).to.equal(digest); const goodSig = falconMockEnvelope(pk, digest, rng); // Tampered signature: flip a byte inside the commitment. Payout reverts, nothing paid. const bad = ethers.getBytes(goodSig); bad[41] ^= 0x01; // esig = nonce(40) || 0x29 || commitment ; byte 41 is inside the commitment await expect( market.connect(other).finalize(id, ethers.hexlify(bad)) ).to.be.revertedWithCustomError(market, "NotPQCAuthorized"); expect(Number((await market.getJob(id)).status)).to.equal(Status.Submitted); // unchanged await assertSolvent(); // Genuine Falcon-512 authorization: payout succeeds. const before = await ethers.provider.getBalance(provider.address); await expect(market.connect(other).finalize(id, goodSig)) .to.emit(market, "PQCSettlementAuthorized") .and.to.emit(market, "JobPaid"); const after = await ethers.provider.getBalance(provider.address); expect(after - before).to.equal(ethers.parseEther("12")); // reward 10 + bond 2 expect(Number((await market.getJob(id)).status)).to.equal(Status.Paid); await assertSolvent(); }); it("rejects a Falcon authorization for the wrong amount (digest binds the payout)", async function () { const rng = makeRng("pqc-amount"); const pk = falconPubKey(rng); const id = await post({ mode: Mode.OPTIMISTIC, pqcSettlement: true, falconPubKey: pk }); await market.connect(provider).claimJob(id, { value: ethers.parseEther("2") }); await market.connect(provider).submitResult(id, ethers.toUtf8Bytes("r")); await network.provider.send("evm_increaseTime", [3601]); await network.provider.send("evm_mine", []); const j = await market.getJob(id); // Sign a digest for the WRONG amount (5 AERE instead of 10). const wrongDigest = settlementDigest( chainId, await market.getAddress(), id, provider.address, ZERO, ethers.parseEther("5"), j.resultHash ); const wrongSig = falconMockEnvelope(pk, wrongDigest, rng); await expect( market.connect(other).finalize(id, wrongSig) ).to.be.revertedWithCustomError(market, "NotPQCAuthorized"); await assertSolvent(); }); it("rejects a PQC signature supplied for a non-PQC job", async function () { const rng = makeRng("nonpqc"); const pk = falconPubKey(rng); const id = await post({ mode: Mode.OPTIMISTIC, pqcSettlement: false }); await market.connect(provider).claimJob(id, { value: ethers.parseEther("2") }); await market.connect(provider).submitResult(id, ethers.toUtf8Bytes("r")); await network.provider.send("evm_increaseTime", [3601]); await network.provider.send("evm_mine", []); const junkSig = falconMockEnvelope(pk, ethers.ZeroHash, rng); await expect( market.connect(other).finalize(id, junkSig) ).to.be.revertedWithCustomError(market, "PQCSigNotAllowed"); }); }); // ========================================================================= // 6. Escrow-solvency invariant across a mixed workload, and no-double-pay // ========================================================================= describe("escrow solvency and no double-pay", function () { it("holds the solvency invariant across many concurrent jobs and terminal states", async function () { const vkey = ethers.keccak256(ethers.toUtf8Bytes("vk")); // A batch of jobs in various modes and states. const idA = await post({ mode: Mode.OPTIMISTIC }); const idB = await post({ mode: Mode.REPLAY, challengeWindow: 100, providerBond: ethers.parseEther("1") }); const idC = await post({ mode: Mode.ZK_VERIFIED, providerBond: 0n, zkProgramVKey: vkey, challengeWindow: 0 }); const idD = await post({ mode: Mode.OPTIMISTIC }); // will be cancelled while Open await assertSolvent(); await market.connect(provider).claimJob(idA, { value: ethers.parseEther("2") }); await market.connect(provider).claimJob(idB, { value: ethers.parseEther("1") }); await market.connect(provider).claimJob(idC, { value: 0 }); await assertSolvent(); // Cancel D while Open. await market.connect(requester).cancelJob(idD); await assertSolvent(); // A submits + disputed + slashed. await market.connect(provider).submitResult(idA, ethers.toUtf8Bytes("A")); await market.connect(challenger).dispute(idA, ethers.ZeroHash, { value: ethers.parseEther("2") }); await market.connect(arbiter).resolveDispute(idA, false); await assertSolvent(); // B submits + finalized. await market.connect(provider).submitResult(idB, ethers.toUtf8Bytes("B")); await network.provider.send("evm_increaseTime", [101]); await network.provider.send("evm_mine", []); await market.connect(other).finalize(idB, "0x"); await assertSolvent(); // C settles via ZK. const specHashC = (await market.getJob(idC)).specHash; const rhC = ethers.keccak256(ethers.toUtf8Bytes("C-result")); const pvC = abi().encode(["bytes32", "bytes32"], [specHashC, rhC]); const proofC = "0xabcdef01"; await sp1.setValid(ethers.keccak256(abi().encode(["bytes32", "bytes", "bytes"], [vkey, pvC, proofC])), true); await market.connect(provider).submitResultZK(idC, pvC, proofC, "0x"); await assertSolvent(); // All liabilities cleared. expect(await market.totalLiabilities(ZERO)).to.equal(0n); // No double-pay: finalizing/settling a terminal job reverts. await expect(market.connect(other).finalize(idB, "0x")).to.be.revertedWithCustomError(market, "WrongStatus"); await expect(market.connect(provider).submitResultZK(idC, pvC, proofC, "0x")).to.be.revertedWithCustomError(market, "WrongStatus"); await expect(market.connect(arbiter).resolveDispute(idA, true)).to.be.revertedWithCustomError(market, "WrongStatus"); }); it("reclaims a claimed job that blew its deadline, slashing the provider bond to the requester", async function () { const now = (await ethers.provider.getBlock("latest")).timestamp; const id = await post({ mode: Mode.OPTIMISTIC, deadline: now + 100 }); await market.connect(provider).claimJob(id, { value: ethers.parseEther("2") }); await assertSolvent(); await network.provider.send("evm_increaseTime", [101]); await network.provider.send("evm_mine", []); const before = await ethers.provider.getBalance(requester.address); const tx = await market.connect(requester).reclaimExpired(id); const rc = await tx.wait(); const gas = rc.gasUsed * rc.gasPrice; const after = await ethers.provider.getBalance(requester.address); // Requester recovers reward (10) + slashed provider bond (2), minus gas. expect(after - before + gas).to.equal(ethers.parseEther("12")); expect(Number((await market.getJob(id)).status)).to.equal(Status.Refunded); await assertSolvent(); }); }); });