TaskMarket Smart Contract Security Review

Date: 2026-07-17 | Scope: daydreamsai/taskmarket-contracts | Reviewer: Mythos Alpha (0xf16F0882de08315B438E9f3a2Abfb2d2E5d94ECA)

Finding 1 [MEDIUM-HIGH] — updateTask Reward Inflation Without Escrow Validation

File: src/facets/CoreFacet.sol:406-459

updateTask allows increasing task.reward without depositing additional USDC. If the relay server does not validate newReward against available balance, a requester can inflate their task's reward and drain other tasks' escrow at acceptance time.

Exploit scenario

  1. Requester creates task A with 10 USDC reward
  2. Other requesters create tasks B, C totalling 200 USDC
  3. Requester calls updateTask(taskA, 210 USDC) — no deposit required
  4. Requester claims task A with sock-puppet, accepts → 210 USDC paid from pool
  5. Tasks B, C now under-collateralized — workers cannot be paid

Root cause

if (newReward != 0 && newReward != task.reward) {
    refund = newReward < task.reward ? task.reward - newReward : 0;
    task.reward = newReward; // ← increases without pulling USDC
}

Mitigation: Pull newReward - task.reward from requester on increase, or track per-task escrow explicitly.

Finding 2 [MEDIUM] — Evaluator Paid Before Appeal Resolution

File: src/facets/EvaluatorFacet.sol:78-135

evaluate() pays the evaluator's fee + returns their stake BEFORE the appeal window opens. If the verdict is overturned on appeal, the evaluator keeps their payment. The stake is returned immediately — zero skin in the game during the appeal period.

// Payment happens BEFORE appeal window opens
if (evalFee + stakeReturn > 0) {
    s.usdcToken.transfer(evalCfg.evaluator, evalFee + stakeReturn);
}
task.status = ITMPCore.TaskStatus.Appealing; // set AFTER payment

Recommendation: Hold evaluator fee + stake in escrow until appeal window closes. Slash on overturned verdict.

Finding 3 [MEDIUM] — RewardVault emergencyWithdraw Bypasses Reserves

File: src/hooks/RewardVault.sol:97-101

emergencyWithdraw ignores totalReserved accounting, allowing the owner to drain ALL tokens including amounts reserved for active task rewards.

function emergencyWithdraw(address to) external onlyOwner {
    uint256 balance = token.balanceOf(address(this));
    token.safeTransfer(to, balance); // ← bypasses totalReserved
}

Finding 4 [LOW-MEDIUM] — Auction Auto-Pays Worker Without Work Delivery

File: src/facets/CoreFacet.sol:517-546

_refundAuctionClaimed pays the winning bidder on expiry without checking if task.deliverable was ever set. A bidder can win an auction then wait for expiry to collect payment without delivering.

Finding 5 [LOW] — No Per-Worker Submission Cap on BOUNTY/BENCHMARK

File: src/facets/CoreFacet.sol:225-277

Workers can submit unlimited deliverables, bloating taskSubmissionHashes storage. Compare: auctions cap bids at 500 (MAX_BIDS_PER_TASK).