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 asdiamond()) and applies anonlyDiamondmodifier to every external callback, revertingBaseTMPHook__UnauthorizedCaller(caller)for anyone else. Without this, anyone can call youronCompletedirectly 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 revertsBaseTMPHook__ZeroDiamond()for a zero address. - It answers
supportsInterfaceforITMPHookandIERC165. - It defaults every
_check*toreturn trueand 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:
| Hook | What it shows |
|---|---|
AllowlistedWorkerHook | Gating. 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. |
CompletionReceiptHook | Observing 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. |
RequiredTagPolicyHook | Creation-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:
checkFundstores a per-task state struct keyed bytaskId. Configuration lives on the hook contract, andhookDatais ignored: a valid pattern when a hook needs no per-task settings.checkClaim/checkSelectWorkerlock the rate and reserve tokens from a vault when a worker is committed, so the payout does not move with price afterwards.checkSubmitchecks that the submitting worker is the one recorded at reservation.checkCompletepays the reserved amount for Claim, Pitch and Auction, and computes each winner's share fromverdict.awardsfor Bounty. Handled shortfalls degrade gracefully so they do not block USDC settlement, but an unhandled revert in acheck*would still reject settlement.onComplete/onForfeit/onCancel/onExpireall 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 = truebefore 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 buildandforge 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 emitsHookCallFailed. - Assuming
checkFundsees 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.