fix(task-auditor): close the test-data dispute reward-pool drain (#205) - #215
Conversation
…initeZeroFoundation#205) Any address could take 25% of a GI's reward pool per batch: opening a test-data dispute had no caller check, the bond defaulted to 0, and resolveTestDataDispute was callable by anyone, so a caller-chosen junk K always mismatched the commitment and upheld the dispute (repeatable after every reassignment). Per InfiniteZeroFoundation#205 options 1 + 2 (PR InfiniteZeroFoundation#209 Decision 3): - openTestDataDispute: only an auditor of that batch (isBatchAuditor), else TA_NotAssignedAuditor - disputeBondAmount defaults to 100 DIN (matches DINTaskCoordinator .disputeBond); still owner-settable - resolveTestDataDispute: onlyOwner; a matching reveal clears the dispute (bond forfeited), a non-matching owner reveal upholds it - closeExpiredDispute: an unanswered dispute is upheld (bond back, disputePenaltyBps penalty, pendingReassignment) instead of forfeiting the bond -- silence counts against the party holding the data. Upheld logic shared via _upholdTestDataDispute. - DisputeExpired drops its bondForfeited field (ABI regenerated). Option 3 (separate keccak256(K) commitment) is left to InfiniteZeroFoundation#181/InfiniteZeroFoundation#38; the owner-judges-own-data trust assumption is stated in NatSpec. Tests: EncryptedTestData.t.sol rewrites the upheld tests that encoded the attack (owner's mismatching reveal now), adds non-owner resolve, non-batch- auditor open, default bond, owner-silent expiry upholds, early close, and the InfiniteZeroFoundation#205 junk-key scenario at a zero bond; TreasuryForwarding.t.sol uses a batch auditor and asserts the upheld expiry's penalty split. DINTaskAuditor runtime 22,589 -> 22,649 B (margin 1,927 B, gate green). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Reviewed against Claimed: Verified — exact match: Claimed: the tests encode the fix, i.e. they would catch a regression. Verified — break-then-fix: I reverted each fix in
After restoring: 18/18 pass. Claimed: Verified: the bundled Claimed: Verified: 389 passed in a venv with torch (the gap is the torch-dependent files, as the PR says). Finding — an upheld dispute after
|
Files changed (6) — as of
|
| Field | Value |
|---|---|
| Change | Modified |
| Lines | +54/-30 |
| Diff (what exactly is in this PR) | openTestDataDispute: new TA_BatchDoesNotExist + isBatchAuditor checks (TA_NotAssignedAuditor). disputeBondAmount defaults to 100 * 1e18. resolveTestDataDispute becomes onlyOwner. closeExpiredDispute now emits DisputeExpired(gi, batchId) (no bondForfeited) and upholds the dispute through the new internal _upholdTestDataDispute (bond back, disputePenaltyBps of giRewardPool[gi] burned/forwarded, pendingReassignment), which the mismatch branch of resolveTestDataDispute shares. NatSpec states the remaining owner-judges-own-data assumption. |
| Functionality — how & why | How: only an assigned auditor can stake a bond to challenge the batch's test data. Only the data holder can answer, by revealing K. A match forfeits the bond; a mismatch or silence past the window upholds. Why: issue No. 205: any address could open with a 0 bond and "resolve" with a junk K. The mismatch always upheld, so a free, repeatable 25% pool burn per batch. Each of the four changes is pinned by a test (see the verification comment's break-then-fix). Size 22,649 B (+60), margin 1,927 B, gate green (warn band). |
Diff vs current develop HEAD |
None — untouched by develop since merge-base |
| Recommended merge proposal | Merge, with one fix (pre-existing behaviour, now reachable through owner silence): an upheld dispute after settleRewards penalises giRewardPool[gi], which claims no longer read. That leaves the GI's snapshot underfunded (scratch test: 950.95 DIN owed, 700.70 DIN held). Suggested: in openTestDataDispute, revert when giRewardSnapshot[gi].settled, and in _upholdTestDataDispute, skip the penalty (or take it from the snapshot pools) if the GI settled while the dispute was open. |
| Actual merge proposal | Merged as-is in ac6f8f9, then fixed in 740a613 (the settlement fix, done in this merge). openTestDataDispute reverts with the new TA_RewardsAlreadySettled (in DINShared.sol) once giRewardSnapshot[gi].settled. _upholdTestDataDispute takes no pool penalty if the GI settled while the dispute was open; the bond is still returned and the batch still flagged. Size: 22,718 B runtime (+69 B), 1,858 B margin, warning band. Break-then-fix: reverting either guard fails its new test. Evidence on 740a613: forge clean && forge build (via_ir) clean; size gate exit 0; forge test 491 passed / 0 failed / 0 skipped; pytest -m "not integration" 395 passed / 0 failed. |
| Pending proposal | Post-settlement dispute penalty: fix in this PR or open a follow-up issue (Umer's call). |
| Local merge conflict | No |
| GitHub merge conflict | No |
foundry/test/EncryptedTestData.t.sol
| Field | Value |
|---|---|
| Change | Modified |
| Lines | +95/-16 |
| Diff (what exactly is in this PR) | The disputer is now batch 0's auditor (disputer = auditors[0] in _assignBatch0). The upheld_* tests uphold through the owner's own non-matching reveal. New: test_resolveDispute_nonOwner_reverts, test_openDispute_nonBatchAuditor_reverts, test_defaultDisputeBond_isNonZero, test_closeExpiredDispute_ownerSilent_upholds, test_closeExpiredDispute_beforeWindowEnds_reverts, and test_issue205_junkKeyDrain_isClosed (the issue's scenario at a 0 bond). |
| Functionality — how & why | How: these drive a real platform + task pair to AuditorsBatchesCreated, assign batch 0 with a known commitment, then exercise each dispute path. Why: the old upheld tests encoded the attack (a non-owner junk reveal winning). The new set fails when any of the four fixes is reverted. 18/18 pass. |
Diff vs current develop HEAD |
None — untouched by develop since merge-base |
| Recommended merge proposal | Merge as-is. If the settlement fix goes in, add a test for it here (open/uphold after settleRewards). |
| Actual merge proposal | Merged as-is in ac6f8f9. 740a613 adds test_openDispute_afterSettle_reverts and test_upheldAfterSettle_skipsPenalty (bond refunded, pool untouched, contract balance covers the snapshot pools, batch flagged), and updates the header coverage notes (expiry now upholds; settlement). 20/20 in the suite. Evidence on 740a613: forge clean && forge build (via_ir) clean; size gate exit 0; forge test 491 passed / 0 failed / 0 skipped; pytest -m "not integration" 395 passed / 0 failed. |
| Pending proposal | Settlement-path test, if the source fix lands. |
| Local merge conflict | No |
| GitHub merge conflict | No |
foundry/test/TreasuryForwarding.t.sol
| Field | Value |
|---|---|
| Change | Modified |
| Lines | +12/-9 |
| Diff (what exactly is in this PR) | The dispute helper opens from a batch auditor. The expiry test asserts the upheld split instead: the penalty goes 50% burned / 50% to the treasury, and the bond is refunded. |
| Functionality — how & why | How: same helpers, new expectations. Why: an expiry no longer forfeits the bond, so the old "bond forfeited 50/50" assertion would encode removed behaviour. The full suite passes (489/489). |
Diff vs current develop HEAD |
None — untouched by develop since merge-base |
| Recommended merge proposal | Merge as-is. |
| Actual merge proposal | Merged as-is in ac6f8f9, byte-identical to PR head. Evidence on 740a613: forge clean && forge build (via_ir) clean; size gate exit 0; forge test 491 passed / 0 failed / 0 skipped; pytest -m "not integration" 395 passed / 0 failed. |
| Pending proposal | None |
| Local merge conflict | No |
| GitHub merge conflict | No |
dincli/abis/DINTaskAuditor.json
| Field | Value |
|---|---|
| Change | Modified |
| Lines | +0/-6 |
| Diff (what exactly is in this PR) | Removes the bondForfeited input from the DisputeExpired event entry. |
| Functionality — how & why | How: regenerated ABI. It is identical (as a sorted entry set) to foundry/out/DINTaskAuditor.sol/DINTaskAuditor.json from my via_ir build. Why: keeps the bundled ABI in step with the contract. No dincli or other repo consumer reads this event. |
Diff vs current develop HEAD |
None — untouched by develop since merge-base |
| Recommended merge proposal | Merge as-is (regenerate again if the settlement fix changes the ABI; a guard alone won't). |
| Actual merge proposal | Merged as-is in ac6f8f9, then regenerated in 740a613 with dump-abi --official from the merged build. The only diff is the new TA_RewardsAlreadySettled error entry. |
| Pending proposal | None |
| Local merge conflict | No |
| GitHub merge conflict | No |
Documentation/technical/contracts/DINTaskAuditor.md, Documentation/technical/contracts/DINShared.md
| Field | Value |
|---|---|
| Change | Modified (2 files) |
| Lines | +8/-6, +1/-1 |
| Diff (what exactly is in this PR) | DINTaskAuditor.md: §3 default bond 100 DIN; §4 access table (new "Auditor of the disputed batch" row; openTestDataDispute/resolveTestDataDispute removed from "Any address"); §10 dispute table rewritten for owner resolve + silence upholds; §13 No. 1 marked Fixed, with the trust assumption and issue No. 181; change-log line. DINShared.md: TA_NotAssignedAuditor row now covers openTestDataDispute. |
| Functionality — how & why | How: documentation only. Why: it keeps the contract docs describing develop. Link check: all resolve. No other doc under Documentation/ describes the old anyone-resolves or bond-forfeited-on-expiry behaviour (grep). |
Diff vs current develop HEAD |
None. PR No. 214 edits DINTaskAuditor.md §13 No. 9 only, which is disjoint from these hunks. |
| Recommended merge proposal | Merge as-is. If the settlement fix lands, add a line to §10/§13 noting that disputes close at settlement. |
| Actual merge proposal | Merged as-is in ac6f8f9; DINTaskAuditor.md auto-merged with PR No. 214's §13 No. 9 hunk. 740a613: §10 table (settled-GI guard, no penalty after settlement), a stale §10 line that still said anyone can open and win a dispute is replaced, a change-log line, and a TA_RewardsAlreadySettled row in DINShared.md. check_doc_links.py Documentation: 258 links, all resolve. |
| Pending proposal | Doc line for the settlement fix, if it lands. |
| Local merge conflict | No |
| GitHub merge conflict | No |
Verification
Full detail is in the verification comment above. In short: forge build (via_ir) passes. forge test gives 489/489 in 39 suites. The size gate is green (DINTaskAuditor 22,649 B, margin 1,927 B, warn band). Break-then-fix: 4 of 4 reverted fixes are caught. The bundled ABI matches the build output. pytest -m "not integration" gives 389 passed. The lint gate is clean, doc links resolve, git diff --check is clean, and GitHub CI is green. One finding: an upheld dispute after settleRewards underfunds the GI's claims (pre-existing, but now reachable through owner silence).
Local vs. GitHub agree: both report a clean merge; no file overlap with the 1 commit develop gained since the merge-base.
Actual outcome — PR No. 215 merged + settlement fix applied (pushed)This supersedes the pre-merge review comment above with what actually happened when landing this PR on Two commits, both on
Files unchanged from the PR (1 of 6)
Files changed by
|
| File | What changed vs. this PR's merged version |
|---|---|
foundry/src/DINTaskAuditor.sol |
openTestDataDispute reverts with TA_RewardsAlreadySettled once giRewardSnapshot[gi].settled. _upholdTestDataDispute sets the penalty to 0 if the GI settled while the dispute was open. The bond is still returned, the batch still flagged pendingReassignment, and TestDataDisputeUpheld still emitted (with ownerPenalty 0). NatSpec updated. |
foundry/src/DINShared.sol (outside the PR) |
New error TA_RewardsAlreadySettled(). |
foundry/test/EncryptedTestData.t.sol |
New test_openDispute_afterSettle_reverts. New test_upheldAfterSettle_skipsPenalty: open, settle, owner silent, expire. It asserts the bond is refunded, giRewardPool is untouched, the contract balance covers the snapshot's client + auditor + aggregator pools, and the batch is flagged. Header coverage notes updated. |
dincli/abis/DINTaskAuditor.json |
Regenerated with dump-abi --official; the only diff is the new error entry. |
Documentation/technical/contracts/DINTaskAuditor.md |
§10 table: the settled-GI guard on open, and no penalty after settlement. The §10 line "anyone can open one … any caller can trigger the upheld branch with a wrong K" was stale after this PR; it's replaced with the settlement rule. Change-log line added. |
Documentation/technical/contracts/DINShared.md |
TA_RewardsAlreadySettled row. |
Why the change
Not develop drift. It's the review finding: claims pay from giRewardSnapshot[gi], so a giRewardPool[gi] penalty after settleRewards took tokens claimants were already owed (scratch test: 950.95 DIN owed, 700.70 DIN held). The bug predates this PR, but the PR's silence-upholds rule made it reachable without any owner action. The open guard alone doesn't cover a dispute opened before settlement and upheld after it, hence the second check in _upholdTestDataDispute.
Verification
On the pushed tree, 740a613 (PR No. 214 + this PR + the fix):
forge clean && forge build(via_ir) is clean.contract_size_gate.pyexits 0.DINTaskAuditoris 22,718 B (+69 B over the PR, 1,858 B margin) andDINTaskCoordinatoris unchanged at 23,497 B (1,079 B); both are in the warning band.forge test: 491 passed, 0 failed (the PR's 489 + 2 new).- Break-then-fix: reverting the open guard fails
test_openDispute_afterSettle_reverts; reverting the penalty skip failstest_upheldAfterSettle_skipsPenalty. pytest -m "not integration": 395 passed, 0 failed.forge_lint_gate.py: 0 diagnostics at error/warning level. Doc links inDocumentation(258) all resolve.git diff --checkis clean.- The local CI mirror on
740a613passed: docs, pytest, via_ir build, forge test and hardhat. - GitHub
pushrun ondevelop: green (Docs, Python, Solidity incl. the contract size gate, CI OK).
Local vs. GitHub agree: clean merge with no conflicts, as both predicted. Issue No. 205 is fixed by this PR plus 740a613.
Summary
PR 4 of task_021026_19, Part E. It implements #205 options 1 + 2 as agreed in PR #209 Decision 3. Closes #205.
The bug: any address could take 25% of a GI's reward pool per batch, with no DIN and no role, and repeat it after every reassignment:
openTestDataDisputehad no caller check.disputeBondAmountdefaulted to 0.resolveTestDataDisputewas callable by anyone, so a junkKalways mismatched the commitment and upheld the dispute.Changes (
DINTaskAuditor.sol)openTestDataDisputeisBatchAuditor). Anyone else getsTA_NotAssignedAuditor, and an unknown batch getsTA_BatchDoesNotExistdisputeBondAmountdefault100 * 1e18), matchingDINTaskCoordinator.disputeBond. Still settable throughsetDisputeBondAmount; revisit with #155's valuesresolveTestDataDisputeonlyOwner. A reveal that matches the commitment clears the dispute and forfeits the bond. A non-matching owner reveal upholds itcloseExpiredDispute(owner silent past the window)disputePenaltyBpspenalty, batchpendingReassignment. Silence counts against the party holding the data_upholdTestDataDispute.DisputeExpired(gi, batchId)drops itsbondForfeitedfield, because an expiry no longer forfeits the bond.TestDataDisputeUpheldfollows it. That is the only ABI change, anddincli/abis/DINTaskAuditor.jsonis regenerated. Neither the subgraph (PR feat(indexer): implement DIN Protocol subgraph — platform + task-level contracts #29) nor dincli consumes this event or calls these functions.keccak256(K)commitment, is left to P3 dispute: decentralize adjudication — owner-adjudicator trust assumption must be removed before mainnet #181/P3 slashing: conditions taxonomy, penalty tiers, slashed-stake destination, dispute resolution (P3-4.1/4.2/4.3) #38. The NatSpec states the remaining trust assumption: the owner reveals evidence about their own data, and a bad plaintext behind a correctKcan't be proven on-chain.Tests
EncryptedTestData.t.sol(18 tests):test_resolveDispute_upheld_*, now uphold through the owner's own non-matching reveal. The disputer is now batch 0's auditor.test_resolveDispute_nonOwner_reverts:OwnableUnauthorizedAccount, and the dispute stays open.test_openDispute_nonBatchAuditor_revertstest_defaultDisputeBond_isNonZero:100 * 1e18.test_closeExpiredDispute_ownerSilent_upholds: permissionless; the bond is returned, the pool penalised and the batch flagged; checks theDisputeExpiredevent.test_closeExpiredDispute_beforeWindowEnds_revertstest_issue205_junkKeyDrain_isClosed: the security: anyone can drain a GI reward pool through test-data disputes (unauthenticated resolveTestDataDispute, zero default bond) #205 scenario at the old zero bond. The outsider can't open, a batch auditor who opens can't resolve, and the pool and batch are untouched.TreasuryForwarding.t.sol:Size
DINTaskAuditorruntime goes from 22,589 B to 22,649 B (+60 B), a 1,927 B margin. The size gate passes, with the 2,048 B warning.Verification
forge clean && forge build && contract_size_gate.py && forge test: 489 passed, 0 failed (39 suites).pytest -m "not integration": 313 passed. The torch-dependent files and the 2test_integration_markerchecks can't run in my local venv, the same as ondevelop.forge lint src/DINTaskAuditor.sol: the same single pre-existing warning as ondevelop.check_doc_links.py Documentation: all links resolve.DINTaskAuditor.md: §3 default bond, §4 access table, §10 dispute table, §13 No. 1 (fixed), and the change log.DINShared.md: theTA_NotAssignedAuditorrow.git merge-treeagainst fix(cli): keep auditor commit salts across retries; name aggregate-t2 work after the T2 batch (#202) #214's head is clean.🤖 Generated with Claude Code