Spec 07 — TaskMarket Program
Owner: anchor-engineer Depends on: 03, 04, 06 Blocks: 08 (indexer needs TaskMarket IDL), 10 (portal marketplace), 12 (e2e) References: backend PDF §2.1 (CU budgets: ~120K create, ~80K submit, ~400K verify+settle), §2.4 (full spec, state machine, Groth16 integration), §2.6 (14-day timelock — critical path), §5.1 (Re-entrancy, Integer Safety, Token Safety, Account Validation)
Goal
On-chain task escrow and lifecycle. A client creates a task with committed hash + deadline + escrowed payment; an assigned agent submits a result hash + proof reference; ProofVerifier confirms the Groth16 proof; funds release to the agent minus protocol fee and SolRep fee. Expired tasks refund the client. Disputed tasks park state for DisputeArbitration (wired in M2; M1 stubs the Disputed transition).
Jito bundle support: create_task + fund_task intended to ship atomically so a funded task either fully exists or does not — ordering handled by the client submitting both instructions in one Jito bundle.
State
MarketGlobal PDA — singleton
- Seeds:
[b"market_global"] - Fields:
authority: Pubkeyagent_registry: Pubkeytreasury_standard: Pubkeyproof_verifier: Pubkeyfee_collector: Pubkey— stubbed M1, set to authoritysolrep_pool: Pubkey— stubbed M1protocol_fee_bps: u16— 10 (0.10%) per §2.4solrep_fee_bps: u16— 5 (0.05%)dispute_window_secs: i64— 86_400 (24h) per §2.4max_deadline_secs: i64— 30 days defaultallowed_payment_mints: [Pubkey; 8]— USDC-dev, SOL-wrapped, SAEP-mock seededpaused: boolbump: u8
TaskContract PDA (per backend §2.4)
- Seeds:
[b"task", client.as_ref(), task_nonce.as_ref()]wheretask_nonce: [u8; 8] - Fields:
task_id: [u8; 32]—= Poseidon2(client || task_nonce || created_at)client: Pubkeyagent_did: [u8; 32]payment_mint: Pubkeypayment_amount: u64protocol_fee: u64— computed at createsolrep_fee: u64task_hash: [u8; 32]— Poseidon2 of task description (client-provided)result_hash: [u8; 32]— set on submitproof_key: [u8; 32]— IPFS/Arweave CID of the proof blob, or 0 until submitcriteria_root: [u8; 32]— Merkle root of success criteria, matches circuit public inputmilestone_count: u8— 0 = single payment; 1..=8 allowed in M1milestones_complete: u8status: TaskStatuscreated_at: i64funded_at: i64deadline: i64submitted_at: i64dispute_window_end: i64— set at verify time =deadline + dispute_window_secsverified: boolbump: u8
TaskStatus
enum TaskStatus {
Created, // created, not yet funded
Funded, // escrow holds payment
InExecution, // agent acknowledged; optional intermediate — M1 may skip
ProofSubmitted, // result_hash + proof_key written
Verified, // ProofVerifier returned Ok
Released, // funds paid out
Expired, // past deadline, refunded
Disputed, // client raised dispute in window (M2 wires DisputeArbitration)
Resolved, // terminal after dispute (M2)
}
TaskEscrow PDA — SPL token account per task
- Seeds:
[b"task_escrow", task.key().as_ref()]
Scaffold-vs-spec §State deltas
As-built surface (programs/task_market/src/{state,guard}.rs) diverges from the pre-edit intent above. Landing arc: 25a6c63 (initial scaffold) → 84c1253 (real AgentAccount reads per F-2026-03 caller) → 3c71455 (typed payload + commit-reveal bidding + treasury call-target whitelist) → b435db7 (personhood gate F-2026-01 + hook allowlist F-2026-05) → c759a7b (reentrancy-guard primitives F-2026-04 #7) → 41d18ff (F-2026-01/03/04/05/06/07/08 close-out) → 2241925 (claim_bond winner check fix) → cd5b594 (reset_guard extract across all 7 guard modules).
MarketGlobaldrift (2 absent):pending_authority: Option<Pubkey>(state.rs:48) — two-step auth staging slot; cohort now 6-of-6 across capability / treasury / dispute / proof_verifier / agent_registry / task_market — universal pattern confirmed.hook_allowlist: Pubkey(:64) —fee_collector::HookAllowlistPDA pointer for F-2026-05 TransferHook whitelist; startsPubkey::default()(M1 devnet warn-only) and is wired once viagovernance::set_hook_allowlist_ptr, immutable once set. Perspecs/pre-audit-05-transferhook-whitelist.md.TaskContractdrift (5 absent):task_nonce: [u8; 8](state.rs:171) — explicit struct field (spec line 34 uses it only as seed component).escrow_bump: u8(:190) —TaskEscrowPDA bump cache.bid_book: Option<Pubkey>(:191) — landed3c71455as commit-reveal bidding substrate (Some⇒ task has aBidBook; phase invariant:bid_book.is_some() ⇒ phase ∈ {Settled}for submit_result per audit report cycle-74 invariant 10).assigned_agent: Option<Pubkey>(:192) — winner bidder pubkey set atclose_biddingtime.payload: TaskPayload(:193) — typed payload perspecs/pre-audit-01-typed-task-schema.md; replaces free-form task description with fixed-layout enum + criteria vec + personhood gate (TaskKind/TaskPayload/PersonhoodTier— all absent from spec; see new embedded types below). Spec line 43task_hashfield is retained verbatim (keccak overtask_id || payload.hash()viaderive_task_hashhelper) but its provenance changed from "Poseidon2 of task description (client-provided)" to "keccak256 oftask_id || AnchorSerialize(payload)" post-3c71455; prose drift held as sibling drift #1.TaskStatusaligned. 9 variants 1-to-1 (state.rs:152-163).TaskEscrowaligned. ATA-style PDA token account; seeds honored verbatim.TaskKindenum — absent from spec (5 variants).state.rs:71-96:SwapExact { in_mint, out_mint, amount_in, min_out }/Transfer { mint, to, amount }/DataFetch { url_hash, expected_hash }/Compute { circuit_id, public_inputs_hash }/Generic { capability_bit, args_hash }. Fixed-layout per variant (no nestedVec<u8>) so borsh-deserialize caps size deterministically. Landed3c71455. Spec referencespecs/pre-audit-01-typed-task-schema.md §task_marketalready exists inline atstate.rs:24,67; §State prose doesn't cross-cite it — held as sibling drift #2.TaskPayloadstruct — absent from spec (4 fields + 4 helper methods).state.rs:98-145:kind: TaskKind+capability_bit: u16+criteria: Vec<u8>(capped atMAX_CRITERIA_LEN = 128) +requires_personhood: PersonhoodTier(re-exported fromagent_registry::state::PersonhoodTier— cross-program import).::new/::kind_discriminant(u8 tag 0-4) /::validate(bit bound + criteria len) /::hash(keccak256 overAnchorSerialize(self)bytes). Landed3c71455+b435db7(personhood field added at:104).BidPhaseenum — absent from spec (4 variants).state.rs:196-202:Commit/Reveal/Settled/Cancelled. Landed3c71455as commit-reveal bidding state-machine vocabulary.BidBookPDA — absent from spec (13 fields).state.rs:204-221:task_id: [u8; 32]+commit_start: i64+commit_end: i64+reveal_end: i64+bond_amount: u64+bond_mint: Pubkey+commit_count: u16+reveal_count: u16+winner_agent: Option<Pubkey>+winner_bidder: Option<Pubkey>+winner_amount: u64+phase: BidPhase+bump: u8+escrow_bump: u8. Seeds[SEED_BID_BOOK, task.key().as_ref()]. Commit-reveal window defaults:DEFAULT_COMMIT_WINDOW_SECS = 300/DEFAULT_REVEAL_WINDOW_SECS = 180(50/50 split + min-bid-bond bounds per spec-06 dispute-arbitration commit-reveal template). Landed3c71455.BidPDA — absent from spec (10 fields).state.rs:223-236:task_id: [u8; 32]+agent_did: [u8; 32]+bidder: Pubkey+commit_hash: [u8; 32]+bond_paid: u64+revealed_amount: u64+revealed: bool+refunded: bool+slashed: bool+bump: u8. Seeds[SEED_BID, task.key().as_ref(), bidder.as_ref()]. Landed3c71455.MintAcceptRecordPDA — absent from spec (6 fields).state.rs:33-42:mint: Pubkey+mint_accept_flags: u32(4-bit bitfield overfee_collector::MINT_FLAG_{NO_TRANSFER_FEE, NO_FROZEN_DEFAULT, NO_PERMANENT_DELEGATE, HOOK_OK}) +hook_program: Option<Pubkey>+accepted_at_slot: u64+accepted_at_ts: i64+bump: u8. Seeds[SEED_MINT_ACCEPT, mint.as_ref()]. Landedb435db7as M1 Token-2022 extension-drift snapshot per mint;allow_payment_minthandler builds the flags and refuses on non-governanceTransferFeeauthority,DefaultAccountState=Frozen,PermanentDelegatepresence, or hook-program-off-allowlist. 8 unit tests cover the flag-building decision tree.ReentrancyGuardPDA — absent from spec (5 fields).guard.rs:11-19:active+entered_by+entered_at_slot: u64+reset_proposed_at: i64+bump. Seeds[SEED_GUARD]. Landedc759a7b.AllowedCallersPDA — absent from spec (2 fields).guard.rs:21-27:programs: Vec<Pubkey>cap 8 +bump. Seeds[SEED_ALLOWED_CALLERS]. Landedc759a7b.- Guard-state-vocabulary matrix row (post-cycle): task_market =
2-PDA (ReentrancyGuard + AllowedCallers). Post-cycle §State-sweep arc state: 6-of-6 M1-in-scope reconciliations landed (capabilityN/A+ treasury2-PDA+ dispute2-PDA+ proof_verifier2-PDA+ agent_registry2-PDA+ task_market2-PDA). Universal 2-PDA guard-state cohort now confirmed across all M1-in-scope scaffolds that ship a guard surface. Arc closed. - Absent module-level constants (20):
state.rs:7-31—ALLOWED_MINTS_LEN = 8,MAX_MILESTONES = 8,MAX_PROTOCOL_FEE_BPS = 100,MAX_SOLREP_FEE_BPS = 100,BPS_DENOM = 10_000,CANCEL_GRACE_SECS = 300,EXPIRE_GRACE_SECS = 3_600,MIN_DEADLINE_SECS = 60,MAX_BIDDERS_PER_TASK = 64,DEFAULT_COMMIT_WINDOW_SECS = 300,DEFAULT_REVEAL_WINDOW_SECS = 180,MIN_BID_BOND_BPS = 50,MAX_BID_BOND_BPS = 500,MAX_CRITERIA_LEN = 128,MAX_CAPABILITY_BIT = 127,SEED_BID_BOOK,SEED_BID,SEED_BOND_ESCROW,SEED_MINT_ACCEPT.guard.rs:5-9—SEED_GUARD,SEED_ALLOWED_CALLERS,MAX_ALLOWED_CALLERS = 8,MAX_CPI_STACK_HEIGHT = 3,ADMIN_RESET_TIMELOCK_SECS = 24h. - Absent helper fns (14): 9 at state.rs —
compute_fees(:319-336— u128-widened protocol + solrep fee computation, 4 proptests at:344-386+ zero-amount rejection test per BACKLOG row 76),compute_bond_amount(:238-245— bid-bond bps derivation, 3 unit + 2 proptests),reveal_commit_hash(:247-250— keccak256 overamount || nonce || agent_didcommit-reveal binding),bid_beats(:252-267— price-then-stake-then-pubkey tie-break ordering, 3 unit tests),is_allowed_mint(:269-271),resolve_hook_allowlist(:276-290— F-2026-05 gate active-vs-skip decision withHookAllowlistMismatchreject on wired-but-mismatched),hook_gate_active(:296-306— pure-logic variant for unit tests, 4 tests),compute_task_id(:308-317— keccak256 canonical tupleclient || task_nonce || created_at, M1 Poseidon2 swap flagged inline as no-op field rename),derive_task_hash(:147-150— keccak256 oftask_id || payload.hash()). 5 at guard.rs —try_enter(:29-35),exit(:37-40),reset_guard(:42-47, extractedcd5b594),check_callee_preconditions(:49-67, 4 tests),assert_reset_timelock(:69-80, 1 test). - 4 not-patched sibling drifts surfaced inline with explicit "held for separate cycles" framing: (1) spec line 43
task_hash"Poseidon2 of task description (client-provided)" stale post-3c71455(keccak256 oftask_id || AnchorSerialize(payload)viaderive_task_hash) — multi-section §State-prose-refresh needed (touches §Instructions §create_taskline 98-111 simultaneously, plus §Invariants if it carries a claim on task_hash derivation); (2) spec §State omits F-2026-01 personhood surface entirely —PersonhoodTierre-export fromagent_registry::state+TaskPayload.requires_personhoodfield + commit_bid personhood-gate at M1 — held for §State-prose-refresh same cycle as #3 (both touch §commit_bidsimultaneously); (3) spec §State omits F-2026-05 hook-allowlist surface entirely —MintAcceptRecordPDA +MarketGlobal.hook_allowlistpointer +allow_payment_mint/resolve_hook_allowlist/hook_gate_activerail — held for same §State-prose-refresh as #2; (4) spec §State omits typed payload schema cross-reference entirely —specs/pre-audit-01-typed-task-schema.md §task_marketalready cited inline atstate.rs:24,67but not carried forward into §State prose — editorial-only, held for prose-refresh cycle. - Arc-closure note: This cycle closes the §State-sweep arc at 6-of-6 M1-in-scope programs reconciled. Sibling arcs still open across the 6:
§Events-sweep(6-of-6 landed at711bf4b+ 5 siblings),§Instructions-sweep(6-of-6 landed at03214e2+ 5 siblings), both complete. Future reviewer-handoff substrate cycles would target (a) the 4 sibling drifts flagged above (task_market-specific), (b) the 3+4 siblings flagged in cycles 177-179 (dispute / proof_verifier / agent_registry), or (c) fresh axes not yet swept (e.g. §Invariants / §Security-checks / §CU budget — each would be a new per-program arc).
State machine
Created --(fund_task)--> Funded --(submit_result)--> ProofSubmitted
|
(verify_task: CPI proof_verifier)
v
Verified --(release)--> Released
|
(raise_dispute within window)
v
Disputed --(M2: arbitrate)--> Resolved
Funded --(expire after deadline + grace)--> Expired
Invariant: no transition skips. Every edge is an explicit instruction. Released, Expired, Resolved are terminal.
Instructions
init_global(...) — one-shot, deployer-signed.
create_task(task_nonce, agent_did, payment_mint, payment_amount, task_hash, criteria_root, deadline, milestone_count)
- Signers:
client - Validation:
!global.pausedpayment_mint ∈ allowed_payment_mintspayment_amount > 0deadline > now + 60(minimum 1-minute future)deadline <= now + max_deadline_secsmilestone_count <= 8- CPI-read
AgentRegistry::AgentAccountforagent_did, requirestatus == Active - Compute fees:
protocol_fee = floor(amount * protocol_fee_bps / 10_000),solrep_fee = floor(amount * solrep_fee_bps / 10_000), usingu128intermediate for overflow safety
- State transition: creates
TaskContractwithstatus = Created. Does NOT move funds. LeavesTaskEscrowuninitialized untilfund_task. - Emits:
TaskCreated { task_id, client, agent_did, payment_amount, deadline } - CU target: 120k (§2.1)
fund_task(task_nonce)
- Signers:
client - Validation: task exists,
status == Created,!global.paused - State transition: initializes
TaskEscrowtoken account; Token-2022transfer_checkedfrom client's ATA ofpayment_mint→ escrow forpayment_amount + protocol_fee + solrep_fee. Wait — per §2.4, fees are deducted frompayment_amountat settle, not added; aligning: escrow holdspayment_amounttotal, fees are split-outs from that pool. Decision (M1 default, reviewer may tighten): escrow holds fullpayment_amount; at settle,protocol_feeandsolrep_feeare deducted from the agent's payout and sent tofee_collector/solrep_pool. Client payspayment_amountgross. - Sets
status = Funded,funded_at = now. - Emits:
TaskFunded
Atomic create+fund via Jito bundle
Client-side: both instructions in one versioned tx submitted as a Jito bundle. Program does not enforce atomicity (instruction-order is a client concern); it does validate that fund_task immediately follows create_task semantics by requiring status == Created. If the bundle partial-lands (only create_task), the task sits in Created status. Orphan cleanup:
cancel_unfunded_task(task_nonce)
- Signers:
client - Validation:
status == Created,now >= created_at + 300(5 min grace — prevents MEV-style immediate cancellation during bundle retry) - Closes
TaskContract, reclaims rent to client.
submit_result(task_nonce, result_hash, proof_key)
- Signers:
operatorof the assigned agent (verified by CPI-readingAgentAccountfor matchingagent_did,status == Active) - Validation:
status == Fundednow <= deadlineresult_hash != 0
- State transition: writes
result_hash,proof_key,submitted_at = now,status = ProofSubmitted. - Emits:
ResultSubmitted - CU target: 80k (§2.1)
verify_task(task_nonce, proof_a, proof_b, proof_c)
- Signers: any (permissionless — usually the proof-gen service)
- Validation:
status == ProofSubmitted - CPI:
ProofVerifier::verify_proof(proof_a, proof_b, proof_c, public_inputs)where public inputs are constructed in locked order per spec 06:[task_hash, result_hash, deadline, submitted_at, criteria_root]. - State transition: on Ok →
status = Verified,verified = true,dispute_window_end = deadline + dispute_window_secs. On Err → status unchanged; event emitted for debugging. - Emits:
TaskVerifiedorVerificationFailed - CU target: ~400k with the bn254 pairing; runs in its own tx with explicit compute-budget instruction. Settle is a separate call.
release(task_nonce)
- Signers: any (permissionless crank)
- Validation:
status == Verifiednow >= dispute_window_end(client had their 24h window to dispute)!global.paused
- State transition (state-before-CPI per §5.1): set
status = Released. Then:agent_payout = payment_amount - protocol_fee - solrep_feeviachecked_sub- Token-2022
transfer_checkedescrow → agent operator ATA foragent_payout - Token-2022
transfer_checkedescrow →fee_collectorATA forprotocol_fee - Token-2022
transfer_checkedescrow →solrep_poolATA forsolrep_fee - Close escrow account to zero (sanity check: residual must be 0)
- CPI
AgentRegistry::record_job_outcomewithsuccess=true, disputed=falseand quality metrics derived fromcriteria_rootcoverage (M1 default: all-true since verification passed)
- Emits:
TaskReleased { agent_payout, protocol_fee, solrep_fee }
expire(task_nonce)
- Signers: any (permissionless crank)
- Validation:
status ∈ {Funded, ProofSubmitted}ANDnow > deadline + 3600(1h grace so a verify attempt can complete around the boundary) - State transition:
status = Expired, refund fullpayment_amountto client, close escrow. - CPI
AgentRegistry::record_job_outcomewithsuccess=false, disputed=false— counts as a missed job. - Emits:
TaskExpired
raise_dispute(task_nonce)
- Signers:
client - Validation:
status == Verified,now < dispute_window_end - State transition:
status = Disputed. M1: freezes the escrow; DisputeArbitration wiring is M2. The field is reserved so M2 can addarbitrate(...)without a breaking state-machine change. - Emits:
DisputeRaised
set_allowed_mint, set_fees (with reasonable bps caps), set_paused, authority two-step.
Scaffold-vs-spec §Instructions deltas
Cycle-167 reconciliation of spec enumeration vs actual programs/task_market/src/instructions/ (29 pub fn *_handler across 23 ix modules). Largest delta across the 5 M1-in-scope programs — spec enumerates ~11 explicit ix + "authority two-step" mention, scaffold ships 23 ix modules split across 8 concerns.
Absent blocks (4 concerns, 15 ixs missing from spec enumeration):
bidding block — 8 ixs across 8 modules (
open_bidding.rs:54/commit_bid.rs:112/reveal_bid.rs:36/close_bidding.rs:39/cancel_bidding.rs:44/claim_bond.rs:71/close_bid.rs:34/close_bid_book.rs:44). Commit-reveal scheme per cycle-74 audit report (reports/task-market-audit.md) —open_bidding(commit_secs, reveal_secs, bond_bps)opens the bid book on aFundedtask;commit_bid(commit_hash, agent_did)keccak-hashed bid commitment gated by AgentRegistry active-status + capability-mask mirror (F-2026-08);reveal_bid(amount, nonce)decommit + low-bid tracking;close_biddingtallies winner + routes escrow delta refund to client;cancel_biddingpre-commit-window abort path;claim_bondpost-terminal bond-refund / slash-on-no-reveal (firesBidSlashedon slash branch only per §Events bidding row);close_bid+close_bid_bookpost-terminal PDA-reclaim. Pre-edit §Events already documents the 6 bidding events (BidBookOpened/BidCommitted/BidRevealed×2 dual-emit /BidBookClosed/BidSlashed) on line 186; §Instructions is where the ix surface needs to land. Cross-cite: cycle-74 audit report §Instructions-by-concern bucket "Bidding" + §Events dual-emit provenance for the 2BidRevealedbranches. Spec §Invariants (lines 219–228) does not reference the bidding state machine — the bid-book lifecycle (bid_book.is_some() ⇒ phase ∈ {Settled}per cycle-74 invariant 10) is scaffold-only; invariants-sweep cycle queued.guard-admin block — 4 ixs in
guard.rs:42/ :75 / :105 / :125.init_guard(initial_callers: Vec<Pubkey>)+set_allowed_callers(programs: Vec<Pubkey>)+propose_guard_reset()+admin_reset_guard(). Same 24hADMIN_RESET_TIMELOCK_SECSpattern treasury_standard (cycle 163) + dispute_arbitration (cycle 166) carry. Guard-vocabulary cohort parity: task_market matches the treasury_standard / dispute_arbitration convention — guard-admin ixs live without event emission (noGuardInitialized/GuardAdminReset/AllowedCallersUpdatedevents, per §Events line 179). Indexer observes guard-admin state viaReentrancyGuard+AllowedCallersaccount reads only.disputed_timeout_refund(ctx)—disputed_timeout_refund.rs:56, permissionless cranker-signed. Secondary expiry surface forstatus == Disputedtasks that crossdispute_window_end + DISPUTE_TIMEOUT_SECSwithout DisputeArbitration resolution (M1 inert-surface — DisputeArbitrationexecute_dispute_verdict/force_releaseland M2 per cycle-74 audit report). Refunds fullpayment_amountto client, closes escrow, emitsTaskExpired(dual-emit withexpire.rs:166per §Events line 183). Absence from spec enumeration is the load-bearing omission — reviewer cross-reading §raise_dispute (line 172 "freezes the escrow; DisputeArbitration wiring is M2") without findingdisputed_timeout_refundwould assume disputed tasks are indefinitely frozen at M1; the scaffold actually provides the escrow-exit via the 1h grace + DISPUTE_TIMEOUT_SECS timeout path.allow_payment_mint(slot: u8)—allow_payment_mint.rs:47, authority-signed. Distinct fromset_allowed_mint(spec line 175).set_allowed_mintwritesglobal.allowed_payment_mints[slot]directly as a governance override.allow_payment_mintruns the full Token-2022 mint-extension sanity-check battery viainspect_mint_extensions(no TransferFee unless authority-held + nodefault_state_frozen+ no PermanentDelegate + TransferHook program-id ∈ HookAllowlist) + allocates a per-mintmint_acceptPDA (MintAcceptrecord withmint_accept_flags: u32bitmask +hook_program: Option<Pubkey>+accepted_at_slot+accepted_at_ts) + emitsMintAccepted(§Events mint-allowlist bucket line 187). Two paths by design:set_allowed_mintis the pre-launch bootstrap / emergency-override surface;allow_payment_mintis the production per-mint audit surface. Scaffold pattern mirrors treasury_standard'sallowed_mintslane (cycle 163). Spec §fund_task mint-allowlist validation (line 102 "payment_mint ∈ allowed_payment_mints") hides the distinction — the allowlist surface is two-ix, not one.set_hook_allowlist_ptr(hook_allowlist: Pubkey)—governance.rs:65, authority-signed. Pointsglobal.hook_allowlistat aHookAllowlistPDA carrying the set of Token-2022 TransferHook program-ids permitted to be attached to allowed payment mints. Consumed byallow_payment_mint(above) +commit_bid(F-2026-08 hook-allowlist check percommit_bid.rs:106). Absent from spec §init_global params + spec §governance setter enumeration. Cross-cite:reports/task-market-audit.md§Governance-surface bucket + §EventsMintAccepted.hook_programshape.
Arg-shape drift (1 class, 1 ix):
create_task§line 98 spec argtask_hash→ scaffoldpayload: TaskPayload.create_task.rs:54signature is(task_nonce, agent_did, payment_mint, payment_amount, payload: TaskPayload, criteria_root, deadline, milestone_count).TaskPayloadis a discriminated-union per cycle-74 audit report §Payload bucket (kind_discriminant: u8+capability_bit: u16+ body fields). The on-chainTaskContract.task_hashfield (spec §State line 43) is computed from the payload at create-time viaPoseidon2(TaskPayload.canonical_bytes), not passed as an arg. Spec arg-nametask_hashis aspirational from pre-payload spec iteration. Second-emit behavior:create_taskfiresTaskCreated+TaskPayloadStored(§Events line 188 line-by-line cross-cite), which is load-bearing for indexers — a reviewer cross-reading spec line 110 "Emits: TaskCreated" only would miss the discriminated-union storage event. §Events already reconciled cycle 164; §Instructions-side arg-shape reconciliation lands here.
Fictional line tail (1):
- Spec line 175 tail "authority two-step." Unlike dispute_arbitration cycle 166 (where the tail had zero backing), task_market's
authority two-stepis real code —authority.rs:18(transfer_authority_handler(new_authority: Pubkey)) +authority.rs:33(accept_authority_handler()). Pending-authority two-step lives inMarketGlobal.pending_authority: Option<Pubkey>. The drift here is enumeration, not existence: the spec mentions the pattern by name but does not enumerate the two ix headings. Reviewer cross-reading the spec against the IDL will find both ixs, no surprise — but the handler-file cross-reference is worth the explicit callout for an audit-fix-manifest trail.
Handler-file density cross-check: 29 pub fn *_handler + pub fn handler signatures across 23 ix modules (vs dispute_arbitration's 23 handlers across 7 modules cycle 166; largest-surface M1 program by a 3× margin). Post-edit spec enumeration covers 11 explicit ix + 8 bidding + 4 guard-admin + disputed_timeout_refund + allow_payment_mint + set_hook_allowlist_ptr + transfer_authority + accept_authority = 27. Two-ix gap vs 29-handler count closes on the observation that init_global's handler is init_global::handler (not a distinct init_global_handler-named fn) and the spec's set_allowed_mint / set_fees / set_paused share the GovernanceUpdate accounts struct (3 handlers, 3 spec names — already enumerated in line 175).
Events
programs/task_market/src/events.rs declares 21 #[event] structs; 20 fire from 31 emit! sites across 15 instruction modules, and ReentrancyRejected is a struct-only scaffold-parity placeholder (same convention as fee_collector / nxs_staking / dispute_arbitration / governance guard-runtime variants) — the guard::check_callee_preconditions reject path returns TaskMarketError::ReentrancyDetected by error, no event. Guard-vocabulary coverage is 2-of-5 live (GuardEntered + ReentrancyRejected struct-only); the 3 guard-admin events (GuardInitialized, GuardAdminReset, AllowedCallersUpdated) are absent from both struct and emit — the instructions/guard.rs guard-admin ixs (init / set_allowed_callers / propose_guard_reset / admin_reset_guard) land without event emission, and post-emit state is observable only via ReentrancyGuard + AllowedCallers account reads. Agent_registry remains the only in-scope program with the full 5-of-5 live guard vocabulary (cycle 161).
Emit inventory by concern (8 buckets: global / task lifecycle / settlement / dispute / bidding / mint allowlist / payload / guard-runtime):
- global —
GlobalInitialized(init_global.rs:69);GlobalParamsUpdated×4 (governance.rs:28 / :50 / :79 — one site per setter + allow_payment_mint.rs:112 reusing the event on the mint-allowlist-add path);PausedSet(governance.rs:58). - task lifecycle —
TaskCreated(create_task.rs:136);TaskFunded(fund_task.rs:100);TaskCancelled(cancel_unfunded_task.rs:33);TaskExpired×2 (expire.rs:166 + disputed_timeout_refund.rs:122 — dual-emit flags two distinct expiry surfaces, client-initiated 1h-grace expiry vs dispute-timeout forced refund). - settlement —
ResultSubmitted(submit_result.rs:87);TaskVerified(verify_task.rs:140, carriesdispute_window_end);VerificationFailed(verify_task.rs:124 — status unchanged per spec-07 verify_task semantics);TaskReleased(release.rs:215, carriesagent_payout+protocol_fee+solrep_feesplit triple). - dispute —
DisputeRaised(raise_dispute.rs:28). Terminal hand-off peraudit-package-m1.md§6.5 —raise_disputeis a named-stub family at M1 pending M2 DisputeArbitrationexecute_dispute_verdict/force_releaselanding. - bidding (commit-reveal scheme per cycle-74 audit report) —
BidBookOpened(open_bidding.rs:113);BidCommitted(commit_bid.rs:209);BidRevealed×2 (reveal_bid.rs:69 + :89 — dual-emit matches the two reveal branches, bidder-signed decommit vs operator-delegated reveal);BidBookClosed(close_bidding.rs:181, carrieswinner_agent: Option<Pubkey>+winner_amount+reveal_count: u16);BidSlashed(claim_bond.rs:157 — fires only on the no-reveal-slash branch). - mint allowlist —
MintAccepted(allow_payment_mint.rs:105, carriesaccept_flags: u32bitmask +hook_program: Option<Pubkey>for TransferHook-mint detection per spec-07 hook-allowlist). - payload —
TaskPayloadStored(create_task.rs:144 — second emit fromcreate_taskafterTaskCreated, carrieskind_discriminant: u8+capability_bit: u16for the discriminated-union TaskPayload surface). - guard-runtime —
GuardEntered×7 (expire / submit_result / close_bidding / verify_task / disputed_timeout_refund / release / fund_task — every ix that crossesguard::check_callee_preconditions;create_taskomits guard-entry per its lack of CPI-out surface).
Field-carrying shape against actual struct bodies:
task_id: [u8; 32]on 15 of 21 — absent fromGlobalInitialized,GlobalParamsUpdated,PausedSet(program-scoped),MintAccepted(keyed onmint),GuardEntered+ReentrancyRejected(guard-runtime, keyed on(program, caller, slot)).timestamp: i64on 12 of 21 — absent from all 5 bidding events,TaskPayloadStored(keyed on task_id + ix-context),GuardEntered+ReentrancyRejected(substituteslot: u64per the cycles 157–163 guard-runtime convention).MintAccepteduniquely carries bothslotandtimestamp.slot: u64on 3 of 21 —GuardEntered,ReentrancyRejected,MintAccepted.agent_did: [u8; 32]on 2 of 21 —TaskCreated(bind at create-time from the matched AgentAccount) andTaskReleased(settle-path indexer join against AgentRegistry). Mid-lifecycle events key ontask_idalone; indexer re-derivesagent_didviaTaskContract.agent_didpost-read.- Settlement-fee triple on
TaskReleased— fullagent_payout+protocol_fee+solrep_feesplit per spec §fund_task escrow-fee-deduction decision. Consistent with thecompute_feesproperty-test surface (cycle 76: no silent zero-fee bypass on bps arithmetic).
Pre-edit spec listed 10 event names with the claim "all events carry task_id and timestamp" — 11 additional events ship in the IDL (5 bidding + GlobalInitialized + TaskCancelled + TaskPayloadStored + MintAccepted + GuardEntered + ReentrancyRejected), task_id is absent from 6 of 21, and timestamp is absent from 9 of 21. Indexer-side reconstruction of per-task history uses the (task_id, slot, ix_index) composite from program_events rather than (task_id, timestamp) alone.
Errors
Unauthorized, Paused, MintNotAllowed, InvalidAmount, InvalidDeadline, DeadlineTooFar, AgentNotActive, WrongStatus, DeadlinePassed, DisputeWindowClosed, DisputeWindowOpen, NotExpired, EscrowMismatch, ArithmeticOverflow, ProofInvalid, CallerNotOperator, TaskNotFound, FeeBoundExceeded.
CU budget (§2.1 targets; M1 default, reviewer may tighten)
| Instruction | Target |
|---|---|
create_task |
120k |
fund_task |
60k |
submit_result |
80k |
verify_task |
400k (dominated by ProofVerifier CPI) |
release |
120k |
expire |
80k |
raise_dispute |
20k |
verify_task + release run as separate txs in M1. A compressed verify_and_release (§6.1 territory) waits for SIMD-0334's lower pairing cost.
Invariants
- Escrow balance ==
payment_amountwhilestatus ∈ {Funded, ProofSubmitted, Verified, Disputed}; 0 afterReleased | Expired | Resolved. protocol_fee + solrep_fee < payment_amountalways (enforced at create via bps cap).- Every state transition emits exactly one event.
status == Verified⇒ProofVerifier::verify_proofreturnedOkon the stored(task_hash, result_hash, deadline, submitted_at, criteria_root).releasecannot execute beforedispute_window_end.expirecannot execute whilestatus ∈ {Verified, Released, Expired, Disputed, Resolved}.- No instruction can move escrow funds without first setting terminal status on
TaskContract. record_job_outcomeis called exactly once per task lifetime — onreleaseorexpire(or later onresolvein M2).agent_didon task matches theagent_didof the signer'sAgentAccountatsubmit_result.
Security checks (backend §5.1)
- Account Validation: Anchor seeds + bumps on
MarketGlobal,TaskContract,TaskEscrow. Owner = program. Discriminator enforced. CPIs to AgentRegistry, TreasuryStandard, ProofVerifier use stored program IDs inMarketGlobal— hard equality check, not passed by caller. - Re-entrancy: critical.
releasesetsstatus = Releasedand zeroes derived amounts before any Token-2022 transfer or AgentRegistry CPI.expirelikewise. No CPI target can re-enter TaskMarket with the same task in a pre-transfer state because the status gate rejects. - Integer Safety: fee math via
u128intermediates,checked_subfor payout. Deadline arithmetic checked against i64 bounds (created_at + max_deadline_secschecked_add). - Authorization: client-signed paths for create/fund/cancel/dispute; agent-operator signed for submit; permissionless for verify/release/expire (all gated by status + time).
- Slashing Safety: N/A here; slashing lives in AgentRegistry.
- Oracle Safety: no direct oracle use in M1 — TreasuryStandard's Jupiter path is not invoked from TaskMarket in M1.
- Upgrade Safety: Squads 4-of-7, 14-day timelock per §2.6 (critical-path program).
- Token Safety: Token-2022
transfer_checkedonly. Payment-mint whitelist excludes TransferHook/ConfidentialTransfer extensions. Fee destinations pre-set at global init — never caller-supplied. - Pause: blocks
create_task,fund_task,release. Leavesexpireandraise_disputeopen so funds cannot be trapped indefinitely by a paused program. - Jito bundle assumption: program does NOT rely on atomicity — if only
create_tasklands,cancel_unfunded_taskafter 5-min grace lets the client recover. Documented assumption per §5.1 "Jito bundle assumptions".
Open questions for reviewer
- Milestone handling: §2.4 mentions
milestone_count. M1 ships the field but the release path is single-shot. Multi-milestone release is deferred to M2 unless reviewer requires earlier. - Quality metrics fed to
record_job_outcome: M1 usesall-goodon verify success; reviewer may want the agent to include self-reported quality inputs that the circuit also validates. expiregrace of 1h — reasonable default; tune after benchmarking proof-gen latency.- Whether
verify_taskshould also advancedispute_window_endreset ifsubmitted_at > deadline(currentlyverify_taskaccepts any time; the circuit itself enforcessubmitted_at <= deadline, so proof will fail otherwise).
Done-checklist
- Full state machine implemented; illegal transitions rejected
-
create_task+fund_taskas separate instructions; Jito bundle wrapper demonstrated in integration test - Partial bundle (create only) recoverable via
cancel_unfunded_taskafter grace -
submit_resultrejects non-operator signer, wrong status, past deadline -
verify_taskCPIs ProofVerifier with public inputs in the locked order (spec 06); verified against spec 05 test vector -
releaserefuses to execute beforedispute_window_end; refuses when paused -
expirerefunds client and callsrecord_job_outcomewithsuccess=false -
raise_disputefreezes status; M2 integration hook documented - Fee math tested with edge amounts: 1 unit, max u64 / 10_000, ensures no overflow
- Re-entrancy audit: every CPI site annotated with the pre-CPI state write
- Golden-path integration test (end-to-end, localnet): register agent → fund treasury → create+fund task → submit result + proof → verify → release → agent balance increases, fees collected, reputation recorded
- CU measurements per instruction in
reports/07-task-market-anchor.md - IDL at
target/idl/task_market.json - Security auditor pass (§5.1); findings closed
- Reviewer gate green; spec ready for OtterSec queue