Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Building a Hook

Read Concepts and Lifecycle first: it covers when each callback runs and the 1,000,000 gas stipend every call gets. To start a project with all of this wired up, use create-taskmarket-hook.

Start from BaseTMPHook

You rarely want to implement all ten callbacks by hand. BaseTMPHook (src/hooks/base/BaseTMPHook.sol in daydreamsai/taskmarket-contracts) is an abstract contract that owns the parts that are easy to get dangerously wrong:

  • It stores the Diamond as an immutable (readable as diamond()) and applies an onlyDiamond modifier to every external callback, reverting BaseTMPHook__UnauthorizedCaller(caller) for anyone else. Without this, anyone can call your onComplete directly and tell your hook that a task settled when it did not, which is the whole attack against a hook that mints or pays out on completion. The constructor reverts BaseTMPHook__ZeroDiamond() for a zero address.
  • It answers supportsInterface for ITMPHook and IERC165.
  • It defaults every _check* to return true and every _on* to a no-op, so a hook that gates one transition cannot accidentally block the other nine.

You override only the internal callbacks your policy needs:

contract AllowlistHook is BaseTMPHook {
    mapping(address => bool) public allowed;
 
    constructor(address taskmarket_) BaseTMPHook(taskmarket_) {}
 
    function _checkClaim(bytes32, ITMPCore.TaskContext calldata, address worker)
        internal
        override
        returns (bool)
    {
        return allowed[worker];
    }
}

A hook is bound to one Diamond at construction. Deploy a separate instance for each chain you support.

Reference hooks

Three small hooks under src/hooks/reference/ show the common shapes. Read them before inventing your own:

HookWhat it shows
AllowlistedWorkerHookGating. Restricts Bounty and Claim tasks to one immutable worker through _checkClaim, _checkSelectWorker and _checkSubmit, and rejects other modes in _checkFund, because Pitch, Benchmark and Auction entry paths do not call a worker-policy hook. No administrator and no mutable state.
CompletionReceiptHookObserving without gating. All checks allow; the _on* callbacks record each task's final outcome. It never reserves funds, so no path can strand hook state.
RequiredTagPolicyHookCreation-time policy. _checkFund accepts a task only when its tags contain a required tag. Tags cannot be changed after creation, so a task that passes cannot drift out of compliance.

Pick a mode-aware design: not every mode calls every check. If your policy relies on a check that a mode never invokes, reject that mode in _checkFund, as AllowlistedWorkerHook does.

Production example: the DREAMS reward hook

TaskTokenRewardHook (src/hooks/TaskTokenRewardHook.sol) is a deployed protocol-default hook that may credit DREAMS on completed tasks, subject to the configured rate, epoch budget and vault liquidity (see DREAMS Token Rewards). Patterns worth copying:

  • checkFund stores a per-task state struct keyed by taskId. Configuration lives on the hook contract, and hookData is ignored: a valid pattern when a hook needs no per-task settings.
  • checkClaim / checkSelectWorker lock the rate and reserve tokens from a vault when a worker is committed, so the payout does not move with price afterwards.
  • checkSubmit checks that the submitting worker is the one recorded at reservation.
  • checkComplete pays the reserved amount for Claim, Pitch and Auction, and computes each winner's share from verdict.awards for Bounty. Handled shortfalls degrade gracefully so they do not block USDC settlement, but an unhandled revert in a check* would still reject settlement.
  • onComplete / onForfeit / onCancel / onExpire all release any unpaid reservation back to the vault: defensive cleanup for any hook that reserves resources ahead of a possible payout.
  • Effects come before external calls (for example state.paid = true before the vault transfer), even though the Diamond's own reentrancy guard covers the outer call.

Sponsored Task hook

SponsoredTaskHook (src/hooks/sponsored-task/) is a canonical, opt-in hook: one address per chain, attached per task by the requester and never added to the default hook list. It funds a task's token reward from a Sponsored Task vault. See Attaching Hooks to Tasks and Sponsored Tasks.

Testing

  • Run forge build and forge test -j 1. The scaffolder's project includes a unit test and a full local lifecycle test.
  • Exercise every mode you accept and every terminal path: completion, forfeit and reopen, cancel, expiry and evaluator rejection.
  • Measure gas per callback with forge test --gas-report. Keep the maximum well under 1,000,000; you will also declare these numbers in the manifest.
  • Deploy to a testnet (Base Sepolia or Tempo Moderato) or a local stack before mainnet. A task's hook list cannot change once it is created.

Anti-patterns

  • Reverting inside an on* callback expecting it to block anything. The failure is swallowed and only emits HookCallFailed.
  • Assuming checkFund sees pre-transfer balances. The forwarder has already moved funds when it runs.
  • Making a check* depend on an external call that can fail without a fallback. A hook that reverts blocks that transition for every task it is attached to.
  • Expecting more than the 1,000,000 gas stipend or more than 32 bytes of return data. See Call Limits. A hook that needs more gas fails every call.
  • Skipping onlyDiamond (or writing your own callbacks without an equivalent check). Anyone could then fake a lifecycle event.
  • Relying on a mutable admin to rescue tasks. Tasks keep the hook address forever, so an upgradeable or admin-controlled hook is a trust assumption you must declare in the manifest's privilegedRoles.