{"name":"skill:oracle-consumer","files":[{"path":".imd/reads/skills/oracle-consumer.md","digest":"0a2a124615d0466728b34d3a8758189da430ed9b33d07480e72659e236274503","content":"# IdentityMD oracle consumers\n\nA contract pays the `Intake` for an `oracle.request`, a panel of agents answers, and the oracle's\nsigner signs the result as EIP-712 typed data in **the consumer's own domain**. The plane's writer\nthen calls the contract back with the attestation and the signature. The contract verifies the\nsignature and acts on the answer.\n\nEverything here is fixed by contracts that are already live. A brief that restates part of it\n(a struct, a type string, an address) may be truncated or wrong. **Where the brief and this\nreference disagree on the protocol, this reference wins**, and the summary says what differed.\n\nThe reference delivered beside this skill, `REFERENCE.md`, holds the canonical Solidity copied\nfrom the protocol's own contracts: the `OracleAttestation` library and `OracleAttestationConsumer`,\nthe two worked examples, and a conformance test. Copy them; never retype them.\n\n## Live deployments (2026-10-10)\n\n| | Ethereum (1) | Robinhood Chain (4663) |\n| --- | --- | --- |\n| `Intake` (version 2) | `0xa43e6f75ee006411f79ac1c84120606c2330de82` | `0xa43e6f75ee006411f79ac1c84120606c2330de82` |\n| `Intake` (version 1, still served) | `0x1397434cd35e8a9c8ac312a61d3a285eb31dea56` | `0x1397434cd35e8a9c8ac312a61d3a285eb31dea56` |\n| `IntakeDelivery` | `0xce0e6a670aa75e161d02aca3c7f00b94ca428ccb` | `0xce0e6a670aa75e161d02aca3c7f00b94ca428ccb` |\n| IMD (the asset paid in) | `0xd34a99bc0f67ae1bbd63c660e6d0b0dd03e263b7` | `0x5f7bb59365ce557c26dbcaa4ee9d39a4b95b7127` |\n| Oracle signer | `0x5598aa9146215bc13eb26f2c692ad1461fd32982` | `0x5598aa9146215bc13eb26f2c692ad1461fd32982` |\n\n- Action id: `bytes32(\"oracle.request@oracle-1\")` =\n  `0x6f7261636c652e72657175657374406f7261636c652d31000000000000000000`, the UTF-8 bytes\n  right-padded with zeros, **not** a keccak hash. Price 0.5 IMD (`500000000000000000`) on both\n  chains; read it with `Intake.priceOf(action, imd)`.\n- This skill is about `oracle.request`. Ethereum and Robinhood Chain also sell `job.open`,\n  `job.continue`, `launch.open` and `workflow.open` at the same price: a contract that opens those\n  reads the `intake-client` skill. Sepolia sells `oracle.request` only. The native coin is not sold: `request` with asset zero\n  reverts `ActionNotSold`.\n- Use version 2 for anything new: it has everything version 1 has, plus `requestWithFailure`.\n  Version 1 still takes and completes requests, for contracts already pointing at it.\n- Sepolia (11155111) has the same `Intake` addresses and `IntakeDelivery`, selling in imdTEST\n  `0x56bcf2a5f47710649a4091219ef0b56e2d1b58bf` (only its owner and named minters mint it; ask the\n  team), and only for an answer signed for a Sepolia contract. Base (8453) has no `Intake` or `IntakeDelivery` yet. Unit tests use a mock\n  either way (see Tests).\n- Every one of these is **constructor input and owner-settable**, never a constant: the intake,\n  the delivery contract, the action id, the asset and price (set together), the signer. A new\n  action version (`oracle-2`) or a moved price must not need a redeploy.\n\n## The intake, as a consumer calls it\n\n```solidity\ninterface IIntake {\n    struct Callback { address target; bytes4 selector; }\n    function priceOf(bytes32 action, address asset) external view returns (uint256);\n    function request(bytes32 action, bytes calldata body, Callback calldata callback, address asset, uint256 amount)\n        external payable returns (bytes32 requestId);\n}\n```\n\nAsk in one call: `forceApprove(intake, price)` on the IMD the contract holds or just pulled, then\n`request(...)`. The intake pulls exactly `amount` and forwards it; nothing is escrowed or refunded.\nKeep the returned `requestId`: it is the intake's id, and it is the first argument of the callback.\n\n## The callback\n\nThe writer calls `target` with `selector ++ abi.encode(bytes32 requestId, Attestation a, bytes signature)`.\nThe function's name is yours; its parameter types are not:\n\n```solidity\nfunction onOracleResult(bytes32 requestId, OracleAttestation.Attestation calldata a, bytes calldata signature) external;\n```\n\n- `Attestation` is the protocol's struct, exactly: 15 fields, in this order, with these names:\n  `requestId, chainId, questionHash, answerType, answer, figure, fromBlock, toBlock, blockHash,\n  panelJobId, panelSize, quorum, agreed, issuedAt, expiresAt`. A struct that drops `figure` or\n  renames `agreed` has a different selector and a different type hash. The callback then cannot\n  decode, no signature verifies, and every request is paid for and never answered.\n- Inherit `OracleAttestationConsumer` and call `_verifyAttestation(a, signature)`. It checks the\n  window and the signature over `attestationDigest(a)`, which is the consumer's domain:\n  name `\"IdentityMD Oracle\"`, version `\"2\"`, `block.chainid`, and `verifyingContract` =\n  **this contract**. The Intake is never the verifying contract. The plane sets the domain to the\n  callback target (or the `deliver` target), unless the body names its own `consumer`.\n- Check `msg.sender == intake` (same chain) or `msg.sender == intakeDelivery` (another chain).\n  Only a request this contract made and has not settled may be answered.\n- `a.requestId` is the oracle request's UUID; the callback's first argument is the intake's id.\n  They differ on purpose. Consume one of them, once.\n- Read the answer with the typed decoders (`decodeBool`, `decodeUint256`, …). `answerType` codes:\n  bool 0, address 1, bytes32 2, uint256 3, address[] 4, bytes32[] 5.\n- `panelSize`, `quorum` and `agreed` are signed. Require `agreed >= quorum` and that the panel and\n  quorum are at least what the contract asked for.\n- The callback gets a **200 000 gas** stipend inside a try. Verify and store; act in a later call\n  of the user's own (an `execute` that reads the stored approval). A callback that reverts or runs\n  out of gas is recorded as not delivered and is not retried. A \"no\" answer is a result to store,\n  never a revert.\n- `questionHash` covers the question, the resolved block window and the definitions, so a\n  contract cannot compute it in advance when the window is relative (`{\"hours\": N}`). With an\n  absolute window (`{\"fromBlock\",\"toBlock\"}`, already confirmed when asked) it can:\n  `OracleQuestion.hash(q)` on chain (`REFERENCE.md`), `oracleQuestionHash(...)` in the protocol\n  package. Without that check the pairing of an answer to the intake request is the writer's; with\n  it, the signature carries it (see [Binding an answer to its request](#binding-an-answer-to-its-request)).\n\n## What a callback proves\n\nThe signature proves the oracle answered *a* question for *this contract*. It does not prove this\ncontract asked it: anyone can pay 0.5 IMD, name your contract as the callback, the `deliver`\ntarget or the body's `consumer`, and get their own question answered and signed for you. A\ncontract that moves value on an answer checks, in this order:\n\n1. **Its own question.** On the asking chain, only a request id it created and has not settled\n   (the example's `pending`). A receiver on another chain cannot see who asked, so it pins the\n   question: the owner stores the `questionHash` in advance and the receiver refuses any other,\n   and takes each pinned question once (the receiver example's `expectQuestion`). A question with\n   a relative window cannot be pinned, so a receiver uses absolute windows. On the asking chain\n   too, a contract that asks a different question per user or per request binds each answer to\n   its request by the question's hash (next section), so an answer paired with the wrong request\n   is refused rather than acted on.\n2. **The panel.** `panelSize`, `quorum` and `agreed` are signed. Require `agreed >= quorum`, and a\n   panel and agreement at least as large as the value at stake warrants (the examples'\n   `setPanelMinimums`). A small panel is cheap to fill with seats one holder controls.\n3. **Freshness.** `_verifyAttestation` refuses an expired attestation; check `toBlock` or\n   `issuedAt` against how old an answer your use can tolerate.\n4. **Failures are not signals.** The failure function is unsigned and anyone can cause one at\n   your contract (ask with a bad body and name it). Clear only a request of your own; never move\n   value on a failure.\n5. **Liveness.** An answer comes minutes later, is attempted once, and can be withheld or lost. A\n   design that loses money when an answer is late or missing (a liquidation, a margin call) does\n   not belong on this oracle today.\n\nThe truth of an answer is the panel's, and its signature is one hot key's. Treat it as a strong\nsignal for moderate value, not as a price feed for liquidations.\n\n## Binding an answer to its request\n\nA contract that asks a different question per request (one per user, per review, per claim)\nwrites the request's identifiers into the question and keeps the question's hash beside the\nrequest. The oracle signs that hash with the answer, so the callback can check it: an answer to\nanother request's question, delivered by mistake or on purpose, is refused, and so is an answer\nto a question someone else paid for. `OracleBoundConsumer` in `REFERENCE.md` is the worked example.\n\n1. **Build the question in the contract** with `OracleQuestion` (`REFERENCE.md`), naming what the\n   request is about: an id, a hash, an address. Fill one `Question` and take both from it:\n   `OracleQuestion.hash(q)` for the pin and `OracleQuestion.body(q, panelSize, quorum,\n   validForSeconds)` for the body, so the body and the hash come from the same values. A body that\n   needs more members builds them on the same values, with `OracleQuestion.jsonString` for text.\n2. **Use an absolute window on the question's chain** whose last block is at least five deep when\n   the plane reads the ask: close it a few blocks behind the head (the example uses eight). On an\n   Arbitrum chain, which Robinhood Chain is, `block.number` is an **Ethereum** block number; read\n   the chain's own from `ArbSys(address(100)).arbBlockNumber()`, as the example's `head()` does.\n3. **Store the hash when asking and compare it first in the callback**:\n   `a.questionHash == pending[requestId].questionHash`, then the panel, then `_verifyAttestation`.\n   Hashing costs tens of thousands of gas when asking; the callback compares one word inside its\n   200 000. Consume `a.requestId` too: two asks of the same question over the same window share a\n   hash, and one attestation settles one of them.\n4. **Write the text as the plane will read it**: ASCII only; addresses and hashes in lowercase hex\n   (`Strings.toHexString` writes lowercase, and the intake lowercases a value that is exactly an\n   address or a 32-byte hex string, so a checksummed one would never match); definitions sorted by\n   key; no `definitions` member at all rather than `{}`. `OracleQuestion` reverts on anything else\n   rather than return a hash the oracle will never sign.\n5. **Check a real body before deploying**: the body for one sample request, at\n   `POST /requests/check` (see [The body](#the-body)). A question the wording screen refuses is\n   refused on every ask.\n\n## When no answer comes\n\nA refused body (status 1: bad JSON, unknown key, an ambiguous question) or a panel that did not\nagree (status 2) spends the price and never reaches the callback. Do not promise a refund the\ncontract cannot pay.\n\nVersion 2 of the `Intake` can say so. `requestWithFailure(action, body, callback, onFailure, asset, amount)`\nnames a second function on the callback target, which `complete` calls on status 1 or 2 under\nthe same 200 000 gas:\n\n```solidity\nfunction onOracleFailure(bytes32 requestId, uint8 status, bytes32 reason, uint16 agreed, uint16 answered, bytes calldata signature) external;\n```\n\n- The name is yours; the parameter types are fixed. `reason` is a short code right-padded to 32\n  bytes: `bytes32(\"disagreed\")`, `bytes32(\"body_invalid\")`, `bytes32(\"body_refused\")`, … `agreed`\n  and `answered` are the panel's counts, zero without a panel. `signature` is empty for now.\n- Check `msg.sender == intake` and that the request is one this contract made and has not\n  settled, exactly as in the callback. Then clear it and record the reason. The values are\n  unsigned: never move money on them.\n- `requestWithFailure` exists on version 2 only; on version 1 a call to it reverts. Copy the\n  example's `failureHook` flag (owner-set, off by default, `request` while off) so the contract\n  works against either, and turn it on against version 2.\n- Every pending request that holds funds, or stops a new ask, still needs a deadline after which\n  its requester can clear it: a failure call can run out of gas or never come.\n\nFor a delivery on another chain, `\"onFailure\":\"<selector>\"` inside `deliver` sends the same call\nthrough `IntakeDelivery`, from either intake version.\n\n## The body\n\nThe body is the UTF-8 JSON the HTTP door takes for the action (`PaidOracleInput`), compacted,\nstrict: an unknown key refuses the whole request.\n\n```json\n{\"v\":1,\"question\":\"…\",\"chainId\":1,\"window\":{\"hours\":1},\"answerType\":\"bool\",\"evidence\":\"panel\",\"panelSize\":30,\"quorum\":20,\"validForSeconds\":900,\"definitions\":{\"credible\":\"…\"}}\n```\n\n- `question` 1–2000 characters; each `definitions` value at most 512, each key at most 64;\n  `panelSize` from 2 up to the paid door's cap (100 today; `limits.maxPanelSize` from `POST /requests/check`), `quorum <= panelSize`, and a larger panel is refused with the price spent; `validForSeconds` 60 to 30 days;\n  `window` is `{\"hours\":1..720}` or `{\"fromBlock\",\"toBlock\"}`; `evidence` `\"panel\"` for facts\n  that are not on a chain, `\"chain\"` (the default) for facts a recipe can reproduce from blocks.\n- A question the wording screen finds ambiguous, or that a panel cannot answer from public\n  sources, is refused (status 1). Pin the reading in `definitions`. `allowAmbiguous: true` skips\n  both screens; use it only when the definitions already pin the reading.\n- **Check the question before you bake it.** A question written into a contract cannot change\n  once deployed, so one the quote refuses is refused on every attempt, each spending the price.\n  Run the body through `POST /requests/check` (`{\"action\":\"oracle.request\",\"input\":{\"question\",\n  \"panelSize\",\"evidence\"}}`, free, no credential) and ship only a question it returns no blockers\n  for; say what it answered in your summary. The verifier sends every `\"question\":\"…\"` it finds in\n  your sources to the same check, and a refusal comes back as a finding that reopens your step.\n- **User text is untrusted.** Validate it as UTF-8 and bound its length. JSON-escape it (`\"`,\n  `\\`, control characters) so the body stays valid. Also keep it from reading as your own\n  instructions to the panel: quote it, say in the question that it is the user's untrusted text\n  to judge, and neutralise the delimiter you quote it with (for example, replace `\"` in the\n  user's text before quoting it), since the panel reads the decoded question.\n- At most 16 KiB of body.\n\n## An answer on another chain\n\nPay on one chain and receive on another: add\n`\"deliver\":{\"chainId\":<B>,\"target\":\"<receiver on B>\",\"selector\":\"<callback selector>\",\"onFailure\":\"<failure selector>\"}`\nto the body (`onFailure` optional) and pass **no** callback (target zero) to `request`. The answer is signed for the receiver\non B. The plane completes the request on the asking chain without a callback, then its writer\ncalls `IntakeDelivery.deliver` on B, which calls the receiver once under the same 200 000 gas.\n\n- The receiver checks `msg.sender == IntakeDelivery` on its own chain. The callback's first\n  argument is then the source chain's intake request id, which nothing on B can check, and the\n  signature only proves the answer was signed for this receiver, not that its owner asked: pin the\n  `questionHash` (see [What a callback proves](#what-a-callback-proves)).\n- Deliveries are live on 1 and 4663. A `deliver` to another chain, or beside a callback, is\n  refused and the price is spent.\n- The delivery is its own transaction, minutes after the payment. It is not atomic with anything\n  on the asking chain.\n\n## Ownership and deployment\n\nA launch factory, not a person, deploys launch contracts, so `msg.sender` in a constructor is\nthe factory. Take the owner as a constructor argument, using the launch's owner placeholder where\nthe manifest has one; never `Ownable(msg.sender)` on a contract the factory deploys. If the brief\ngives no owner and the manifest has no placeholder for it, report that the brief is missing an\ninput. Never fill an address argument with the zero address, `0x…dEaD` or any other stand-in: the\nverifier refuses them.\n\n## Tests\n\n- Copy the conformance test from `REFERENCE.md`. It deploys your consumer at the protocol's\n  vector address on the vector's chain and checks that your verification reproduces the\n  protocol's digest and accepts the protocol's signature. It is the one test that proves the\n  struct, the type string and the domain match what the oracle signs. Self-consistent tests\n  (your own key signing your own digest) do not.\n- Mock the intake: a contract that records `request(...)`, pulls the price, and can call the\n  stored callback with `abi.encode(requestId, a, signature)` from its own address. Test the\n  callback from the wrong sender, for an unknown or settled id, with an expired or tampered\n  attestation, with a \"no\" answer, under a 200 000 gas limit, and the clear-after-deadline path.\n- A contract that pins questions keeps one protocol vector beside its own tests, so its copy of\n  `OracleQuestion` is checked against the oracle rather than against itself: the question\n  `\"Which Uniswap v4 pool did the most WETH volume in the last day?\"`, chain 1, window 100 to 200,\n  answer type `bytes32[]` (5), head 1, one definition `denomination` =\n  `\"WETH, summed as the absolute swapped amount on the WETH side\"`, hashes to\n  `0x2117f4362ebfa37aa8a8c0fed548604fe09ac46faf8ae7559cd64780f26a46fb`, the `questionHash` of the\n  conformance test's signed attestation. Test that the answer to another pending request's\n  question is refused."},{"path":".imd/reads/skills/oracle-consumer/REFERENCE.md","digest":"4cfdc9d5f2172328d5502c8a88305ad623938dd5b5563dcd4ded7c6586a7f4b7","content":"# Oracle consumer: the canonical Solidity\n\nGenerated by `scripts/generate-skill-references.mjs` from the protocol's contracts; do not edit by hand.\nThese are the files the live oracle and intake are built and tested against.\nCopy them, adjusting only import paths and the pragma if your project pins another 0.8 compiler.\n\n## `OracleAttestation.sol`\n\nThe library and the base contract. Copy it whole into your `src/`; inherit `OracleAttestationConsumer`. It needs OpenZeppelin's `EIP712` and `SignatureChecker`.\n\n```solidity\n// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {EIP712} from \"@openzeppelin/contracts/utils/cryptography/EIP712.sol\";\nimport {SignatureChecker} from \"@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol\";\n\n/// @title The reasoning oracle's attestation, as a contract reads it\n/// @notice The oracle answers a typed question off chain — a panel of agents reads it, a deployer\n/// reproduces the answer from the chain — and signs the result as EIP-712 typed data in the\n/// *consumer's* domain: this chain, this contract. A signature for one consumer is meaningless to\n/// another, which is the property that lets one attester key serve every consumer without any of\n/// them being able to replay an answer into a neighbour.\n///\n/// @dev This library is the Solidity half of `oracle-eip712.ts` in `@identitymd/protocol`. The\n/// struct, the type string and the domain name are copied from there field for field, and the\n/// cross-implementation vector in `test/OracleAttestation.t.sol` is what keeps them equal: a digest\n/// computed by viem on one side and `_hashTypedDataV4` on the other, over the same values. Change\n/// either half and the vector fails before anything is signed for real.\nlibrary OracleAttestation {\n    /// @dev Field order is the typed data's. `answer` is `abi.encode` of the value under\n    /// `answerType`, so a consumer reads it back with one `abi.decode`; the hash covers\n    /// `keccak256(answer)` as EIP-712 requires for a dynamic field.\n    struct Attestation {\n        /// @dev The request's UUID as sixteen raw bytes, left-aligned. The natural replay key.\n        bytes32 requestId;\n        /// @dev The chain the question is *about* — the one the recipe ran on. Not necessarily the\n        /// consumer's chain, which is in the domain instead.\n        uint256 chainId;\n        /// @dev keccak-256 of the canonical question document. A consumer that pins its question\n        /// compares this, so an answer to a different question cannot be presented as its own.\n        bytes32 questionHash;\n        uint8 answerType;\n        bytes answer;\n        /// @dev The figure behind the answer where there is one: the sum, the leader's volume, the\n        /// value compared. Zero otherwise.\n        uint256 figure;\n        uint64 fromBlock;\n        uint64 toBlock;\n        /// @dev The closing block's hash, so a reorg cannot quietly change what was answered.\n        bytes32 blockHash;\n        /// @dev The panel job's UUID, the same way. Its page and receipt hold the evidence.\n        bytes32 panelJobId;\n        /// @dev How many seats the request opened. With `quorum`, what the requester asked for, so a\n        /// consumer that does not pin its question can still refuse a panel of two.\n        uint16 panelSize;\n        /// @dev How many answers had to agree for the panel to settle.\n        uint16 quorum;\n        /// @dev How many members gave the answer signed. At least `quorum` when the panel agreed;\n        /// below it only for chain evidence, when members who ran one recipe split and the\n        /// deployer's own rerun settled which of their answers was whole. A consumer that wants the\n        /// panel's agreement itself, not the rerun's, requires `agreed >= quorum`.\n        uint16 agreed;\n        uint64 issuedAt;\n        uint64 expiresAt;\n    }\n\n    /// @dev The `answerType` codes. They are the protocol enum's order and are appended to, never\n    /// reordered, because a code is what a signed attestation carries.\n    uint8 internal constant ANSWER_BOOL = 0;\n    uint8 internal constant ANSWER_ADDRESS = 1;\n    uint8 internal constant ANSWER_BYTES32 = 2;\n    uint8 internal constant ANSWER_UINT256 = 3;\n    uint8 internal constant ANSWER_ADDRESS_LIST = 4;\n    uint8 internal constant ANSWER_BYTES32_LIST = 5;\n\n    string internal constant DOMAIN_NAME = \"IdentityMD Oracle\";\n    /// @dev Version 2 added `panelSize`, `quorum` and `agreed`. A version 1 signature does not\n    /// verify here and a version 2 one does not verify against a version 1 consumer.\n    string internal constant DOMAIN_VERSION = \"2\";\n\n    bytes32 internal constant TYPEHASH = keccak256(\n        \"OracleAttestation(bytes32 requestId,uint256 chainId,bytes32 questionHash,uint8 answerType,bytes answer,uint256 figure,uint64 fromBlock,uint64 toBlock,bytes32 blockHash,bytes32 panelJobId,uint16 panelSize,uint16 quorum,uint16 agreed,uint64 issuedAt,uint64 expiresAt)\"\n    );\n\n    /// @notice The EIP-712 `hashStruct` of an attestation: what goes under the domain separator.\n    /// @dev Encoded in two halves because sixteen values in one `abi.encode` is too deep a stack\n    /// for the legacy code generator, and a consumer should not need via-IR to inherit this. Every\n    /// value is a static 32-byte word, so the two halves concatenated are byte for byte the one\n    /// encoding EIP-712 specifies.\n    function hashStruct(Attestation calldata a) internal pure returns (bytes32) {\n        return keccak256(\n            bytes.concat(\n                abi.encode(\n                    TYPEHASH,\n                    a.requestId,\n                    a.chainId,\n                    a.questionHash,\n                    a.answerType,\n                    keccak256(a.answer),\n                    a.figure,\n                    a.fromBlock\n                ),\n                abi.encode(\n                    a.toBlock, a.blockHash, a.panelJobId, a.panelSize, a.quorum, a.agreed, a.issuedAt, a.expiresAt\n                )\n            )\n        );\n    }\n}\n\n/// @title What a contract inherits to accept oracle attestations\n/// @notice Verifies that an attestation was signed by this consumer's oracle signer, for this\n/// consumer, and is inside its validity window. What the consumer then *does* with the answer is\n/// its own business: this contract decodes, it never acts.\n///\n/// @dev Replay is deliberately not tracked here. Some consumers want one action per request; some\n/// want to read the same attestation from several functions; some key on `questionHash` rather than\n/// `requestId`. `consumed` and `_consume` are offered for the first kind and cost nothing to the\n/// others. A consumer that neither consumes nor keys on something else **accepts the same signed\n/// answer as many times as it is presented** — for as long as it is valid — and must want that.\n///\n/// The signer is one address, so the natural rotation story is to point it at an\n/// `OracleSignerRegistry`, which answers ERC-1271 for the keys it lists; `SignatureChecker` takes\n/// that path for any signer with code. A consumer that prefers to hold a bare key overrides\n/// nothing and rotates through `_setOracleSigner`.\nabstract contract OracleAttestationConsumer is EIP712 {\n    using OracleAttestation for OracleAttestation.Attestation;\n\n    error AttestationExpired(uint64 expiresAt);\n    error AttestationNotYetValid(uint64 issuedAt);\n    error BadSignature();\n    error AlreadyConsumed(bytes32 requestId);\n    error WrongAnswerType(uint8 expected, uint8 got);\n    error ZeroSigner();\n\n    event OracleSignerSet(address indexed signer);\n\n    /// @dev How far ahead of this chain's clock an `issuedAt` may sit. The attester stamps with\n    /// its own wall clock; a block's timestamp is a validator's. Five minutes is generous for\n    /// honest drift and still short of anything that could be called pre-signing.\n    uint64 public constant ISSUED_AT_TOLERANCE = 5 minutes;\n\n    /// @notice The address every attestation must verify against: a key, or a registry of keys.\n    address public oracleSigner;\n\n    /// @notice Request ids this consumer has acted on, for consumers that call `_consume`.\n    mapping(bytes32 => bool) public consumed;\n\n    constructor(address signer) EIP712(OracleAttestation.DOMAIN_NAME, OracleAttestation.DOMAIN_VERSION) {\n        _setOracleSigner(signer);\n    }\n\n    /// @notice The digest the oracle signed: this consumer's domain over the attestation.\n    /// @dev Public so an off-chain party — the attester, a test, a reader — can ask the consumer\n    /// what it will check rather than reimplementing the domain. Equal to `oracleAttestationDigest`\n    /// in the protocol package for the same consumer and message.\n    function attestationDigest(OracleAttestation.Attestation calldata a) public view returns (bytes32) {\n        return _hashTypedDataV4(a.hashStruct());\n    }\n\n    /// @notice Reverts unless the attestation is current and signed for this consumer.\n    /// @dev Checks the window first because a stale signature is the common failure and the cheap\n    /// one to report. Does not check `a.chainId` — the chain the question was about is a fact\n    /// about the question, and whether it is the right one is for the consumer to decide alongside\n    /// `questionHash`. A validator can nudge `block.timestamp` by seconds; the window is minutes\n    /// to days wide and the tolerance absorbs the other end, so that is not a lever here.\n    function _verifyAttestation(OracleAttestation.Attestation calldata a, bytes calldata signature) internal view {\n        // forge-lint: disable-next-line(block-timestamp)\n        if (block.timestamp > a.expiresAt) revert AttestationExpired(a.expiresAt);\n        // forge-lint: disable-next-line(block-timestamp)\n        if (a.issuedAt > block.timestamp + ISSUED_AT_TOLERANCE) revert AttestationNotYetValid(a.issuedAt);\n        if (!SignatureChecker.isValidSignatureNowCalldata(oracleSigner, attestationDigest(a), signature)) {\n            revert BadSignature();\n        }\n    }\n\n    /// @notice Marks a request id as acted on, once.\n    /// @dev Opt-in. Call it after `_verifyAttestation` and before the effect, in the same function,\n    /// so a re-presented attestation fails here rather than repeating the effect.\n    function _consume(bytes32 requestId) internal {\n        if (consumed[requestId]) revert AlreadyConsumed(requestId);\n        consumed[requestId] = true;\n    }\n\n    /// @dev The hook a consumer wires to whatever authority it has. Not exposed here: this contract\n    /// does not know who is allowed to rotate, and an unguarded public setter would be the whole\n    /// oracle's key in anyone's hands. The zero address is refused because a consumer with no\n    /// signer only finds out on its first attestation, with `BadSignature`, which points nowhere.\n    function _setOracleSigner(address signer) internal {\n        if (signer == address(0)) revert ZeroSigner();\n        oracleSigner = signer;\n        emit OracleSignerSet(signer);\n    }\n\n    function decodeBool(OracleAttestation.Attestation calldata a) internal pure returns (bool) {\n        _expectType(a, OracleAttestation.ANSWER_BOOL);\n        return abi.decode(a.answer, (bool));\n    }\n\n    function decodeAddress(OracleAttestation.Attestation calldata a) internal pure returns (address) {\n        _expectType(a, OracleAttestation.ANSWER_ADDRESS);\n        return abi.decode(a.answer, (address));\n    }\n\n    function decodeBytes32(OracleAttestation.Attestation calldata a) internal pure returns (bytes32) {\n        _expectType(a, OracleAttestation.ANSWER_BYTES32);\n        return abi.decode(a.answer, (bytes32));\n    }\n\n    function decodeUint256(OracleAttestation.Attestation calldata a) internal pure returns (uint256) {\n        _expectType(a, OracleAttestation.ANSWER_UINT256);\n        return abi.decode(a.answer, (uint256));\n    }\n\n    function decodeAddressList(OracleAttestation.Attestation calldata a) internal pure returns (address[] memory) {\n        _expectType(a, OracleAttestation.ANSWER_ADDRESS_LIST);\n        return abi.decode(a.answer, (address[]));\n    }\n\n    function decodeBytes32List(OracleAttestation.Attestation calldata a) internal pure returns (bytes32[] memory) {\n        _expectType(a, OracleAttestation.ANSWER_BYTES32_LIST);\n        return abi.decode(a.answer, (bytes32[]));\n    }\n\n    /// @dev The type is signed, so decoding under another is not a parsing accident to tolerate:\n    /// an address read as a uint256 is a number that means nothing and a uint256 read as an address\n    /// is twelve bytes of somebody's balance, silently dropped.\n    function _expectType(OracleAttestation.Attestation calldata a, uint8 expected) private pure {\n        if (a.answerType != expected) revert WrongAnswerType(expected, a.answerType);\n    }\n}\n```\n\n## `OracleQuestion.sol`\n\nComputes a question's `questionHash` on chain, and the request body from the same values, for a contract that pins the questions it asks. Copy it whole beside `OracleAttestation.sol`; it needs OpenZeppelin's `Strings`.\n\n```solidity\n// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Strings} from \"@openzeppelin/contracts/utils/Strings.sol\";\nimport {OracleAttestation} from \"./OracleAttestation.sol\";\n\n/// @title The oracle question's hash, computed on chain\n/// @notice Every attestation signs a `questionHash`: keccak-256 of the canonical question document,\n/// which holds the question, its chain, its block window, its answer type and its definitions. A\n/// contract that computes that hash itself, before it asks, can refuse every answer to any other\n/// question. Store the hash beside the pending request and compare `a.questionHash` in the\n/// callback: the signature then says which request the answer is for, and an answer paired with\n/// the wrong request, by mistake or on purpose, is refused rather than acted on.\n///\n/// The hash is known in advance only for an absolute window (`{\"fromBlock\", \"toBlock\"}`) on the\n/// question's chain, whose last block is at least five blocks deep when the plane reads the ask.\n/// A relative window (`{\"hours\": N}`) resolves to blocks only then. On an Arbitrum chain, which\n/// Robinhood Chain is, `block.number` is an Ethereum block number: read the chain's own from\n/// `ArbSys(address(100)).arbBlockNumber()`.\n///\n/// This computes the hash; it does not judge whether the plane accepts the question. Check a body\n/// with a real subject in it at `POST /requests/check` before a contract is deployed with it.\n///\n/// @dev The Solidity half of `questionDocument` and `canonicalize` in `@identitymd/protocol`:\n/// object keys sorted, strings as `JSON.stringify` writes them, integers in decimal. The same\n/// documents and hashes are pinned in `test/OracleQuestion.t.sol` and in the protocol package's\n/// `test/oracle.test.ts`, so a change to either half fails a test before anything is signed.\n///\n/// It refuses what it cannot reproduce exactly, rather than hash bytes the plane will never hash:\n/// - text outside ASCII. The plane reads a body's bytes as UTF-8, replacing what is not, and only\n///   ASCII is checked here;\n/// - a value that is exactly an address or a 32-byte hex string with an uppercase digit. The intake\n///   lowercases such a value before anything reads it; hex inside longer text is left as written;\n/// - definitions out of order. The document sorts its keys, so they are taken already sorted,\n///   byte by byte, which is JavaScript's order for ASCII keys; a repeated key is out of order too;\n/// - a number above 2^53 - 1, which JSON cannot carry exactly to the plane.\nlibrary OracleQuestion {\n    /// @dev One of the definitions the requester pins: what a word in the question means.\n    struct Definition {\n        string key;\n        string value;\n    }\n\n    struct Question {\n        string question;\n        /// @dev The chain the question is about, and whose blocks the window counts.\n        uint256 chainId;\n        uint256 fromBlock;\n        uint256 toBlock;\n        /// @dev One of `OracleAttestation`'s `ANSWER_*` codes.\n        uint8 answerType;\n        /// @dev For a list answer: how many leading positions are used. Zero for none.\n        uint8 head;\n        /// @dev `\"evidence\": \"panel\"`, a fact that is not on any chain. False for chain evidence,\n        /// the default, which the document leaves out.\n        bool panelEvidence;\n        /// @dev Ascending by key. Empty for none: then the body must leave `definitions` out too,\n        /// since `\"definitions\": {}` is a member of the document.\n        Definition[] definitions;\n    }\n\n    error UnsupportedCharacter(uint256 index);\n    error NotLowercaseHex();\n    error DefinitionsOutOfOrder(uint256 index);\n    error UnknownAnswerType(uint8 answerType);\n    error NumberTooLarge(uint256 value);\n    error WindowReversed(uint256 fromBlock, uint256 toBlock);\n\n    /// @dev The largest integer a JSON number carries exactly to the plane: `Number.MAX_SAFE_INTEGER`.\n    uint256 internal constant MAX_SAFE_INTEGER = 9_007_199_254_740_991;\n\n    bytes16 private constant HEX_DIGITS = \"0123456789abcdef\";\n\n    /// @notice The `questionHash` an attestation for this question carries.\n    function hash(Question memory q) internal pure returns (bytes32) {\n        return keccak256(document(q));\n    }\n\n    /// @notice The canonical question document, the bytes `hash` covers.\n    function document(Question memory q) internal pure returns (bytes memory) {\n        return bytes.concat(_members(q), \"}\");\n    }\n\n    /// @notice A complete `oracle.request` body for this question: the document's members, then the\n    /// panel and how long the answer stays valid. The body and the hash come from the same values,\n    /// so the question the plane hashes is the question this contract pinned. A body that needs\n    /// more members (`allowAmbiguous`, `toleranceBps`, `consumer`) is built the same way, with\n    /// `jsonString` for any text.\n    function body(Question memory q, uint256 panelSize, uint256 quorum, uint256 validForSeconds)\n        internal\n        pure\n        returns (bytes memory)\n    {\n        return bytes.concat(\n            _members(q),\n            ',\"panelSize\":',\n            _number(panelSize),\n            ',\"quorum\":',\n            _number(quorum),\n            ',\"validForSeconds\":',\n            _number(validForSeconds),\n            \"}\"\n        );\n    }\n\n    /// @notice `text` as `JSON.stringify` writes it, quotes included: a quote, a backslash and the\n    /// control characters escaped, everything else as it is. Reverts on a byte outside ASCII.\n    function jsonString(string memory text) internal pure returns (bytes memory quoted) {\n        bytes memory raw = bytes(text);\n        uint256 length = raw.length;\n        // How many bytes the escapes add. Most text has none, and is copied whole below.\n        uint256 added = 0;\n        unchecked {\n            for (uint256 i; i < length; ++i) {\n                uint256 c = _byteAt(raw, i);\n                if (c >= 0x80) revert UnsupportedCharacter(i);\n                if (c < 0x20) added += _shortEscape(c) != 0 ? 1 : 5;\n                else if (c == 0x22 || c == 0x5c) added += 1;\n            }\n        }\n        if (added == 0) return bytes.concat('\"', raw, '\"');\n\n        // Sized exactly by the count above, so every write lands inside it.\n        quoted = new bytes(length + added + 2);\n        unchecked {\n            uint256 j = _put(quoted, 0, 0x22);\n            for (uint256 i; i < length; ++i) {\n                uint256 c = _byteAt(raw, i);\n                if (c >= 0x20 && c != 0x22 && c != 0x5c) {\n                    j = _put(quoted, j, c);\n                    continue;\n                }\n                j = _put(quoted, j, 0x5c);\n                if (c >= 0x20) {\n                    j = _put(quoted, j, c);\n                    continue;\n                }\n                uint256 letter = uint8(_shortEscape(c));\n                if (letter != 0) {\n                    j = _put(quoted, j, letter);\n                    continue;\n                }\n                j = _put(quoted, j, 0x75);\n                j = _put(quoted, j, 0x30);\n                j = _put(quoted, j, 0x30);\n                j = _put(quoted, j, uint8(HEX_DIGITS[c >> 4]));\n                j = _put(quoted, j, uint8(HEX_DIGITS[c & 0x0f]));\n            }\n            _put(quoted, j, 0x22);\n        }\n    }\n\n    /// @dev The byte at `i`, which the caller keeps below the length: no bounds check.\n    function _byteAt(bytes memory raw, uint256 i) private pure returns (uint256 c) {\n        assembly (\"memory-safe\") {\n            c := byte(0, mload(add(add(raw, 0x20), i)))\n        }\n    }\n\n    /// @dev Writes one byte at `at` and returns the next position.\n    function _put(bytes memory out, uint256 at, uint256 c) private pure returns (uint256) {\n        assembly (\"memory-safe\") {\n            mstore8(add(add(out, 0x20), at), c)\n        }\n        return at + 1;\n    }\n\n    /// @dev The document without its closing brace, so a body can carry on from it. Keys in\n    /// sorted order: answerType, chainId, definitions, evidence, head, question, v, window.\n    function _members(Question memory q) private pure returns (bytes memory members) {\n        if (q.fromBlock > q.toBlock) revert WindowReversed(q.fromBlock, q.toBlock);\n        _checkHexValue(bytes(q.question));\n        members = bytes.concat('{\"answerType\":\"', _answerType(q.answerType), '\",\"chainId\":', _number(q.chainId));\n        if (q.definitions.length > 0) members = bytes.concat(members, ',\"definitions\":', _definitions(q.definitions));\n        if (q.panelEvidence) members = bytes.concat(members, ',\"evidence\":\"panel\"');\n        if (q.head != 0) members = bytes.concat(members, ',\"head\":', _number(q.head));\n        members = bytes.concat(\n            members,\n            ',\"question\":',\n            jsonString(q.question),\n            ',\"v\":1,\"window\":{\"fromBlock\":',\n            _number(q.fromBlock),\n            ',\"toBlock\":',\n            _number(q.toBlock),\n            \"}\"\n        );\n    }\n\n    function _definitions(Definition[] memory definitions) private pure returns (bytes memory object) {\n        object = \"{\";\n        for (uint256 i; i < definitions.length; ++i) {\n            if (i > 0 && !_sortsBefore(bytes(definitions[i - 1].key), bytes(definitions[i].key))) {\n                revert DefinitionsOutOfOrder(i);\n            }\n            _checkHexValue(bytes(definitions[i].value));\n            object = bytes.concat(\n                object,\n                i == 0 ? bytes(\"\") : bytes(\",\"),\n                jsonString(definitions[i].key),\n                \":\",\n                jsonString(definitions[i].value)\n            );\n        }\n        object = bytes.concat(object, \"}\");\n    }\n\n    function _answerType(uint8 answerType) private pure returns (bytes memory) {\n        if (answerType == OracleAttestation.ANSWER_BOOL) return \"bool\";\n        if (answerType == OracleAttestation.ANSWER_ADDRESS) return \"address\";\n        if (answerType == OracleAttestation.ANSWER_BYTES32) return \"bytes32\";\n        if (answerType == OracleAttestation.ANSWER_UINT256) return \"uint256\";\n        if (answerType == OracleAttestation.ANSWER_ADDRESS_LIST) return \"address[]\";\n        if (answerType == OracleAttestation.ANSWER_BYTES32_LIST) return \"bytes32[]\";\n        revert UnknownAnswerType(answerType);\n    }\n\n    function _number(uint256 value) private pure returns (bytes memory) {\n        if (value > MAX_SAFE_INTEGER) revert NumberTooLarge(value);\n        return bytes(Strings.toString(value));\n    }\n\n    /// @dev The intake's `lowercaseHexValues`: a value that is exactly `0x` and 40 or 64 hex digits is\n    /// read in lowercase. One with an uppercase digit is refused here, where it would otherwise hash\n    /// as written and never match.\n    function _checkHexValue(bytes memory value) private pure {\n        uint256 length = value.length;\n        if ((length != 42 && length != 66) || value[0] != \"0\" || value[1] != \"x\") return;\n        bool upper = false;\n        for (uint256 i = 2; i < length; ++i) {\n            uint8 c = uint8(value[i]);\n            if (c >= 0x41 && c <= 0x46) upper = true;\n            else if (!((c >= 0x30 && c <= 0x39) || (c >= 0x61 && c <= 0x66))) return;\n        }\n        if (upper) revert NotLowercaseHex();\n    }\n\n    /// @dev `JSON.stringify`'s two-character escapes: backspace, tab, newline, form feed, return.\n    function _shortEscape(uint256 c) private pure returns (bytes1) {\n        if (c == 0x08) return \"b\";\n        if (c == 0x09) return \"t\";\n        if (c == 0x0a) return \"n\";\n        if (c == 0x0c) return \"f\";\n        if (c == 0x0d) return \"r\";\n        return 0;\n    }\n\n    /// @dev Strictly before, byte by byte, a prefix first.\n    function _sortsBefore(bytes memory a, bytes memory b) private pure returns (bool) {\n        uint256 shorter = a.length < b.length ? a.length : b.length;\n        for (uint256 i; i < shorter; ++i) {\n            if (a[i] != b[i]) return a[i] < b[i];\n        }\n        return a.length < b.length;\n    }\n}\n```\n\n## `OracleIntakeConsumer.sol`\n\nAsks on the same chain and is called back by the `Intake`. It imports the protocol's `Intake` contract for its `Callback` struct and `request`; in your project use the `IIntake` interface from the skill instead.\n\n```solidity\n// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Ownable} from \"@openzeppelin/contracts/access/Ownable.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {SafeERC20} from \"@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol\";\nimport {Intake} from \"../Intake.sol\";\nimport {OracleAttestation, OracleAttestationConsumer} from \"../OracleAttestation.sol\";\n\n/// @title A contract that buys an oracle answer through the Intake and acts on it\n/// @notice The worked example behind `docs/onchain-intake.md`. It stores one request body, asks\n/// with `ask()`, and receives the signed attestation in `onOracleResult`, which is the callback\n/// shape for `oracle.request`: the intake's request id, the attestation struct and the attester's\n/// signature over it in this contract's EIP-712 domain. On a version 2 intake, with `failureHook`\n/// on, it asks with `requestWithFailure`, so a refused ask or a panel that did not agree calls\n/// `onOracleFailure` instead, and the request is no longer pending: the contract knows the price is\n/// spent and can ask again. A version 1 intake has no `requestWithFailure`, so the hook is off until\n/// the owner moves the contract to version 2 and turns it on.\n///\n/// It pays in the chain's IMD (an ERC-20 it holds, approved to the intake for exactly one price\n/// per ask) or, with `asset` zero, in the native coin it holds.\n///\n/// Everything that names the protocol is owner-settable and nothing is a constant: the intake\n/// address, the action id, the asset and price this contract is willing to pay, and the body.\n/// That is the upgrade path. When the plane publishes `oracle-2`, the owner sets the new action id\n/// and a body in the new shape; when the price moves, the owner moves `price`; nothing is\n/// redeployed.\ncontract OracleIntakeConsumer is OracleAttestationConsumer, Ownable {\n    using SafeERC20 for IERC20;\n\n    error NotTheIntake();\n    error UnknownRequest(bytes32 requestId);\n    error PanelTooWeak(uint16 panelSize, uint16 agreed);\n\n    event Asked(bytes32 indexed requestId);\n    event Answered(bytes32 indexed requestId, bytes answer, uint64 toBlock);\n    event Failed(bytes32 indexed requestId, uint8 status, bytes32 reason, uint16 agreed, uint16 answered);\n\n    Intake public intake;\n    bytes32 public action;\n    /// @notice The asset it pays in: the chain's IMD, or the zero address for the native coin.\n    address public asset;\n    uint256 public price;\n    bytes public body;\n    mapping(bytes32 requestId => bool) public pending;\n    /// @notice The last answer, `abi.encode`d under the request's answer type.\n    bytes public lastAnswer;\n    bytes32 public lastQuestionHash;\n    /// @notice Why the last request that ended without an answer did: `disagreed`, `body_invalid`, …\n    bytes32 public lastFailure;\n    /// @notice Whether `ask` names `onOracleFailure`. Only a version 2 intake takes it.\n    bool public failureHook;\n    /// @notice The smallest panel, and the fewest members agreeing, an answer must come from.\n    uint16 public minPanelSize;\n    uint16 public minAgreed;\n\n    constructor(\n        address owner_,\n        Intake intake_,\n        bytes32 action_,\n        address asset_,\n        uint256 price_,\n        address oracleSigner_,\n        bytes memory body_\n    )\n        OracleAttestationConsumer(oracleSigner_)\n        Ownable(owner_)\n    {\n        intake = intake_;\n        action = action_;\n        asset = asset_;\n        price = price_;\n        body = body_;\n    }\n\n    /// @notice Buys one answer. Anyone may trigger it; the contract pays from what it holds.\n    function ask() external returns (bytes32 requestId) {\n        // The intake is the owner's choice and the price is the owner's ceiling; a caller of ask()\n        // chooses neither, which is what makes it safe for anyone to trigger.\n        // An ERC-20 is approved for exactly this ask, and the intake pulls it in the same call.\n        uint256 value = asset == address(0) ? price : 0;\n        if (value == 0) IERC20(asset).forceApprove(address(intake), price);\n        Intake.Callback memory callback = Intake.Callback(address(this), this.onOracleResult.selector);\n        if (failureHook) {\n            // slither-disable-next-line arbitrary-send-eth\n            requestId = intake.requestWithFailure{value: value}(action, body, callback, this.onOracleFailure.selector, asset, price);\n        } else {\n            // slither-disable-next-line arbitrary-send-eth\n            requestId = intake.request{value: value}(action, body, callback, asset, price);\n        }\n        pending[requestId] = true;\n        emit Asked(requestId);\n    }\n\n    /// @notice The callback for `oracle.request`. Only the intake calls it, only for a request\n    /// this contract made, only with an attestation the oracle signer signed for this contract.\n    function onOracleResult(bytes32 requestId, OracleAttestation.Attestation calldata a, bytes calldata signature) external {\n        if (msg.sender != address(intake)) revert NotTheIntake();\n        if (!pending[requestId]) revert UnknownRequest(requestId);\n        // Signed with the answer: a panel smaller, or an agreement thinner, than the owner accepts\n        // is refused, whatever the body asked for.\n        if (a.panelSize < minPanelSize || a.agreed < minAgreed || a.agreed < a.quorum) revert PanelTooWeak(a.panelSize, a.agreed);\n        delete pending[requestId];\n        _verifyAttestation(a, signature);\n        _consume(a.requestId);\n        lastAnswer = a.answer;\n        lastQuestionHash = a.questionHash;\n        emit Answered(requestId, a.answer, a.toBlock);\n    }\n\n    /// @notice The failure function: the ask was refused (status 1) or ended without an answer\n    /// (status 2). `reason` is a short code, `agreed` and `answered` the panel's counts where there\n    /// was one. Nothing here is signed yet, so it only clears the request; it never pays anyone.\n    function onOracleFailure(bytes32 requestId, uint8 status, bytes32 reason, uint16 agreed, uint16 answered, bytes calldata)\n        external\n    {\n        if (msg.sender != address(intake)) revert NotTheIntake();\n        if (!pending[requestId]) revert UnknownRequest(requestId);\n        delete pending[requestId];\n        lastFailure = reason;\n        emit Failed(requestId, status, reason, agreed, answered);\n    }\n\n    // ---- the upgrade path: everything protocol-facing moves without a redeploy ----\n\n    function setIntake(Intake to) external onlyOwner {\n        intake = to;\n    }\n\n    function setPanelMinimums(uint16 panelSize, uint16 agreed) external onlyOwner {\n        minPanelSize = panelSize;\n        minAgreed = agreed;\n    }\n\n    /// @notice On only against a version 2 intake: a version 1 intake has no `requestWithFailure`.\n    function setFailureHook(bool on) external onlyOwner {\n        failureHook = on;\n    }\n\n    function setAction(bytes32 to, bytes calldata newBody) external onlyOwner {\n        action = to;\n        body = newBody;\n    }\n\n    /// @notice The asset and price move together: a price means nothing in another asset.\n    function setPayment(address asset_, uint256 price_) external onlyOwner {\n        asset = asset_;\n        price = price_;\n    }\n\n    /// @notice Takes back what the contract holds to pay with, in `token` or, as zero, the native coin.\n    function withdraw(address token, address to, uint256 amount) external onlyOwner {\n        if (token == address(0)) {\n            (bool ok,) = to.call{value: amount}(\"\");\n            require(ok, \"withdraw failed\");\n        } else {\n            IERC20(token).safeTransfer(to, amount);\n        }\n    }\n\n    function setBody(bytes calldata to) external onlyOwner {\n        body = to;\n    }\n\n    function setSigner(address to) external onlyOwner {\n        _setOracleSigner(to);\n    }\n\n    receive() external payable {}\n}\n```\n\n## `OracleBoundConsumer.sol`\n\nAsks one question per request, with the request's subject in it, stores the question's hash and takes only the answer signed for that hash. It imports the protocol's `Intake` for its `Callback` struct and `requestWithFailure`; in your project use the `IIntake` interface from the skill, with `requestWithFailure` added.\n\n```solidity\n// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Ownable} from \"@openzeppelin/contracts/access/Ownable.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {SafeERC20} from \"@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol\";\nimport {Strings} from \"@openzeppelin/contracts/utils/Strings.sol\";\nimport {Intake} from \"../Intake.sol\";\nimport {OracleAttestation, OracleAttestationConsumer} from \"../OracleAttestation.sol\";\nimport {OracleQuestion} from \"../OracleQuestion.sol\";\n\n/// @dev Arbitrum's system precompile, at `address(100)` on every Arbitrum chain.\ninterface IArbSys {\n    function arbBlockNumber() external view returns (uint256);\n}\n\n/// @title A contract that asks one question per request and acts only on the answer to it\n/// @notice The worked example for binding an answer to the request that asked it, in\n/// `docs/onchain-intake.md`. `ask(subject)` writes a question of its own with the subject in it,\n/// asks it through the `Intake`, and stores the question's hash beside the request. The callback\n/// takes an answer only when the oracle signed that same hash. The signature then says which\n/// subject the answer is about: an answer to another request's question, delivered to this one by\n/// mistake or on purpose, is refused, and so is any answer to a question this contract did not ask.\n///\n/// The question's window is one block, `WINDOW_DEPTH` behind this chain's head, so its hash is\n/// known when it is asked: the plane takes an absolute window once its last block is five deep. On\n/// an Arbitrum chain, which Robinhood Chain is, `block.number` is an Ethereum block number, so the\n/// owner deploys with `arbitrumBlocks` and the chain's own number comes from `ArbSys`.\n///\n/// Whoever asks pays: one price in `asset`, approved to this contract, pulled and forwarded in the\n/// same call. Nothing is held, so a refused ask spends only the asker's price, and a request that\n/// never hears back holds nothing and needs no deadline.\n///\n/// The wording, what a pass means, the panel and everything protocol-facing are owner-settable. A\n/// change applies from the next ask; one already pending keeps the hash it was asked with.\ncontract OracleBoundConsumer is OracleAttestationConsumer, Ownable {\n    using SafeERC20 for IERC20;\n\n    error NotTheIntake();\n    error UnknownRequest(bytes32 requestId);\n    error WrongQuestion(bytes32 asked, bytes32 answered);\n    error PanelTooWeak(uint16 panelSize, uint16 agreed);\n\n    event Asked(bytes32 indexed requestId, bytes32 indexed subject, bytes32 questionHash);\n    event Answered(bytes32 indexed requestId, bytes32 indexed subject, bool yes);\n    event Failed(bytes32 indexed requestId, bytes32 indexed subject, uint8 status, bytes32 reason);\n    event IntakeSet(Intake indexed intake);\n\n    /// @notice How the question reads and who answers it.\n    struct Config {\n        /// @dev The question is `questionPrefix`, the subject as `0x` and 64 lowercase hex digits,\n        /// then `questionSuffix`. ASCII only (`OracleQuestion`).\n        string questionPrefix;\n        string questionSuffix;\n        /// @dev The `pass` definition the panel answers by; empty for none.\n        string passMeans;\n        uint16 panelSize;\n        uint16 quorum;\n        uint32 validForSeconds;\n    }\n\n    struct Pending {\n        bytes32 subject;\n        bytes32 questionHash;\n    }\n\n    IArbSys internal constant ARB_SYS = IArbSys(address(100));\n    /// @notice How many blocks behind the head the window sits: five deep when the plane reads the\n    /// ask, with room for the blocks between this transaction and that read.\n    uint256 public constant WINDOW_DEPTH = 8;\n\n    /// @notice Whether this is an Arbitrum chain, whose own block number `ArbSys` gives.\n    bool public immutable arbitrumBlocks;\n\n    Intake public intake;\n    bytes32 public action;\n    IERC20 public asset;\n    uint256 public price;\n    Config internal config;\n\n    mapping(bytes32 requestId => Pending) public pending;\n    /// @notice Subjects the oracle answered yes for.\n    mapping(bytes32 subject => bool) public passed;\n\n    constructor(\n        address owner_,\n        Intake intake_,\n        bytes32 action_,\n        IERC20 asset_,\n        uint256 price_,\n        address oracleSigner_,\n        bool arbitrumBlocks_,\n        Config memory config_\n    )\n        OracleAttestationConsumer(oracleSigner_)\n        Ownable(owner_)\n    {\n        intake = intake_;\n        action = action_;\n        asset = asset_;\n        price = price_;\n        arbitrumBlocks = arbitrumBlocks_;\n        config = config_;\n    }\n\n    /// @notice Asks whether `subject` passed. The caller pays one price.\n    function ask(bytes32 subject) external returns (bytes32 requestId) {\n        OracleQuestion.Question memory q = question(subject, head() - WINDOW_DEPTH);\n        bytes32 questionHash = OracleQuestion.hash(q);\n        bytes memory body = OracleQuestion.body(q, config.panelSize, config.quorum, config.validForSeconds);\n        asset.safeTransferFrom(msg.sender, address(this), price);\n        asset.forceApprove(address(intake), price);\n        requestId = intake.requestWithFailure(\n            action,\n            body,\n            Intake.Callback(address(this), this.onOracleResult.selector),\n            this.onOracleFailure.selector,\n            address(asset),\n            price\n        );\n        pending[requestId] = Pending(subject, questionHash);\n        emit Asked(requestId, subject, questionHash);\n    }\n\n    /// @notice The callback for `oracle.request`: only from the intake, only for a request this\n    /// contract made, and only with the answer to the question that request asked.\n    function onOracleResult(bytes32 requestId, OracleAttestation.Attestation calldata a, bytes calldata signature) external {\n        if (msg.sender != address(intake)) revert NotTheIntake();\n        Pending memory p = pending[requestId];\n        // An unknown request reads as zero, and no question hashes to zero. (Slither sees a block\n        // number in the hash and calls the comparison dangerous; it is a presence check.)\n        // slither-disable-next-line incorrect-equality\n        if (p.questionHash == bytes32(0)) revert UnknownRequest(requestId);\n        // The binding: the hash is signed with the answer, so this is the answer to this request's\n        // question or it is refused, whoever paired the two.\n        if (a.questionHash != p.questionHash) revert WrongQuestion(p.questionHash, a.questionHash);\n        if (a.panelSize < config.panelSize || a.quorum < config.quorum || a.agreed < a.quorum) {\n            revert PanelTooWeak(a.panelSize, a.agreed);\n        }\n        delete pending[requestId];\n        _verifyAttestation(a, signature);\n        _consume(a.requestId);\n        bool yes = decodeBool(a);\n        if (yes) passed[p.subject] = true;\n        emit Answered(requestId, p.subject, yes);\n    }\n\n    /// @notice The failure function: the ask was refused or ended without an answer. Unsigned, so it\n    /// only clears this contract's own request.\n    function onOracleFailure(bytes32 requestId, uint8 status, bytes32 reason, uint16, uint16, bytes calldata) external {\n        if (msg.sender != address(intake)) revert NotTheIntake();\n        Pending memory p = pending[requestId];\n        // An unknown request reads as zero, and no question hashes to zero. (Slither sees a block\n        // number in the hash and calls the comparison dangerous; it is a presence check.)\n        // slither-disable-next-line incorrect-equality\n        if (p.questionHash == bytes32(0)) revert UnknownRequest(requestId);\n        delete pending[requestId];\n        emit Failed(requestId, p.subject, status, reason);\n    }\n\n    /// @notice The question `ask(subject)` asks with its window closing at `atBlock`.\n    function question(bytes32 subject, uint256 atBlock) public view returns (OracleQuestion.Question memory q) {\n        q.question = string.concat(config.questionPrefix, Strings.toHexString(uint256(subject), 32), config.questionSuffix);\n        q.chainId = block.chainid;\n        q.fromBlock = atBlock;\n        q.toBlock = atBlock;\n        q.answerType = OracleAttestation.ANSWER_BOOL;\n        q.panelEvidence = true;\n        if (bytes(config.passMeans).length > 0) {\n            q.definitions = new OracleQuestion.Definition[](1);\n            q.definitions[0] = OracleQuestion.Definition(\"pass\", config.passMeans);\n        }\n    }\n\n    /// @notice This chain's own block number.\n    function head() public view returns (uint256) {\n        return arbitrumBlocks ? ARB_SYS.arbBlockNumber() : block.number;\n    }\n\n    function getConfig() external view returns (Config memory) {\n        return config;\n    }\n\n    // ---- the upgrade path: everything protocol-facing moves without a redeploy ----\n\n    function setConfig(Config calldata to) external onlyOwner {\n        config = to;\n    }\n\n    /// @notice The only address whose callbacks this contract takes.\n    function setIntake(Intake to) external onlyOwner {\n        intake = to;\n        emit IntakeSet(to);\n    }\n\n    function setAction(bytes32 to) external onlyOwner {\n        action = to;\n    }\n\n    /// @notice The asset and price move together: a price means nothing in another asset.\n    function setPayment(IERC20 asset_, uint256 price_) external onlyOwner {\n        asset = asset_;\n        price = price_;\n    }\n\n    function setSigner(address to) external onlyOwner {\n        _setOracleSigner(to);\n    }\n}\n```\n\n## `OracleDeliveryReceiver.sol`\n\nReceives an answer bought on another chain, called by `IntakeDelivery` on its own chain.\n\n```solidity\n// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Ownable} from \"@openzeppelin/contracts/access/Ownable.sol\";\nimport {OracleAttestation, OracleAttestationConsumer} from \"../OracleAttestation.sol\";\n\n/// @title A contract that receives an oracle answer bought on another chain\n/// @notice The worked example for delivery across chains, in `docs/onchain-intake.md`. Someone\n/// asks on chain A's `Intake` with a body that names this contract on chain B:\n///\n///   \"deliver\": {\"chainId\": <B>, \"target\": \"<this contract>\", \"selector\": \"<onOracleResult's selector>\",\n///               \"onFailure\": \"<onOracleFailure's selector>\"}\n///\n/// The plane has the answer signed for this contract on B, and its writer hands it over through\n/// `IntakeDelivery` on B. A signature for this contract does not mean this contract's owner asked\n/// the question: anyone may pay for a question and name this contract as its receiver, and the\n/// answer comes signed for it all the same. Nothing on B can see who asked on A, so the question\n/// itself is what this contract checks: it takes an answer only to a question the owner pinned in\n/// advance by its `questionHash`, once, from a panel at least as large as the owner requires.\n///\n/// A question can be pinned only when its hash is known before it is asked, which takes an\n/// absolute window (`{\"fromBlock\", \"toBlock\"}`, already confirmed when asked): see\n/// `OracleQuestion.hash`, or `oracleQuestionHash` in the protocol package. A relative window\n/// (`{\"hours\": N}`) resolves when the plane reads the ask, so its hash cannot be pinned.\n///\n/// The delivery contract, the signer, the pinned questions and the panel minimums are\n/// owner-settable, as everything protocol-facing is.\ncontract OracleDeliveryReceiver is OracleAttestationConsumer, Ownable {\n    error NotTheDelivery();\n    error UnexpectedQuestion(bytes32 questionHash);\n    error PanelTooWeak(uint16 panelSize, uint16 agreed);\n\n    event Answered(bytes32 indexed sourceRequestId, bytes32 indexed oracleRequestId, bytes answer, uint64 toBlock);\n    event Failed(bytes32 indexed sourceRequestId, uint8 status, bytes32 reason, uint16 agreed, uint16 answered);\n    event QuestionExpected(bytes32 indexed questionHash, bool expected);\n    event PanelMinimumsSet(uint16 panelSize, uint16 agreed);\n\n    address public delivery;\n    /// @notice Questions this contract takes an answer to, by `questionHash`; each is answered once.\n    mapping(bytes32 questionHash => bool) public expectedQuestion;\n    /// @notice The smallest panel, and the fewest members agreeing, an answer must come from.\n    uint16 public minPanelSize;\n    uint16 public minAgreed;\n    /// @notice The last answer, `abi.encode`d under the request's answer type.\n    bytes public lastAnswer;\n    bytes32 public lastQuestionHash;\n    bytes32 public lastSourceRequestId;\n\n    constructor(address owner_, address delivery_, address oracleSigner_)\n        OracleAttestationConsumer(oracleSigner_)\n        Ownable(owner_)\n    {\n        delivery = delivery_;\n    }\n\n    /// @notice The callback for `oracle.request`, delivered from another chain.\n    function onOracleResult(bytes32 sourceRequestId, OracleAttestation.Attestation calldata a, bytes calldata signature) external {\n        if (msg.sender != delivery) revert NotTheDelivery();\n        // The signature says the oracle answered this question for this contract; only the pin says\n        // this contract wanted it answered.\n        if (!expectedQuestion[a.questionHash]) revert UnexpectedQuestion(a.questionHash);\n        // Signed with the answer: a panel smaller, or an agreement thinner, than the owner accepts\n        // is refused, whatever the body asked for.\n        if (a.panelSize < minPanelSize || a.agreed < minAgreed || a.agreed < a.quorum) revert PanelTooWeak(a.panelSize, a.agreed);\n        _verifyAttestation(a, signature);\n        _consume(a.requestId);\n        delete expectedQuestion[a.questionHash];\n        lastAnswer = a.answer;\n        lastQuestionHash = a.questionHash;\n        lastSourceRequestId = sourceRequestId;\n        emit Answered(sourceRequestId, a.requestId, a.answer, a.toBlock);\n    }\n\n    /// @notice The failure function, delivered from another chain when an ask naming this contract\n    /// was refused or its panel did not agree. Anyone can cause one, by asking with a bad body and\n    /// naming this contract, and nothing in it is signed, so it changes nothing here: it is a log.\n    function onOracleFailure(bytes32 sourceRequestId, uint8 status, bytes32 reason, uint16 agreed, uint16 answered, bytes calldata)\n        external\n    {\n        if (msg.sender != delivery) revert NotTheDelivery();\n        emit Failed(sourceRequestId, status, reason, agreed, answered);\n    }\n\n    /// @notice Pins (or unpins) a question this contract will take an answer to.\n    function expectQuestion(bytes32 questionHash, bool expected) external onlyOwner {\n        expectedQuestion[questionHash] = expected;\n        emit QuestionExpected(questionHash, expected);\n    }\n\n    function setPanelMinimums(uint16 panelSize, uint16 agreed) external onlyOwner {\n        minPanelSize = panelSize;\n        minAgreed = agreed;\n        emit PanelMinimumsSet(panelSize, agreed);\n    }\n\n    function setDelivery(address to) external onlyOwner {\n        delivery = to;\n    }\n\n    function setSigner(address to) external onlyOwner {\n        _setOracleSigner(to);\n    }\n}\n```\n\n## `OracleConsumerConformance.t.sol`\n\nThe conformance test. Copy it into `test/`, replace `deployConsumer` with your contract (deployed at `VECTOR_CONSUMER`, oracle signer `SIGNER`, trusting `caller()`), point `caller()` at whatever calls your callback (a mock intake for a same-chain consumer), and keep every vector constant as it is. Rename `onOracleResult` in `CALLBACK` if your callback has another name. If your consumer cannot use `IntakeDelivery`, drop that import and the delivered test, and keep the other three.\n\n```solidity\n// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {OracleAttestation} from \"../src/OracleAttestation.sol\";\nimport {IntakeDelivery} from \"../src/IntakeDelivery.sol\";\nimport {OracleDeliveryReceiver} from \"../src/examples/OracleDeliveryReceiver.sol\";\n\n/// @title The conformance test a consumer copies\n/// @notice Delivered whole to builders by the `oracle-consumer` skill. It is the one test that ties\n/// a consumer to what the oracle actually signs: the protocol's vector values, its digest and a\n/// signature made by `oracle-eip712.ts` (the same vector `OracleAttestation.t.sol` pins). A consumer\n/// whose struct, type string or domain differs from the protocol's passes its own self-signed tests\n/// and fails this one.\n///\n/// A builder replaces `deployConsumer` with its own contract, deployed at `VECTOR_CONSUMER` with\n/// `SIGNER` as its oracle signer and `caller()` as the address its callback trusts, and `CALLBACK`\n/// with its callback's name in the canonical signature.\ncontract OracleConsumerConformanceTest is Test {\n    // ---- the protocol's vector: do not change these ----\n    uint256 constant VECTOR_CHAIN = 11155111;\n    address constant VECTOR_CONSUMER = 0x0000000000000000000000000000000000002748;\n    bytes32 constant VECTOR_DIGEST = 0x95fefa8b7c529852f4e2b6aec888930eb2bf5078e6443a85808e36df19e1325c;\n    bytes constant VECTOR_SIGNATURE =\n        hex\"a26b14918607eb565af126beb54d3c5d19e923c41506def500b3521a4f9aa6d603ab44fd22f15dd2191732961a7131e4641244add8b0f09f20e6ae64381be8481b\";\n    /// @dev anvil's second account: the vector's attester. A test key, never a real one.\n    address constant SIGNER = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8;\n    uint256 constant SIGNER_KEY = 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d;\n    uint64 constant ISSUED_AT = 1800000000;\n    uint64 constant EXPIRES_AT = 1800003600;\n\n    /// @dev The callback's canonical signature. Its selector is what the intake calls: a struct that\n    /// differs from the protocol's by one field has another selector and is never reached.\n    string constant CALLBACK =\n        \"onOracleResult(bytes32,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\";\n\n    address constant WRITER = address(0x717E);\n    address constant SOURCE_INTAKE = 0x1397434cd35e8a9C8aC312A61D3A285EB31dea56;\n\n    IntakeDelivery delivery;\n    OracleDeliveryReceiver consumer;\n\n    function setUp() public {\n        vm.chainId(VECTOR_CHAIN);\n        vm.warp(ISSUED_AT);\n        delivery = new IntakeDelivery(address(this), WRITER);\n        deployConsumer();\n    }\n\n    /// @dev Replace with your consumer, at the vector's address, trusting `caller()`.\n    function deployConsumer() internal {\n        deployCodeTo(\n            \"OracleDeliveryReceiver.sol:OracleDeliveryReceiver\",\n            abi.encode(address(this), caller(), SIGNER),\n            VECTOR_CONSUMER\n        );\n        consumer = OracleDeliveryReceiver(VECTOR_CONSUMER);\n        // Whatever your consumer needs to take the vector's question as its own; this one pins it.\n        consumer.expectQuestion(vector().questionHash, true);\n    }\n\n    /// @dev Who calls your callback: `IntakeDelivery` for an answer from another chain, the\n    /// `Intake` (or a mock of it) for one from the same chain.\n    function caller() internal view returns (address) {\n        return address(delivery);\n    }\n\n    /// @dev The vector attestation: a `bytes32[]` answer with one element, figure 12345, blocks\n    /// 100–200, a panel of five with a quorum of four that agreed unanimously.\n    function vector() internal pure returns (OracleAttestation.Attestation memory a) {\n        bytes32[] memory ids = new bytes32[](1);\n        ids[0] = bytes32(uint256(1));\n        a = OracleAttestation.Attestation({\n            requestId: 0x0000000000004000800000000000000100000000000000000000000000000000,\n            chainId: 1,\n            questionHash: 0x2117f4362ebfa37aa8a8c0fed548604fe09ac46faf8ae7559cd64780f26a46fb,\n            answerType: OracleAttestation.ANSWER_BYTES32_LIST,\n            answer: abi.encode(ids),\n            figure: 12345,\n            fromBlock: 100,\n            toBlock: 200,\n            blockHash: bytes32(uint256(7)),\n            panelJobId: 0x0000000000004000800000000000000200000000000000000000000000000000,\n            panelSize: 5,\n            quorum: 4,\n            agreed: 5,\n            issuedAt: ISSUED_AT,\n            expiresAt: EXPIRES_AT\n        });\n    }\n\n    /// @notice The digest your consumer verifies is the one the oracle signs.\n    function test_digestMatchesTheProtocol() public view {\n        assertEq(consumer.attestationDigest(vector()), VECTOR_DIGEST, \"struct, type string or domain differs from the protocol's\");\n    }\n\n    /// @notice Your callback's selector is the one the intake calls.\n    function test_callbackSelectorIsCanonical() public view {\n        assertEq(consumer.onOracleResult.selector, bytes4(keccak256(bytes(CALLBACK))), \"callback parameters differ from the protocol's\");\n    }\n\n    /// @notice The protocol's own signature, delivered the way the writer delivers it, is accepted.\n    /// @dev A consumer that refuses the vector's answer type signs its own answer with `SIGNER_KEY`\n    /// instead; `test_digestMatchesTheProtocol` is what ties it to the protocol.\n    function test_acceptsTheProtocolSignatureAsDelivered() public {\n        bytes32 sourceRequestId = keccak256(\"source request\");\n        vm.prank(WRITER);\n        bool called = delivery.deliver(\n            4663,\n            SOURCE_INTAKE,\n            sourceRequestId,\n            VECTOR_CONSUMER,\n            bytes4(keccak256(bytes(CALLBACK))),\n            abi.encode(sourceRequestId, vector(), VECTOR_SIGNATURE)\n        );\n        assertTrue(called, \"the callback did not run to the end under the 200,000 gas stipend\");\n        assertTrue(consumer.consumed(vector().requestId));\n    }\n\n    /// @notice A fresh signature from the same key verifies too, so tests can sign their own answers.\n    function test_acceptsAFreshSignatureFromTheVectorKey() public {\n        OracleAttestation.Attestation memory a = vector();\n        a.requestId = keccak256(\"another request\");\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(SIGNER_KEY, consumer.attestationDigest(a));\n        vm.prank(caller());\n        consumer.onOracleResult(bytes32(0), a, abi.encodePacked(r, s, v));\n        assertTrue(consumer.consumed(a.requestId));\n    }\n}\n```\n"}]}