Repository navigation
Description
Summary
Found while reviewing PR No. 204 (verification comment, surfaced item A). This is older than PR No. 204 and out of its scope. PR No. 204 records it as a caveat in DINTaskAuditor.md §13 No. 1, but nothing tracks a fix. Line numbers are for develop @ e373c8d.
Any address can take 25% of a GI's reward pool per batch through a test-data dispute. It needs no DIN, no stake, and no role in the batch. The attack can be repeated after every reassignment, and each round also blocks the batch.
Problem
foundry/src/DINTaskAuditor.sol:
openTestDataDispute(gi, batchId)(L1461) has no caller check. It only needs a stored commitment, no active dispute, and no pending reassignment. It pullsdisputeBondAmount, which is 0 at deploy (L283).resolveTestDataDispute(gi, batchId, K, plaintextHash)(L1495) is also callable by anyone. Its NatSpec says so: "Anyone may call this — the disputer is the beneficiary if upheld". It recomputeskeccak256(gi, batchId, keccak256(K), plaintextHash)and compares it with the owner's commitment:- match (L1510) → dispute false, bond forfeited;
- mismatch → dispute upheld: bond returned,
disputePenaltyBps(2500 = 25%, L285) ofgiRewardPool[gi]burned or forwarded via_burnAndForward(L1527), and the batch set topendingReassignment.
A mismatch can't tell "the owner committed bad test data" apart from "the caller passed a made-up K". K and plaintextHash are both caller-supplied and neither is checked independently, so the caller controls which branch runs.
Failure scenario
Proven with a throwaway forge test that extends EncryptedTestDataTest. It was run and deleted, not committed.
- The model owner assigns batch 0's test data with a valid commitment (
assignAuditTestDataset). outsiderhas no DIN, isn't a batch auditor, and the bond is the default 0. They callopenTestDataDispute(1, 0), thenresolveTestDataDispute(1, 0, junkK, 0x0).- The dispute is upheld:
giRewardPool(1)drops 25%, and batch 0 ispendingReassignmentuntil the owner callsreassignAuditTestDataset. - The outsider repeats after each reassignment.
After 3 rounds the pool is at 0.75³ ≈ 42% of its starting value (asserted below 50%), and token.balanceOf(outsider) == 0 throughout. The existing suite already has this shape as intended behavior: test_resolveDispute_upheld_returnsBondAndPenalises has the disputer resolve their own dispute with wrongK and win.
Fix
This needs a small design decision first. Options:
- Only the owner's reveal can resolve. Make
resolveTestDataDisputeowner-only: the owner reveals the trueKand plaintext hash, and a match clears the dispute. If the owner doesn't reveal withindisputeWindowBlocks,closeExpiredDisputeupholds the dispute (bond back, penalty, reassignment) instead of forfeiting the bond. This reverses today's default: silence counts against the party who holds the data. - Restrict who can open. Only an auditor of that batch (
isBatchAuditor) can open a dispute, anddisputeBondAmountgets a non-zero default so each attempt costs something. - Separate the key check from the content check. Commit
keccak256(K)on its own, so an on-chain check can prove a revealedKis the real key. A content mismatch (a bad plaintext) still can't be proven on-chain without the data, so it needs either option 1's "owner must reveal" rule or off-chain adjudication (see No. 38 on dispute resolution).
Option 1 + option 2 closes the free-griefing path without new cryptography. Tests to add or change:
- a non-owner resolve reverts, or can't reach the "upheld" branch;
- an owner reveal with the true
Kclears the dispute; - an owner who stays silent past the window loses (upheld);
- a non-batch-auditor open reverts;
- update
test_resolveDispute_upheld_*, which currently encode the attack.
Size note: this contract has headroom, unlike DINTaskCoordinator (No. 201 Part A).
Related
No. 38 (dispute resolution, P3-4.3), No. 115 (encrypted test-data key assignment), No. 192 (auditor commit hash), PR No. 204.
Activity
docs(tasks-plan): record reviewer decisions 1-4 in task plan 021026-1
docs(tasks-plan): add umermjd11 task plan 021026-1 (#201 A, #206, #192,
docs(tasks): add task_021026_19 — #201 A size gate, #206, #192, #202, #…
fix(task-auditor): close the test-data dispute reward-pool drain (#205)
umeradl commentedon Oct 5, 2026
Done by PR No. 215, merged into
developasac6f8f9, with the review fix in740a613(outcome comment, greendeveloppush run). Closing by hand, because a merge intodevelopdoesn't trigger closing keywords (mainis the default branch).Checked on
origin/develop@740a613, against options 1 + 2 as agreed in PR No. 209 Decision 3 (task_021026_19 Part E):resolveTestDataDisputeisonlyOwner(foundry/src/DINTaskAuditor.sol:1524). A matching reveal clears the dispute and forfeits the bond; a non-matching one upholds it (:1546).closeExpiredDisputeupholds an unanswered dispute through the shared_upholdTestDataDispute(:1557-1563,:1571): bond back,disputePenaltyBpspenalty, batchpendingReassignment.openTestDataDisputerequiresisBatchAuditor[gi][batchId][msg.sender](:1485,TA_NotAssignedAuditor).disputeBondAmountdefaults to100 * 1e18(:288) and stays settable throughsetDisputeBondAmount.740a613): a dispute upheld aftersettleRewardsused to take its penalty from tokens claimants were already owed. NowopenTestDataDisputereverts withTA_RewardsAlreadySettledonce the GI is settled (:1486), and_upholdTestDataDisputetakes no pool penalty if the GI settled while the dispute was open (:1581).foundry/test/EncryptedTestData.t.sol): every test the issue asks for:test_resolveDispute_nonOwner_revertstest_resolveDispute_falseDispute_forfeitsBond: an owner reveal with the trueKclears the dispute.test_closeExpiredDispute_ownerSilent_upholdstest_openDispute_nonBatchAuditor_revertstest_defaultDisputeBond_isNonZerotest_resolveDispute_upheld_*now uphold through the owner's own non-matching reveal.test_issue205_junkKeyDrain_isClosed: the failure scenario at the old 0 bond. The outsider can't open at all, so the repeat-after-reassignment loop can't start; a batch auditor who opens can't resolve; the pool is untouched.test_openDispute_afterSettle_revertsandtest_upheldAfterSettle_skipsPenaltyfrom740a613.forge test491 passed, 0 failed.pytest -m "not integration"395 passed. TheDINTaskAuditorsize gate is green at 22,718 B (1,858 B margin). No dincli command calls these functions, and the bundled ABI is regenerated.DINTaskAuditor.md§13 No. 1 is marked fixed and states the remaining trust assumption. §3, §4, §10 and the change log are updated, andDINShared.mdcovers both errors.Out of scope: option 3 (a separate
keccak256(K)commitment) and the owner-judges-own-data trust assumption are tracked in issue No. 181 and issue No. 38, both open.