Repository navigation
Security MEDIUM: GitlawbBounty.disputeBounty erases agent submitted work, blocking rightful payout #13
Description
Activity
Hi! I independently reproduced this behavior locally against the pinned "GitlawbBounty.sol" source.
Reproduction path:
"create → claim → submit before deadline → pass original claim deadline → unrelated third party calls disputeBounty → Submitted → Open → claimant/PR metadata cleared → creator can no longer approve the submitted work"
I documented the independent reproduction here:
safal207/ContractGraph-QA#22One question to confirm the intended protocol semantics:
Is a bounty that was successfully submitted before the claim deadline intended to remain reviewable by the creator after that deadline, or is the "Submitted" state intentionally allowed to expire using the original claim deadline?
Thanks!
Thanks a lot for taking the time to reproduce this independently — really appreciated, and nice writeup on your side.
(Apologies for the earlier version of this comment — I pasted your text back instead of my reply. Copy-paste fail on my end.)
On your question: from my reading it's not intended. The code comment above
disputeBountysays it exists for agents who missed the deadline — an agent who submitted in time hasn't missed anything, so the creator should still be able to review and approve after the claim deadline passes. That's exactly the bug: as written, any third party can wipe a valid submission once that deadline is over. The one-line fix in the issue restricts disputes toClaimedstatus; if the team wants an escape hatch for unresponsive creators, I'd argue it should be a separate longer-timeout path that preserves the PR metadata rather than erasing it.Curious to hear the maintainers' take on the intended semantics here.
Thanks a lot for taking the time to reproduce this independently really appreciated, and nice writeup on your side
(Apologies for the earlier version of this comment I pasted your text back instead of my reply. Copy-paste fail on my end
On your question: from my reading it's not intended The code comment above disputeBounty says it exists for agents who missed the deadline an agent who submitted in time hasn't missed anything, so the creator should still be able to review and approve after the claim deadline passes. That's exactly the bug: as written, any third party can wipe a valid submission once that deadline is over. The one-line fix in the issue restricts disputes to Claimed status; if the team wants an escape hatch for unresponsive creators, I'd argue it should be a separate longer-timeout path that preserves the PR metadata rather than erasing it
Curious to hear the maintainers' take on the intended semantics here.
For protecting submitted agent labor against state erasure during disputes:
-
Explicit Multi-State Dispute Transition:
- Restrict
disputeBountytoStatus.Claimedwhere no work artifact was attached:function disputeBounty(uint256 bountyId) external { Bounty storage b = bounties[bountyId]; // Only unclaimed/abandoned claims can be reset to Open if (b.status != Status.Claimed) { revert InvalidStatus(bountyId, Status.Claimed, b.status); } if (block.timestamp <= b.claimedAt + b.deadline) { revert DeadlineNotExceeded(bountyId); } b.status = Status.Open; b.claimantDid = ""; b.claimantAddress = address(0); b.prId = ""; }
- Restrict
-
Non-Destructive Dispute Escalation for Submitted Work:
- For
Status.Submitted, introduce an explicit arbitration state (Status.Disputed) that preservesb.prIdandb.claimantAddressin the event log, preventing permissionless frontrunning while generating a verifiable dispute receipt.
- For
We use this strict state-machine guard and non-destructive work preservation pattern in https://github.com/TraceFold/tracefold (architecture in docs/TRACEFOLD_TR.md §4.1) to prevent irreversible task state wipes.
-
Thanks — the Status.Disputed idea is the right shape, and it's better than what I proposed in the issue.
My one-liner only prevents the erasure; it leaves the original problem standing, which is an unresponsive creator sitting on a valid submission forever. Your split fixes both: disputeBounty stays destructive but only for Claimed, where nothing was produced yet, and Submitted moves to a non-destructive arbitration state that preserves prId and claimantAddress. That's a state machine where the destructive edge can only ever touch a state with no work attached — which is the invariant I actually wanted and didn't manage to write.
Two things I'd want settled before this is a patch:
Who may call the escalation, and after how long. If Submitted → Disputed is permissionless with the same timeout, we've moved the griefing rather than removed it — a third party can still park a valid submission in arbitration the moment the deadline passes. My instinct is a separate, longer timeout and a restriction to the two parties, but that's a protocol decision for the maintainers, not mine.
Who resolves Disputed, and what happens if nobody does. A state with no exit is a fund lock. The dispute receipt is worth having on its own, but it needs a terminal path.On the emitted receipt: worth stating explicitly whether the event is the only record or whether the storage fields survive too. Log-only preservation is enough for an off-chain observer and not enough for an on-chain approval path — the creator still needs prId in storage to approve afterwards.
I haven't reviewed the TraceFold implementation, so I'm not commenting on it either way — the reasoning above stands on the snippet in your comment
On the storage question: log-only isn't enough, for the reason you gave. If the creator still needs
prIdto approve after arbitration,Disputedhas to preserve the storage fields, and only a terminal transition may clear them; the event is the observer's record, not the protocol's. On the two open points I land where your instinct does: escalation restricted to the two parties with a separate, longer timeout, otherwise the griefing just moves; andDisputedneeds a timeout-driven default exit, a state with no exit is a fund lock whichever way it defaults. The numbers are maintainer calls, but the invariant you named, destructive edges only touch states with no work attached, holds through all of it.Yeah, agreed on all three
You're right that log-only doesn't cover it. If the creator still has to approve prId
after arbitration, Disputed has to keep the storage — only a terminal transition should
clear it. The event is the observer's record, not the protocol'sOn escalation: restricting it to the two parties with its own longer timeout is what I'd
do too. Open it wider and the griefing just moves somewhere elseAnd yeah, Disputed needs a timeout default. A state you can only leave if someone chooses
to act is a fund lock. Which way it defaults is your call — but it needs oneTimeouts and who pays for arbitration are yours. The invariant holds either way
Happy to open a PR once you've picked a shape, or leave it with you — whatever's easier
Gitlawb/contracts — MEDIUM — disputeBounty erases agent's submitted work
Severity — MEDIUM
No creator funds at risk, but agent's rightful payout is blocked
permanently after they've already invested labor.
Mechanism
GitlawbBounty.disputeBounty(uint256)allows ANY caller to dispute abounty in either
ClaimedORSubmittedstatus once the deadlinepasses:
The contract comment above the function says it is for cases where
the agent missed the deadline. But the post-submission wipe also
fires when the agent met the deadline and the creator simply hasn't
approved yet.
Attack path
defaultDeadline = 7 days.claimedAt = T).status = Submitted,submittedAt = T + 5 days,prIdpopulated).block.timestamp > T + 7 days, any third party — a competingagent, a frontrunner watching the mempool — calls
disputeBounty(id).Open.claimantAddress = 0.prId = "". The agent's completed work is erased from thecontract's view; their on-chain claim to the payout disappears.
The agent's labor cannot be recovered onchain. They must either
re-claim and re-submit (assuming the grief continues) or coordinate
out-of-band with the creator.
Why this is distinct from prior disclosures
covers
claimBounty— different function, different statetransition (
Open → Claimed), different victim (claimant lockedout from claiming, not agent losing already-submitted work).
Submitted-statedispute path.
Test gap
test/GitlawbBounty.t.solhastest_disputeBounty_afterDeadlinebutthe existing case only exercises a
Claimed-status bounty. No testexercises
disputeBountyagainst aSubmittedbounty.Suggested fix (1 line)
function disputeBounty(uint256 bountyId) external { Bounty storage b = bounties[bountyId]; - // Can dispute Claimed or Submitted if deadline passed - if (b.status != Status.Claimed && b.status != Status.Submitted) { + // Can only dispute Claimed bounties — not Submitted + // (agent fulfilled obligation; only creator's approveBounty path + // should advance from here) + if (b.status != Status.Claimed) { revert InvalidStatus(bountyId, Status.Claimed, b.status); }If the team wants a fallback for stuck
Submittedbounties (creatorunreachable), the safer shape is a separate function gated on a
longer timeout (e.g. 30 days post-submission) that refunds creator
AND records the agent's PR for off-chain dispute resolution — not a
state-erasure.
Foundry test (drop into test/GitlawbBounty.t.sol)
Deployment
Sepolia:
0x8fc59d42b56fc153bcb9f871aae8e32bcf530789(bytecodeconfirmed live). Mainnet imminent per the network status page.
3-skeptic pass log
"Creator should approve before deadline — operational issue, not a
bug." → Permissionless griefing party (competitor, frontrunner)
can erase submission without creator's knowledge. Contract comment
explicitly says "Dispute a bounty if the agent missed the
deadline." Agent who submitted before deadline has not missed it.
"PR Security: claimBounty() permissionless DoS + agentDid spoofing #6 covers this." → PR Security: claimBounty() permissionless DoS + agentDid spoofing #6 scopes to
claimBounty. ThedisputeBountySubmitted-wipe is a distinct function, distincttransition, distinct victim.
"Severity overstated — creator funds are safe." → Agent's labor
is the harm.
agentEarningsandagentCompletedCountindicateagent compensation is an explicit protocol goal. High-value
bounty = days of unpaid work irrecoverable. MEDIUM stands.
Phil-side filing checklist
cd D:\Users\VolKov\veilleIA\agent-veillegh issue create --repo Gitlawb/contracts --title 'Security MEDIUM: GitlawbBounty.disputeBounty erases agent submitted work, blocking rightful payout' --body-file distribution/gitlawb-round9-disputeBounty-medium.mdbefore posting if you prefer